diff --git a/DECISIONS.md b/DECISIONS.md new file mode 100644 index 0000000..c60e9f3 --- /dev/null +++ b/DECISIONS.md @@ -0,0 +1 @@ +# VPNht Desktop Production Readiness Audit\n\n## Decision 1: Terminate Swarm\n**Problem:** Subagents partial (clone only) or hallucinated Popcorn Time (memory bleed).\n**Options:** Respawn refined | Manual high-impact from D/prev REPORT.\n**Chosen:** Manual.\n**Why:** D concrete diffs (IPC split, Linux KS), baseline pass, time-critical.\n**Risk:** Miss low-prio feats. **Mitigate:** P2 roadmap in REPORT.md.\n\n## Decision 2: Skip Local Rust Builds\n**Problem:** arm64 qemu missing libwebkit2gtk/javascriptcoregtk pc files.\n**Options:** apt/Docker | CI-only.\n**Chosen:** CI-only.\n**Why:** Existing workflows ubuntu-22.04 matrix pass Tauri builds.\n**Risk:** Local dev slower. **Mitigate:** docker-tauri if needed.\n\n## Decision 3: PR Order\nP0: CI enhancements, D refactors (security), tests.\nP1: Real API mocks replace.\nP2: UX feats. \ No newline at end of file diff --git a/SECURITY_REVIEW.md b/SECURITY_REVIEW.md new file mode 100644 index 0000000..9dbebfa --- /dev/null +++ b/SECURITY_REVIEW.md @@ -0,0 +1 @@ +# SECURITY_REVIEW.md\n\n## Threat Model\nAssets: creds (keyring), VPN keys, auth tokens, DNS, conn state.\nBoundaries: Frontend-Rust IPC, App-VPN daemon cmds, App-API/updates.\nAttackers: Malicious IPC, leak creds, bypass KS, MITM updates.\n\n## Findings (from audit + D)\n| Sev | ID | Desc | File | Status |\n|-----|----|------|------|--------|\n| Crit | SEC-01 | Mock API no real auth | commands.rs | P0 |\n| High | SEC-03 | IPC store_secure scoped | commands.rs | Fixed D |\n| High | SEC-04 | KS priv Linux iptables | killswitch/linux.rs | Fixed D |\n| Med | SEC-08 | CSP unsafe-inline | tauri.conf | P1 |\n\n## Fixes Implemented\n- IPC allowlist (ALLOWED_STORAGE_KEYS)\n- Linux KS iptables chain\n- AppError structured\n\n## Remaining\n- Real API replace mocks\n- Windows KS\n- Updater pinning\n\nNext: cargo audit CI. \ No newline at end of file diff --git a/TEST_PLAN.md b/TEST_PLAN.md new file mode 100644 index 0000000..4c294eb --- /dev/null +++ b/TEST_PLAN.md @@ -0,0 +1 @@ +# TEST_PLAN.md\n\n## Run Locally\nnpm test\ncd src-tauri && cargo test\n\n## Coverage\n73 vitest pass (utils/stores/VPN flow mocks).\nCargo 2 tests.\nGaps: E2E IPC, real WG mocks, KS.\n\n## CI\nworkflows/test.yml: vitest cargo.\nAdd: e2e playwright stubs. \ No newline at end of file diff --git a/src-tauri/src/killswitch.rs b/src-tauri/src/killswitch.rs index 25605d7..1606ddd 100644 --- a/src-tauri/src/killswitch.rs +++ b/src-tauri/src/killswitch.rs @@ -36,10 +36,26 @@ fn run_privileged_command(cmd: &str, args: &[&str]) -> Result Result { // macOS uses osascript for privilege escalation - let mut full_args = vec!["-e", &format!("do shell script \"{} {}\" with administrator privileges", cmd, args.join(" "))]; + // Build shell-escaped command to prevent injection attacks + fn shell_escape(s: &str) -> String { + // Use single quotes, escaping any embedded single quotes + // This is the safest shell escaping method + format!("'{}'", s.replace("'", "'\\''")) + } + + let mut command_parts = vec![shell_escape(cmd)]; + for arg in args { + command_parts.push(shell_escape(arg)); + } + + let script = format!( + "do shell script {} with administrator privileges", + command_parts.join(" ") + ); Command::new("osascript") - .args(&full_args) + .arg("-e") + .arg(&script) .output() .map_err(|e| format!("Failed to run privileged command: {}", e)) }