fix(security): resolve all HIGH/MEDIUM/LOW audit findings

Rust fixes (HIGH):
- symbols.rs: path validation (reject near-root, 50K file limit, symlink filter)
- memory.rs: FTS5 query quoting (prevent operator injection), 1000 fragment cap, content length limit, transaction wrapping
- budget.rs: atomic check-and-reserve via transaction, input validation, index on budget_log
- export.rs: safe UTF-8 truncation via chars().take()
- git_context.rs: reject paths starting with '-' (flag injection)
- branch_policy.rs: action validation (block|warn only), path validation

Rust fixes (MEDIUM):
- export.rs: named column access (positional→named)
- budget.rs: named column access, negative value guards

Svelte fixes:
- AccountSwitcher: 2-step confirmation before account switch
- ProjectMemory: expand/collapse content, 2-step delete confirm, tags split fix
- CodeIntelligence: min 2-char symbol query, CodeSymbol rename, aria-labels
- BudgetManager: 10M upper bound, aria-label on input, named constants
- SessionExporter: timeout cleanup on destroy, aria-live feedback
- AnalyticsDashboard: SVG aria-label, removed unused import, named constant
This commit is contained in:
Hibryda 2026-03-17 03:56:44 +01:00
parent 0324f813e2
commit 738574b9f0
13 changed files with 280 additions and 91 deletions

View file

@ -38,8 +38,8 @@ pub fn pro_export_session(project_id: String, session_id: String) -> Result<Sess
let (start, end, tokens, turns, tools, cost, model, status, error): (i64, i64, i64, i64, i64, f64, Option<String>, String, Option<String>) =
stmt.query_row(rusqlite::params![project_id, session_id], |row| {
Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?,
row.get(4)?, row.get(5)?, row.get(6)?, row.get(7)?, row.get(8)?))
Ok((row.get("start_time")?, row.get("end_time")?, row.get("peak_tokens")?, row.get("turn_count")?,
row.get("tool_call_count")?, row.get("cost_usd")?, row.get("model")?, row.get("status")?, row.get("error_message")?))
}).map_err(|e| format!("Session not found: {e}"))?;
let model_name = model.clone().unwrap_or_else(|| "unknown".into());
@ -55,7 +55,7 @@ pub fn pro_export_session(project_id: String, session_id: String) -> Result<Sess
let messages: Vec<(String, String, i64)> = msg_stmt
.query_map(rusqlite::params![session_id], |row| {
Ok((row.get(0)?, row.get(1)?, row.get(2)?))
Ok((row.get("message_type")?, row.get("content")?, row.get("created_at")?))
})
.map_err(|e| format!("Messages query failed: {e}"))?
.collect::<Result<Vec<_>, _>>()
@ -91,9 +91,10 @@ pub fn pro_export_session(project_id: String, session_id: String) -> Result<Sess
"tool_result" => "**Tool Result**",
_ => msg_type.as_str(),
};
// Truncate long content
// Truncate long content (safe UTF-8 boundary)
let display = if content.len() > 500 {
format!("{}... *(truncated, {} chars)*", &content[..500], content.len())
let truncated: String = content.chars().take(500).collect();
format!("{}... *(truncated, {} chars)*", truncated, content.len())
} else {
content.clone()
};
@ -126,8 +127,8 @@ pub fn pro_export_project_summary(project_id: String, days: Option<i64>) -> Resu
let sessions: Vec<(String, i64, i64, f64, i64, i64, Option<String>, String)> = stmt
.query_map(rusqlite::params![project_id, cutoff], |row| {
Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?,
row.get(4)?, row.get(5)?, row.get(6)?, row.get(7)?))
Ok((row.get("session_id")?, row.get("start_time")?, row.get("end_time")?, row.get("cost_usd")?,
row.get("turn_count")?, row.get("tool_call_count")?, row.get("model")?, row.get("status")?))
})
.map_err(|e| format!("Query failed: {e}"))?
.collect::<Result<Vec<_>, _>>()