Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.1.5] - 2026-06-01

### Fixed

- **Critical: JSON repair corrupts valid unicode escapes** — `is_valid_json_escape()` missing `'u'`, causing `\uXXXX` to be double-escaped. Now properly validates and handles incomplete `\u` sequences (#89)
- **Critical: TOML injection in `save_api_key()`** — API key written via `format!` string interpolation. Now uses `toml::Table` serialization (#69)
- **Retry prompt improvement** — retry on parse failure now includes stricter JSON format instructions (#90)
- **Temp file race condition** — SARIF upload now uses PID-suffixed temp path instead of fixed filename (#70)
- **Confusing unused `_cli_api_key` parameter** — removed from `load_config()` signature (#75)

### Security

- `save_api_key()` now uses `toml::Table::insert()` instead of string interpolation (prevents TOML injection)
- Temp SARIF file path includes process ID (prevents TOCTOU race)

## [0.1.4] - 2026-06-01

### Added
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "cora-cli"
version = "0.1.4"
version = "0.1.5"
edition = "2024"
description = "CLI-first AI code review — BYOK, diff/scan/branch, pre-commit hooks"
license = "MIT"
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ Download the latest release from [GitHub Releases](https://github.com/ajianaz/co

```bash
# Determine your platform tag from the releases page, e.g.:
# cora-aarch64-unknown-linux-gnu-v0.1.4.tar.gz
# cora-x86_64-unknown-linux-gnu-v0.1.4.tar.gz
# cora-aarch64-apple-darwin-v0.1.4.tar.gz
# cora-x86_64-pc-windows-msvc-v0.1.4.zip
# cora-aarch64-unknown-linux-gnu-v0.1.5.tar.gz
# cora-x86_64-unknown-linux-gnu-v0.1.5.tar.gz
# cora-aarch64-apple-darwin-v0.1.5.tar.gz
# cora-x86_64-pc-windows-msvc-v0.1.5.zip

# Example: Linux aarch64
VERSION=$(curl -s https://api.github.com/repos/ajianaz/cora-cli/releases/latest | grep tag_name | cut -d'"' -f4)
Expand Down
2 changes: 1 addition & 1 deletion src/commands/config_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::config::schema::{CoraFile, HookSection, OutputSection, ProviderSectio

/// Execute `cora config show` — print the current resolved configuration.
pub fn execute_config_show() -> Result<()> {
let config = loader::load_config(None, None, None, None, None, None, false)?;
let config = loader::load_config(None, None, None, None, None, false)?;

println!(
"{}",
Expand Down
5 changes: 3 additions & 2 deletions src/config/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,6 @@ pub fn load_config(
cli_provider: Option<&str>,
cli_model: Option<&str>,
cli_base_url: Option<&str>,
_cli_api_key: Option<&str>,
cli_format: Option<&str>,
no_color: bool,
) -> Result<Config> {
Expand Down Expand Up @@ -387,7 +386,9 @@ pub fn save_api_key(key: &str) -> Result<()> {
std::fs::create_dir_all(&dir).with_context(|| format!("failed to create {}", dir.display()))?;

let path = dir.join(AUTH_FILENAME);
let content = format!("api_key = \"{key}\"\n");
let mut table = toml::Table::new();
table.insert("api_key".to_string(), toml::Value::String(key.to_string()));
let content = table.to_string();

std::fs::write(&path, content)
.with_context(|| format!("failed to write {}", path.display()))?;
Expand Down
27 changes: 24 additions & 3 deletions src/engine/llm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,13 +209,19 @@ pub async fn review_diff(
})
}
Err(e) => {
// LLM produced invalid JSON — retry once
// LLM produced invalid JSON — retry once with stricter prompt
debug!(error = %e, "first parse attempt failed, retrying LLM request");
spinner.set_message("Retrying (parse error)…");
let strict_prompt = format!(
"{}\n\nIMPORTANT: Your response MUST contain only valid JSON. \
Ensure all strings use proper JSON escape sequences. \
Do NOT use raw backslashes in string values.",
&user_prompt
);
let retry_raw = chat_completion(
llm_config,
REVIEW_SYSTEM_PROMPT,
&user_prompt,
&strict_prompt,
Some(&spinner),
)
.await?;
Expand Down Expand Up @@ -553,11 +559,26 @@ fn repair_invalid_escapes(input: &str) -> String {
chars.next(); // consume
if next == 'u' {
// Consume exactly 4 hex digits
let mut hex_count = 0;
for _ in 0..4 {
if let Some(&hex) = chars.peek() {
if hex.is_ascii_hexdigit() {
output.push(hex);
chars.next();
hex_count += 1;
}
}
}
if hex_count < 4 {
// Invalid \u escape — not enough hex digits
// Remove the \u we already output and repair
output.truncate(output.len() - 2);
output.push_str("\\\\u");
// Re-peek remaining chars that weren't consumed
for _ in 0..(4 - hex_count) {
if let Some(&c) = chars.peek() {
output.push(c);
chars.next();
}
}
}
Expand Down Expand Up @@ -603,7 +624,7 @@ fn repair_invalid_escapes(input: &str) -> String {

/// Check if a character is a valid JSON escape sequence starter.
fn is_valid_json_escape(c: char) -> bool {
matches!(c, '"' | '\\' | '/' | 'b' | 'f' | 'n' | 'r' | 't')
matches!(c, '"' | '\\' | '/' | 'b' | 'f' | 'n' | 'r' | 't' | 'u')
}

/// Strip ```json / ``` code fences from the response.
Expand Down
6 changes: 1 addition & 5 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -429,11 +429,9 @@ async fn cmd_review(globals: &GlobalOptions, opts: ReviewOpts) -> Result<i32> {
globals.provider.as_deref(),
globals.model.as_deref(),
globals.base_url.as_deref(),
globals.api_key.as_deref(),
globals.format.as_deref(),
globals.no_color,
)?;

let llm_config = loader::build_llm_config(&config, globals.api_key.as_deref())?;

// If --upload is set, force SARIF format
Expand Down Expand Up @@ -495,7 +493,7 @@ async fn upload_sarif_content(

// Write SARIF to a temp file and upload it
let tmp_dir = std::env::temp_dir();
let tmp_path = tmp_dir.join("cora-sarif-upload.json");
let tmp_path = tmp_dir.join(format!("cora-sarif-upload-{}.json", std::process::id()));

{
let mut file = std::fs::File::create(&tmp_path)?;
Expand Down Expand Up @@ -529,11 +527,9 @@ async fn cmd_scan(globals: &GlobalOptions, opts: ScanOpts) -> Result<i32> {
globals.provider.as_deref(),
globals.model.as_deref(),
globals.base_url.as_deref(),
globals.api_key.as_deref(),
globals.format.as_deref(),
globals.no_color,
)?;

let llm_config = loader::build_llm_config(&config, globals.api_key.as_deref())?;

let format = resolve_format(globals.format.as_deref(), &config)?;
Expand Down
Loading