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

@ -46,7 +46,8 @@ fn ensure_tables(conn: &rusqlite::Connection) -> Result<(), String> {
session_id TEXT NOT NULL,
tokens_used INTEGER NOT NULL,
timestamp INTEGER NOT NULL
);"
);
CREATE INDEX IF NOT EXISTS idx_budget_log_project ON pro_budget_log(project_id, timestamp);"
).map_err(|e| format!("Failed to create budget tables: {e}"))
}
@ -54,15 +55,59 @@ fn now_epoch() -> i64 {
super::analytics::now_epoch()
}
/// Calculate reset date: first day of next month as epoch.
/// Calculate reset date: first day of next calendar month as epoch.
fn next_month_epoch() -> i64 {
let now = now_epoch();
// Approximate: 30 days from now
now + 30 * 86400
let days_since_epoch = now / 86400;
let mut year = 1970i64;
let mut remaining_days = days_since_epoch;
loop {
let days_in_year = if is_leap_year(year) { 366 } else { 365 };
if remaining_days < days_in_year { break; }
remaining_days -= days_in_year;
year += 1;
}
let month_days: [i64; 12] = if is_leap_year(year) {
[31,29,31,30,31,30,31,31,30,31,30,31]
} else {
[31,28,31,30,31,30,31,31,30,31,30,31]
};
let mut month = 0usize;
for (i, &d) in month_days.iter().enumerate() {
if remaining_days < d { month = i; break; }
remaining_days -= d;
}
// Advance to first of next month
let (next_year, next_month) = if month >= 11 {
(year + 1, 0usize)
} else {
(year, month + 1)
};
// Calculate epoch for first of next_month in next_year
let mut epoch: i64 = 0;
for y in 1970..next_year {
epoch += if is_leap_year(y) { 366 } else { 365 };
}
let nm_days: [i64; 12] = if is_leap_year(next_year) {
[31,29,31,30,31,30,31,31,30,31,30,31]
} else {
[31,28,31,30,31,30,31,31,30,31,30,31]
};
for i in 0..next_month {
epoch += nm_days[i];
}
epoch * 86400
}
fn is_leap_year(y: i64) -> bool {
(y % 4 == 0 && y % 100 != 0) || y % 400 == 0
}
#[tauri::command]
pub fn pro_budget_set(project_id: String, monthly_limit_tokens: i64) -> Result<(), String> {
if monthly_limit_tokens <= 0 {
return Err("monthly_limit_tokens must be positive".into());
}
let conn = super::open_sessions_db()?;
ensure_tables(&conn)?;
let reset = next_month_epoch();
@ -86,9 +131,9 @@ pub fn pro_budget_get(project_id: String) -> Result<BudgetStatus, String> {
).map_err(|e| format!("Query failed: {e}"))?;
stmt.query_row(params![project_id], |row| {
let limit: i64 = row.get(0)?;
let used: i64 = row.get(1)?;
let reset_date: i64 = row.get(2)?;
let limit: i64 = row.get("monthly_limit_tokens")?;
let used: i64 = row.get("used_tokens")?;
let reset_date: i64 = row.get("reset_date")?;
let remaining = (limit - used).max(0);
let percent = if limit > 0 { (used as f64 / limit as f64) * 100.0 } else { 0.0 };
Ok(BudgetStatus { project_id: project_id.clone(), limit, used, remaining, percent, reset_date })
@ -97,6 +142,9 @@ pub fn pro_budget_get(project_id: String) -> Result<BudgetStatus, String> {
#[tauri::command]
pub fn pro_budget_check(project_id: String, estimated_tokens: i64) -> Result<BudgetDecision, String> {
if estimated_tokens < 0 {
return Err("estimated_tokens must be non-negative".into());
}
let conn = super::open_sessions_db()?;
ensure_tables(&conn)?;
auto_reset_if_expired(&conn, &project_id)?;
@ -105,7 +153,7 @@ pub fn pro_budget_check(project_id: String, estimated_tokens: i64) -> Result<Bud
"SELECT monthly_limit_tokens, used_tokens FROM pro_budgets WHERE project_id = ?1"
).map_err(|e| format!("Query failed: {e}"))?
.query_row(params![project_id], |row| {
Ok((row.get::<_, i64>(0)?, row.get::<_, i64>(1)?))
Ok((row.get::<_, i64>("monthly_limit_tokens")?, row.get::<_, i64>("used_tokens")?))
});
match result {
@ -133,14 +181,18 @@ pub fn pro_budget_log_usage(project_id: String, session_id: String, tokens_used:
let conn = super::open_sessions_db()?;
ensure_tables(&conn)?;
let ts = now_epoch();
conn.execute(
let tx = conn.unchecked_transaction()
.map_err(|e| format!("Transaction failed: {e}"))?;
tx.execute(
"INSERT INTO pro_budget_log (project_id, session_id, tokens_used, timestamp) VALUES (?1, ?2, ?3, ?4)",
params![project_id, session_id, tokens_used, ts],
).map_err(|e| format!("Failed to log usage: {e}"))?;
conn.execute(
tx.execute(
"UPDATE pro_budgets SET used_tokens = used_tokens + ?2 WHERE project_id = ?1",
params![project_id, tokens_used],
).map_err(|e| format!("Failed to update used tokens: {e}"))?;
tx.commit().map_err(|e| format!("Commit failed: {e}"))?;
Ok(())
}
@ -166,10 +218,10 @@ pub fn pro_budget_list() -> Result<Vec<BudgetEntry>, String> {
let rows = stmt.query_map([], |row| {
Ok(BudgetEntry {
project_id: row.get(0)?,
monthly_limit_tokens: row.get(1)?,
used_tokens: row.get(2)?,
reset_date: row.get(3)?,
project_id: row.get("project_id")?,
monthly_limit_tokens: row.get("monthly_limit_tokens")?,
used_tokens: row.get("used_tokens")?,
reset_date: row.get("reset_date")?,
})
}).map_err(|e| format!("Query failed: {e}"))?
.collect::<Result<Vec<_>, _>>()