diff --git a/src-tauri/.cargo/audit.toml b/src-tauri/.cargo/audit.toml new file mode 100644 index 0000000..6d403e4 --- /dev/null +++ b/src-tauri/.cargo/audit.toml @@ -0,0 +1,34 @@ +# cargo-audit configuration. +# +# `cargo audit` reads Cargo.lock, which records optional dependencies even when +# no enabled feature pulls them into the build. An advisory against such a +# package fails CI while the vulnerable code is never compiled into the binary. +# Entries here are for exactly that case and must each carry the evidence. +# +# Before adding an ignore, prove the crate is genuinely not built: +# cd src-tauri +# cargo tree -i --target all # must print "nothing to print" +# cargo tree --target all | grep # must find nothing +# If either finds it, the crate IS in the build and the advisory must be fixed, +# not ignored. + +[advisories] +ignore = [ + # RUSTSEC-2026-0235 — rkyv: insufficient archive validation can cause + # out-of-bounds reads in archives containing Rc/Arc. Fixed in rkyv >= 0.8.17. + # + # Not reachable here. rkyv is an OPTIONAL dependency of rust_decimal, which + # arrives via tauri-plugin-log -> byte-unit -> rust_decimal. No enabled + # feature activates it, so it is present in Cargo.lock but absent from the + # build graph. Verified with the two commands above (both find nothing) and + # by there being no rkyv artifact in target/. + # + # There is no upgrade path: rust_decimal 1.42.1 is the newest release and + # still pins rkyv 0.7.x, so `cargo update` cannot reach 0.8.17. + # + # REMOVE THIS as soon as either becomes true: + # - rust_decimal ships a release depending on rkyv >= 0.8.17, or + # - `cargo tree -i rkyv --target all` starts printing a path, which would + # mean the crate is now genuinely compiled and the advisory applies. + "RUSTSEC-2026-0235", +] diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 77284dc..7cfc63c 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -4093,7 +4093,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" dependencies = [ - "proc-macro-crate 3.5.0", + "proc-macro-crate 1.3.1", "proc-macro2", "quote", "syn 2.0.118", @@ -6861,7 +6861,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.4.3", + "getrandom 0.3.4", "once_cell", "rustix", "windows-sys 0.61.2", diff --git a/src-tauri/src/vpn_extension_detect/browser_scan.rs b/src-tauri/src/vpn_extension_detect/browser_scan.rs index 10471ec..2a6223c 100644 --- a/src-tauri/src/vpn_extension_detect/browser_scan.rs +++ b/src-tauri/src/vpn_extension_detect/browser_scan.rs @@ -285,6 +285,23 @@ mod tests { fs::write(path, contents).unwrap(); } + /// Build a Chromium `Preferences` blob for one extension. + /// + /// Serialized rather than string-interpolated on purpose: a Windows path is + /// `C:\Users\...`, and pasting it into a JSON string literal produces invalid + /// escape sequences, so the file silently fails to parse and every assertion + /// about it passes for the wrong reason. + fn preferences_json(crx_id: &str, state: i64, path: &Path) -> String { + serde_json::json!({ + "extensions": { + "settings": { + crx_id: { "state": state, "path": path.to_string_lossy() } + } + } + }) + .to_string() + } + const VPN_MANIFEST: &str = r#"{"name":"Turbo VPN","version":"2.1.0","permissions":["proxy"]}"#; const CRX_ID: &str = "abcdefghijklmnopabcdefghijklmnop"; @@ -530,6 +547,22 @@ mod tests { assert!(out.is_empty()); } + #[test] + fn preferences_json_escapes_windows_style_paths() { + // The Windows CI failure this guards: a raw `C:\Users\...` pasted into a + // JSON string literal is invalid (`\U` is not an escape), so Preferences + // failed to parse, `preference_extensions` returned nothing, and the + // unpacked extension silently vanished — on Linux the same test passed + // because POSIX paths contain no backslashes. + let json = preferences_json(CRX_ID, 1, Path::new(r"C:\Users\runner\ext\my-vpn")); + let parsed: serde_json::Value = + serde_json::from_str(&json).expect("Preferences must be valid JSON on every platform"); + assert_eq!( + parsed["extensions"]["settings"][CRX_ID]["path"], + r"C:\Users\runner\ext\my-vpn" + ); + } + #[test] fn scan_finds_an_unpacked_developer_mode_extension() { // Sideloading via "Load unpacked" is exactly how a VPN extension gets in @@ -540,10 +573,7 @@ mod tests { write(&unpacked.join("manifest.json"), VPN_MANIFEST); write( &root.join("Default").join("Preferences"), - &format!( - r#"{{"extensions":{{"settings":{{"{CRX_ID}":{{"state":1,"path":"{}"}}}}}}}}"#, - unpacked.to_string_lossy() - ), + &preferences_json(CRX_ID, 1, &unpacked), ); let mut out = Vec::new(); @@ -561,10 +591,7 @@ mod tests { write(&unpacked.join("manifest.json"), VPN_MANIFEST); write( &root.join("Default").join("Preferences"), - &format!( - r#"{{"extensions":{{"settings":{{"{CRX_ID}":{{"state":0,"path":"{}"}}}}}}}}"#, - unpacked.to_string_lossy() - ), + &preferences_json(CRX_ID, 0, &unpacked), ); let mut out = Vec::new(); @@ -580,9 +607,7 @@ mod tests { let root = tmp.path(); write( &root.join("Default").join("Preferences"), - &format!( - r#"{{"extensions":{{"settings":{{"{CRX_ID}":{{"state":1,"path":"{CRX_ID}/2.1.0_0"}}}}}}}}"# - ), + &preferences_json(CRX_ID, 1, Path::new(&format!("{CRX_ID}/2.1.0_0"))), ); let mut out = Vec::new();