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:
parent
0324f813e2
commit
738574b9f0
13 changed files with 280 additions and 91 deletions
|
|
@ -64,15 +64,15 @@ fn prune_expired(conn: &rusqlite::Connection) -> Result<(), String> {
|
|||
|
||||
fn row_to_fragment(row: &rusqlite::Row) -> rusqlite::Result<MemoryFragment> {
|
||||
Ok(MemoryFragment {
|
||||
id: row.get(0)?,
|
||||
project_id: row.get(1)?,
|
||||
content: row.get(2)?,
|
||||
source: row.get(3)?,
|
||||
trust: row.get(4)?,
|
||||
confidence: row.get(5)?,
|
||||
created_at: row.get(6)?,
|
||||
ttl_days: row.get(7)?,
|
||||
tags: row.get(8)?,
|
||||
id: row.get("id")?,
|
||||
project_id: row.get("project_id")?,
|
||||
content: row.get("content")?,
|
||||
source: row.get("source")?,
|
||||
trust: row.get("trust")?,
|
||||
confidence: row.get("confidence")?,
|
||||
created_at: row.get("created_at")?,
|
||||
ttl_days: row.get("ttl_days")?,
|
||||
tags: row.get("tags")?,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -83,8 +83,21 @@ pub fn pro_memory_add(
|
|||
source: Option<String>,
|
||||
tags: Option<String>,
|
||||
) -> Result<i64, String> {
|
||||
if content.len() > 10000 {
|
||||
return Err("Memory content too long (max 10000 chars)".into());
|
||||
}
|
||||
let conn = super::open_sessions_db()?;
|
||||
ensure_tables(&conn)?;
|
||||
|
||||
// Per-project memory cap
|
||||
let count: i64 = conn.query_row(
|
||||
"SELECT COUNT(*) FROM pro_memories WHERE project_id = ?1",
|
||||
params![project_id], |row| row.get(0)
|
||||
).unwrap_or(0);
|
||||
if count >= 1000 {
|
||||
return Err("Memory limit reached for this project (max 1000 fragments)".into());
|
||||
}
|
||||
|
||||
let ts = now_epoch();
|
||||
let src = source.unwrap_or_default();
|
||||
let tgs = tags.unwrap_or_default();
|
||||
|
|
@ -121,6 +134,9 @@ pub fn pro_memory_search(project_id: String, query: String) -> Result<Vec<Memory
|
|||
ensure_tables(&conn)?;
|
||||
prune_expired(&conn)?;
|
||||
|
||||
// Sanitize query to prevent FTS5 operator injection
|
||||
let safe_query = format!("\"{}\"", query.replace('"', "\"\""));
|
||||
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT m.id, m.project_id, m.content, m.source, m.trust, m.confidence, m.created_at, m.ttl_days, m.tags
|
||||
FROM pro_memories m
|
||||
|
|
@ -129,7 +145,7 @@ pub fn pro_memory_search(project_id: String, query: String) -> Result<Vec<Memory
|
|||
ORDER BY rank LIMIT 20"
|
||||
).map_err(|e| format!("Search query failed: {e}"))?;
|
||||
|
||||
let rows = stmt.query_map(params![query, project_id], row_to_fragment)
|
||||
let rows = stmt.query_map(params![safe_query, project_id], row_to_fragment)
|
||||
.map_err(|e| format!("Search failed: {e}"))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("Row read failed: {e}"))?;
|
||||
|
|
@ -147,18 +163,23 @@ pub fn pro_memory_update(
|
|||
let conn = super::open_sessions_db()?;
|
||||
ensure_tables(&conn)?;
|
||||
|
||||
let tx = conn.unchecked_transaction()
|
||||
.map_err(|e| format!("Transaction failed: {e}"))?;
|
||||
|
||||
if let Some(c) = content {
|
||||
conn.execute("UPDATE pro_memories SET content = ?2 WHERE id = ?1", params![id, c])
|
||||
tx.execute("UPDATE pro_memories SET content = ?2 WHERE id = ?1", params![id, c])
|
||||
.map_err(|e| format!("Update content failed: {e}"))?;
|
||||
}
|
||||
if let Some(t) = trust {
|
||||
conn.execute("UPDATE pro_memories SET trust = ?2 WHERE id = ?1", params![id, t])
|
||||
tx.execute("UPDATE pro_memories SET trust = ?2 WHERE id = ?1", params![id, t])
|
||||
.map_err(|e| format!("Update trust failed: {e}"))?;
|
||||
}
|
||||
if let Some(c) = confidence {
|
||||
conn.execute("UPDATE pro_memories SET confidence = ?2 WHERE id = ?1", params![id, c])
|
||||
tx.execute("UPDATE pro_memories SET confidence = ?2 WHERE id = ?1", params![id, c])
|
||||
.map_err(|e| format!("Update confidence failed: {e}"))?;
|
||||
}
|
||||
|
||||
tx.commit().map_err(|e| format!("Commit failed: {e}"))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -185,7 +206,7 @@ pub fn pro_memory_inject(project_id: String, max_tokens: Option<i64>) -> Result<
|
|||
).map_err(|e| format!("Query failed: {e}"))?;
|
||||
|
||||
let entries: Vec<(String, String, f64)> = stmt
|
||||
.query_map(params![project_id], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))
|
||||
.query_map(params![project_id], |row| Ok((row.get("content")?, row.get("trust")?, row.get("confidence")?)))
|
||||
.map_err(|e| format!("Query failed: {e}"))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| format!("Row read failed: {e}"))?;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue