Bushido
Privacy & Security

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> elements
  • toggle_reader — injects/removes reader mode overlay
  • toggle_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:

SettingDefaultWhy
Host ObjectsAlways OFFPrevents unauthorized access to projected Rust methods
DevToolsToggleableCVE-2025-13632 — sandbox escape via crafted extensions
Password AutosaveToggleableCVE-2025-14372 — use-after-free in Password Manager
General AutofillToggleableReduces feature surface
Status BarToggleablePrevents 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:

FlagEffect
--site-per-processEvery site runs in its own renderer process
--origin-agent-cluster=trueDifferent origins within the same site get separate processes
--disable-quicForces TCP, ensures WebResourceRequested intercepts all traffic
--disable-dns-prefetchPrevents DNS query leaks before the blocker acts
--disable-background-networkingNo speculative connections
--enable-features=ThirdPartyStoragePartitioning,PartitionedCookiesCHIPS — third-party cookies partitioned by top-level site
--disable-features=UserAgentClientHintBlocks 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:

VectorMitigation
navigator.plugins / mimeTypesEmpty arrays
navigator.getBatteryUndefined
navigator.hardwareConcurrencyFirefox RFP logic: returns 8 if real count ≥ 8, else 4
navigator.language / languagesen-US / ['en-US','en']
navigator.platformWin32
screen.availWidth/HeightMatches screen.width/height
screen.colorDepth/pixelDepthReturns 24
CanvasPer-session PRNG noise on toDataURL/toBlob (deterministic within session, unique across)
WebGL vendor/rendererSpoofed to generic Intel UHD
AudioContext±0.01 random noise on getFloatFrequencyData
performance.now()Clamped to 16.67ms intervals + random jitter (anti-Spectre)
navigator.connectionUndefined
document.fontsStub object (blocks enumeration)
Service WorkersRegistration blocked
WebRTCSTUN/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.webdriverReturns false
performance.memoryFixed values (Chrome-only heap size fingerprint)
Accept-Language headerNormalized 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.

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

GapRiskNotes
Shared first-party cookie jarAll 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 worldsWebView2 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 channelsSTUN/TURN blocked, but data channels may bypass network interceptionLow risk — most tracking uses HTTP, not WebRTC
TLS/JA4 fingerprintRust reqwest calls have a non-browser TLS fingerprintOnly 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.

On this page