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

@ -56,14 +56,32 @@ fn should_skip(name: &str) -> bool {
SKIP_DIRS.contains(&name)
}
const MAX_FILES: usize = 50_000;
const MAX_DEPTH: usize = 20;
fn walk_files(dir: &Path, files: &mut Vec<PathBuf>) {
walk_files_bounded(dir, files, 0);
}
fn walk_files_bounded(dir: &Path, files: &mut Vec<PathBuf>, depth: usize) {
if depth >= MAX_DEPTH || files.len() >= MAX_FILES {
return;
}
let Ok(entries) = std::fs::read_dir(dir) else { return };
for entry in entries.flatten() {
if files.len() >= MAX_FILES {
return;
}
let ft = entry.file_type();
// Skip symlinks
if ft.as_ref().map_or(false, |ft| ft.is_symlink()) {
continue;
}
let path = entry.path();
if path.is_dir() {
if ft.as_ref().map_or(false, |ft| ft.is_dir()) {
if let Some(name) = path.file_name().and_then(|n| n.to_str()) {
if !should_skip(name) {
walk_files(&path, files);
walk_files_bounded(&path, files, depth + 1);
}
}
} else if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
@ -164,6 +182,9 @@ fn extract_ts_const_fn(line: &str) -> Option<String> {
pub fn pro_symbols_scan(project_path: String) -> Result<ScanResult, String> {
let start = std::time::Instant::now();
let root = PathBuf::from(&project_path);
if !root.is_absolute() || root.components().count() < 3 {
return Err("Invalid project path: must be an absolute path at least 3 levels deep".into());
}
if !root.is_dir() {
return Err(format!("Not a directory: {project_path}"));
}
@ -205,6 +226,9 @@ pub fn pro_symbols_search(project_path: String, query: String) -> Result<Vec<Sym
#[tauri::command]
pub fn pro_symbols_find_callers(project_path: String, symbol_name: String) -> Result<Vec<CallerRef>, String> {
let root = PathBuf::from(&project_path);
if !root.is_absolute() || root.components().count() < 3 {
return Err("Invalid project path: must be an absolute path at least 3 levels deep".into());
}
if !root.is_dir() {
return Err(format!("Not a directory: {project_path}"));
}