From 3531007bd1912d70c4e7dd7f3f0f48d139e87d51 Mon Sep 17 00:00:00 2001 From: graykode Date: Mon, 29 Jun 2026 15:07:49 +0900 Subject: [PATCH] fix: sanitize OpenCode DB strings --- src/collector/opencode.rs | 45 ++++++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/src/collector/opencode.rs b/src/collector/opencode.rs index 59df773..9b12046 100644 --- a/src/collector/opencode.rs +++ b/src/collector/opencode.rs @@ -359,8 +359,8 @@ LIMIT {};"#, model_map.insert( id.to_string(), ( - mr["model"].as_str().unwrap_or("").to_string(), - mr["provider"].as_str().unwrap_or("").to_string(), + sanitize_db_field(mr["model"].as_str().unwrap_or(""), 256), + sanitize_db_field(mr["provider"].as_str().unwrap_or(""), 256), ), ); } @@ -371,16 +371,11 @@ LIMIT {};"#, let id = row["id"].as_str().unwrap_or("").to_string(); let (model, provider) = model_map.remove(&id).unwrap_or_default(); - // Sanitize DB-sourced strings: truncate, redact secrets in title - let mut title = row["title"].as_str().unwrap_or("").to_string(); - let mut directory = row["directory"].as_str().unwrap_or("").to_string(); - let mut version = row["version"].as_str().unwrap_or("").to_string(); - let mut project_name = row["project_name"].as_str().unwrap_or("").to_string(); - truncate_field(&mut title, 512); - truncate_field(&mut directory, 4096); - truncate_field(&mut version, 64); - truncate_field(&mut project_name, 256); - let title = super::redact_secrets(&title); + // Sanitize DB-sourced strings before they reach the TUI/JSON snapshot. + let title = sanitize_db_title(row["title"].as_str().unwrap_or("")); + let directory = sanitize_db_field(row["directory"].as_str().unwrap_or(""), 4096); + let version = sanitize_db_field(row["version"].as_str().unwrap_or(""), 64); + let project_name = sanitize_db_field(row["project_name"].as_str().unwrap_or(""), 256); sessions.push(DbSession { id, @@ -441,6 +436,16 @@ fn is_symlink(path: &Path) -> bool { .unwrap_or(true) } +fn sanitize_db_title(raw: &str) -> String { + super::redact_secrets(&sanitize_db_field(raw, 512)) +} + +fn sanitize_db_field(raw: &str, max_bytes: usize) -> String { + let mut value = super::sanitize_terminal_text(raw); + truncate_field(&mut value, max_bytes); + value +} + /// Truncate a string at a char boundary to avoid panics on multi-byte UTF-8. fn truncate_field(s: &mut String, max_bytes: usize) { if s.len() > max_bytes { @@ -598,6 +603,22 @@ mod tests { assert!(path_str.ends_with("opencode.db")); } + #[test] + fn sanitize_db_field_removes_terminal_control_chars() { + assert_eq!( + sanitize_db_field("proj\u{202E}\u{0008}name", 512), + "projname" + ); + } + + #[test] + fn sanitize_db_title_redacts_known_secret_prefixes() { + assert_eq!( + sanitize_db_title("debug sk-ant-secret-value now"), + "debug [REDACTED] now" + ); + } + #[test] fn match_pid_short_session_dir_never_matches() { // Regression: previously, an empty `directory` made `cmd.contains("")`