From 1889dea96da157cede0c05a42969606abe28f807 Mon Sep 17 00:00:00 2001 From: CTO Hermes Date: Mon, 1 Jun 2026 12:01:17 +0700 Subject: [PATCH] =?UTF-8?q?release:=20v0.1.5=20=E2=80=94=20critical=20secu?= =?UTF-8?q?rity=20&=20bug=20fixes=20from=20audit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 15 +++++++++++++++ Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 8 ++++---- src/commands/config_cmd.rs | 2 +- src/config/loader.rs | 5 +++-- src/engine/llm.rs | 27 ++++++++++++++++++++++++--- src/main.rs | 6 +----- 8 files changed, 50 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b3b2c2..3eddbd5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.lock b/Cargo.lock index e913626..f84a147 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -231,7 +231,7 @@ dependencies = [ [[package]] name = "cora-cli" -version = "0.1.4" +version = "0.1.5" dependencies = [ "anyhow", "assert_cmd", diff --git a/Cargo.toml b/Cargo.toml index 4e674d2..6f15839 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/README.md b/README.md index fbff516..f720c06 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/src/commands/config_cmd.rs b/src/commands/config_cmd.rs index 5ebe458..4e61578 100644 --- a/src/commands/config_cmd.rs +++ b/src/commands/config_cmd.rs @@ -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!( "{}", diff --git a/src/config/loader.rs b/src/config/loader.rs index 52eb83c..f282e2d 100644 --- a/src/config/loader.rs +++ b/src/config/loader.rs @@ -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 { @@ -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()))?; diff --git a/src/engine/llm.rs b/src/engine/llm.rs index 8090fc5..a3443f6 100644 --- a/src/engine/llm.rs +++ b/src/engine/llm.rs @@ -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?; @@ -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(); } } } @@ -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. diff --git a/src/main.rs b/src/main.rs index c21b699..5d7f117 100644 --- a/src/main.rs +++ b/src/main.rs @@ -429,11 +429,9 @@ async fn cmd_review(globals: &GlobalOptions, opts: ReviewOpts) -> Result { 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 @@ -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)?; @@ -529,11 +527,9 @@ async fn cmd_scan(globals: &GlobalOptions, opts: ScanOpts) -> Result { 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)?;