Security Architecture
How Bushido hardens against browser-specific attacks
Bushido renders untrusted web content. Every site you visit runs arbitrary JavaScript in a child webview that shares the same OS process group as the browser UI. This page documents every mitigation that's been implemented and every known limitation.
Threat Model
The security boundary is between:
- Trusted: The main React UI webview (sidebar, tabs, settings) with access to Tauri's
invoke()API - Untrusted: Child webviews (tabs, panels) that render arbitrary web pages
A malicious page should never be able to: execute code in the main UI, access other tabs' data, read local files, or crash the browser process.
IPC Hardening
Tauri Isolation Pattern
Enabled since v0.8.0. A sandboxed iframe sits between the React frontend and Rust core. All IPC messages are encrypted with AES-GCM (keys regenerated every launch) and validated against a command whitelist before reaching Rust.
The isolation bridge blocks unauthorized invoke() calls — if a compromised npm dependency tries to call an unrecognized command, it gets null back. Only 29 specific commands are allowed, plus Tauri's internal plugin prefixes.
postMessage Bridge
Child webviews communicate with Rust via window.chrome.webview.postMessage. Messages use a __bushido JSON namespace with server-side whitelist validation:
{ "__bushido": "shortcut", "action": "new-tab" }
{ "__bushido": "media", "state": "playing", "title": "..." }
{ "__bushido": "video", "hasVideo": true }Only three namespaces are accepted: shortcut, media, video. Within shortcut, only 10 specific action strings are allowed. Everything else is silently dropped.
Origin validation: The handler checks Source() on every incoming message and rejects anything not from https:// or http:// origins. Messages from about:blank, data:, chrome-extension://, or other non-web origins are dropped.
Per-Window Capabilities
invoke() is scoped to "windows": ["main"] in the Tauri capability file. Child webviews can't call invoke at all — they only have postMessage, which goes through the COM handler with origin + namespace validation. The capability file explicitly lists allowed permissions instead of using the broad core:default.
No Arbitrary eval()
Zero eval() calls with user-controlled strings. All child webview interactions use named Rust commands:
detect_video— checks for<video>elementstoggle_reader— injects/removes reader mode overlaytoggle_pip— injects Shadow DOM PiP button
Content Security Policy
The main UI webview has a strict CSP defined in tauri.conf.json. This blocks inline scripts, restricts connections, and prevents XSS in the privileged context.
WebView2 Hardening
Every child webview has these settings applied at the COM level:
| Setting | Default | Why |
|---|---|---|
| Host Objects | Always OFF | Prevents unauthorized access to projected Rust methods |
| DevTools | Toggleable | CVE-2025-13632 — sandbox escape via crafted extensions |
| Password Autosave | Toggleable | CVE-2025-14372 — use-after-free in Password Manager |
| General Autofill | Toggleable | Reduces feature surface |
| Status Bar | Toggleable | Prevents URL spoofing |
Host objects are always disabled. The other four are configurable in Settings → Security (all default to OFF for power-user friendliness).
Process Isolation
WebView2 browser arguments enforced on every child webview:
| Flag | Effect |
|---|---|
--site-per-process | Every site runs in its own renderer process |
--origin-agent-cluster=true | Different origins within the same site get separate processes |
--disable-quic | Forces TCP, ensures WebResourceRequested intercepts all traffic |
--disable-dns-prefetch | Prevents DNS query leaks before the blocker acts |
--disable-background-networking | No speculative connections |
--enable-features=ThirdPartyStoragePartitioning,PartitionedCookies | CHIPS — third-party cookies partitioned by top-level site |
--disable-features=UserAgentClientHint | Blocks Client Hints fingerprinting |
The Rust process runs at ABOVE_NORMAL_PRIORITY_CLASS for UI responsiveness during heavy ad blocking. Max 50 tabs enforced server-side.
Input Sanitization
Tab Titles
The on_document_title_changed callback in Rust strips < and > from all titles before emitting to React. Prevents stored XSS via malicious <title> tags.
URL Scheme Blocklist
Dangerous URL schemes are blocked in three places: create_tab, navigate_tab, and the on_navigation callback.
Web schemes: javascript:, data:, file:, vbscript:, blob:
Windows exploit schemes (added v0.8.2): ms-msdt: (Follina CVE-2022-30190), search-ms:, ms-officecmd:, ms-word:, ms-excel:, ms-powerpoint:, ms-cxh:, ms-cxh-full: — these can trigger arbitrary code execution via Windows system tools.
Find-in-Page
Search queries are escaped for \n, \r, \, and ' before being passed to window.find().
Script Injection Hardening
Guard Variables
All four injection scripts use Object.defineProperty with configurable: false to register their guard variables. A malicious page can't delete window.__bushidoPrivacy to re-trigger script injection.
No Isolated Worlds (Windows Limitation)
WebView2 on Windows does not support Chrome's "Isolated Worlds" for content scripts. Injected scripts share the page's global JavaScript namespace. Mitigated by running all network blocking at the COM level (unaffected by JS namespace pollution).
Network Security
Always-On Header Stripping
WebResourceRequested runs for ALL tabs — even when ad blocking is off. Every outgoing request has these headers stripped or normalized:
Removed: Sec-CH-UA, Sec-CH-UA-Mobile, Sec-CH-UA-Platform, Sec-CH-UA-Full-Version, Sec-CH-UA-Full-Version-List, Sec-CH-UA-Arch, Sec-CH-UA-Bitness, Sec-CH-UA-Model, Sec-CH-UA-Platform-Version, Sec-CH-UA-WoW64, X-Client-Data, X-Requested-With
Normalized: Referer — path stripped at COM level, only origin sent. Accept-Language — set to en-US,en;q=0.9 to match the JS navigator.language spoof (prevents locale mismatch fingerprinting).
Not stripped: Sec-Fetch-Dest, Sec-Fetch-Mode, Sec-Fetch-Site, Sec-Fetch-User — Chromium's network stack overwrites these headers after the WebResourceRequested handler runs. Stripping them is a no-op. This behavior is confirmed by Microsoft's WebView2 team and documented in Chromium's fetch metadata spec implementation.
Ad Blocking at the COM Level
adblock-rust engine with ~140,000 filter rules from EasyList and EasyPrivacy. Intercepts requests via WebResourceRequestedEventHandler before the browser starts the connection. Page JavaScript cannot bypass it.
Service Worker Blocking
navigator.serviceWorker.register() is overridden to return a rejected Promise. This prevents trackers from registering service workers that could bypass WebResourceRequested interception on subsequent visits.
HTTPS-Only Mode
All http:// URLs are upgraded to https://. Plain HTTP navigations are refused entirely when enabled.
Fingerprinting Resistance
23 fingerprinting vectors mitigated via content_blocker.js injection + COM-level header normalization:
| Vector | Mitigation |
|---|---|
navigator.plugins / mimeTypes | Empty arrays |
navigator.getBattery | Undefined |
navigator.hardwareConcurrency | Firefox RFP logic: returns 8 if real count ≥ 8, else 4 |
navigator.language / languages | en-US / ['en-US','en'] |
navigator.platform | Win32 |
screen.availWidth/Height | Matches screen.width/height |
screen.colorDepth/pixelDepth | Returns 24 |
| Canvas | Per-session PRNG noise on toDataURL/toBlob (deterministic within session, unique across) |
| WebGL vendor/renderer | Spoofed to generic Intel UHD |
| AudioContext | ±0.01 random noise on getFloatFrequencyData |
performance.now() | Clamped to 16.67ms intervals + random jitter (anti-Spectre) |
navigator.connection | Undefined |
document.fonts | Stub object (blocks enumeration) |
| Service Workers | Registration blocked |
| WebRTC | STUN/TURN servers filtered |
speechSynthesis.getVoices() | Empty array (prevents TTS voice fingerprinting) |
navigator.mediaDevices.enumerateDevices() | Empty array (prevents hardware ID leaking) |
navigator.storage.estimate() | Fixed 1GB quota, 0 usage |
navigator.webdriver | Returns false |
performance.memory | Fixed values (Chrome-only heap size fingerprint) |
Accept-Language header | Normalized to en-US,en;q=0.9 at COM level |
Spoofed function .toString() | Returns [native code] (anti-detection hardening) |
Three additional vectors (service workers, font enumeration, hardwareConcurrency) are toggleable in Settings → Security for users who need PWA support or accurate core counts.
Crash Recovery & FFI Safety
COM Callback Safety
All 5 COM event handlers (DownloadStarting, GetCookiesCompleted, WebResourceRequested, WebMessageReceived, ProcessFailed) are wrapped in std::panic::catch_unwind with AssertUnwindSafe. If any callback panics — due to a malformed URL, unexpected null pointer, or edge case — the panic is caught and the handler returns gracefully instead of unwinding across the Rust/C++ FFI boundary.
Without this, a panic in any COM callback produces undefined behavior: typically an immediate process crash with access violation 0xc0000005. This is documented in the windows-rs crate — COM callbacks don't automatically catch Rust panics.
Environment Variable Injection Prevention
WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS is cleared with remove_var() before being set with set_var(). This env var affects every WebView2 instance in the process. Without clearing, a malicious process running before Bushido could pre-populate it with flags like --remote-debugging-port=9222 to enable remote debugging on all tabs.
ProcessFailed Handler
WebView2's ProcessFailed event detected at COM level. When a renderer process crashes, a tab-crashed event is emitted to React. Crashed tabs show a visual indicator in the sidebar — click to recreate the webview.
Error Boundary
react-error-boundary wraps the entire app. If a render error occurs, a fallback UI appears with "Try again" instead of a white screen.
Global Rejection Handler
window.onunhandledrejection catches fire-and-forget invoke() failures. Logs to console and prevents silent app breakage.
Mutex Poisoning Recovery
All Mutex::lock().unwrap() calls replaced with .unwrap_or_else(|e| e.into_inner()). If a thread panics while holding a lock, subsequent threads recover the data instead of cascading panics.
Download Security
Path Traversal Prevention
Path::file_name() extracts only the basename from download filenames. A server sending Content-Disposition: attachment; filename="../../../etc/malicious" can't write outside the download directory.
Cookie-Aware Downloads
Downloads capture cookies from the originating tab via the WebView2 cookie manager COM API. Cookies are passed as a header string to the Rust download engine — never written to disk or exposed to other tabs.
Known Limitations
| Gap | Risk | Notes |
|---|---|---|
| Shared first-party cookie jar | All tabs share one WebView2 User Data Folder. Third-party cookies are partitioned via CHIPS, but first-party cookies are shared. | Per-site UDF isolation requires separate browser processes per domain — high RAM cost |
| No isolated worlds | WebView2 on Windows doesn't support script isolation. Injected scripts share the page's JS namespace. | Mitigated: all network blocking at COM level, unaffected by JS pollution |
| WebRTC data channels | STUN/TURN blocked, but data channels may bypass network interception | Low risk — most tracking uses HTTP, not WebRTC |
| TLS/JA4 fingerprint | Rust reqwest calls have a non-browser TLS fingerprint | Only affects internal requests (filter list updates), not user traffic |
Found a Bug?
Open an issue. This is fully open source — no private disclosure process. File it like any other bug.