Scope: the entire extension (manifest, background service worker, content
scripts, popup, cockpit/options, shared libraries).
Date: 2026-07-06 · Version audited: 1.0.0 → 1.0.1 (remediated)
Method: manual source review across the standard browser-extension threat
classes (data exfiltration via postMessage, DOM-XSS sinks, message-sender
trust, prototype pollution, credential handling, permission surface, CSP),
plus in-browser dynamic verification with a headless Chromium harness.
All findings below have been remediated and re-verified.
| # | Severity | Finding | Status |
|---|---|---|---|
| F1 | High | Proxy credentials broadcast to every web page via postMessage |
✅ Fixed |
| F2 | Medium | DOM-XSS sink in popup via user-controlled proxy host | ✅ Fixed |
| F3 | Medium | Privileged service-worker messages accepted without sender authentication | ✅ Fixed |
| F4 | Medium | Prototype pollution in the settings merge | ✅ Fixed |
| F5 | Low | Proxy password sent to any challenging proxy, not just the configured one | ✅ Fixed |
| F6 | Low | Proxy configuration passed to chrome.proxy without validation |
✅ Fixed |
| F7 | Low | postMessage origin/robustness + message-forgery hardening |
✅ Fixed |
| F8 | Hardening | No explicit Content-Security-Policy for extension pages | ✅ Added |
| F9 | Hardening | Web-accessible spoofer script exposed the extension ID to all pages | ✅ Removed |
| F10 | Info | Client-side license is not a security boundary | ✅ Documented |
A functional bug was surfaced during dynamic testing: the fingerprint spoofer was injected as an async
<script>that was removed before it loaded, so spoofing never actually applied to pages. Fixed by declaring it as aworld: "MAIN"content script (see F9). Now verified working end-to-end.
Where: src/content/inject-loader.js
Issue: the bridge posted the entire settings object — including
proxy.username and proxy.password — into the page's MAIN world via
window.postMessage. Any script on any visited site could add a message
listener and harvest the user's proxy credentials.
Fix: the bridge now sends only a strict allowlist of non-secret flags
(ghostMode + the spoof toggles) plus the already-synthetic identity. Secrets
(proxy creds, license, wipe config) never leave the privileged context.
Verified: planted SECRET_USER/SECRET_PASS in storage, loaded a real
page, captured the delivered message → neither secret nor any proxy key is
present; identity and spoof flags still delivered.
Where: src/popup/popup.js
Issue: the network readout used innerHTML with settings.proxy.host
(user-supplied). A crafted host injected markup into the popup, which executes
with extension privileges (chrome.*) — a path to reading storage or
wiping data. A user pasting a malicious "proxy host" from a phishing lure would
be enough.
Fix: replaced all HTML sinks with a setDD() helper that uses
textContent/createElement. No user-derived string reaches innerHTML.
Where: src/background/service-worker.js
Issue: onMessage performs destructive/privileged actions (self-destruct,
panic wipe, settings changes, license activation) and trusted any sender.
Fix: added isTrustedSender() — every message must come from one of our own
extension pages (sender.id === runtime.id and sender.url starts with the
extension origin). Content scripts and web pages are rejected. The browser sets
sender; a page cannot forge it.
Where: src/lib/storage.js
Issue: the recursive mergeSettings walk copied arbitrary keys from a patch;
a __proto__/constructor key could pollute Object.prototype.
Fix: the walk skips __proto__/constructor/prototype and non-own
properties.
Verified: a {"__proto__":{"polluted":true}} patch leaves
({}).polluted === undefined while a legitimate patch still applies.
Where: src/background/service-worker.js (onAuthRequired)
Issue: credentials were returned for any proxy 407 challenge (e.g. a
rogue captive portal), not only the configured proxy.
Fix: credentials are supplied only when proxying is enabled and the
challenger's host/port matches the configured, validated proxy.
Where: src/background/service-worker.js (applyProxy)
Fix: added sanitizeProxy() — scheme allowlist (http/https/socks4/socks5),
host character allowlist (hostnames / IPv4 / bracketed IPv6), and port clamped
to 1–65535. Invalid configs clear the proxy instead of applying garbage.
Where: src/content/inject-loader.js, src/content/fingerprint-spoof.js
Fixes: cross-frame forgery already blocked by the same-window ev.source
check; tightened message-shape checks to __sr === true; debounced the
ready-triggered storage read to prevent a page-driven busy-loop; the payload
is now non-secret by construction so the "*" target origin carries no risk (it
does not cross frames). A page forging config only affects its own context,
which it inherently controls.
Where: manifest.json
Fix: added content_security_policy.extension_pages:
script-src 'self'; object-src 'self'; base-uri 'none'; form-action 'none'.
Where: manifest.json, src/content/inject-loader.js
Issue: the spoofer was a web_accessible_resource injected via a <script>
tag — this (a) exposed the extension ID to every page (an extension-detection
vector) and (b) was removed before it finished loading, so spoofing silently
failed.
Fix: the spoofer is now a declarative world: "MAIN" content script. The
web-accessible resource was removed entirely (smaller fingerprint surface), and
overrides install synchronously at document_start with no race.
Verified: on a live page, navigator.userAgent, platform,
hardwareConcurrency, and timezone all match the spoofed identity.
The offline key decode in src/lib/license.js is a development stub, clearly
marked TODO(monetization). It must be replaced with server-side signed
verification before any paid tier ships. It gates convenience features, not
security; treat any client-side unlock as spoofable by design.
- Broad permissions (
<all_urls>,webRequest,proxy,privacy,browsingData). Each is justified (fingerprint/location spoofing must run everywhere;webRequestis only for proxy auth). This is inherent to the product and will draw Web Store review scrutiny — keepPRIVACY.mdaccurate. - A page can undo spoofing in its own context. Any site can redefine the overridden getters in its own MAIN world; no extension can prevent this. Our overrides defeat passive fingerprinting, not a site actively fighting back.
stripReferrerremoves theRefererheader globally when enabled; a few sites use it for CSRF defenses. It is user-toggleable and off nothing security-critical for the user's own session.- Proxy credentials at rest live in
chrome.storage.localin cleartext (standard for extensions; the OS profile is the trust boundary).
Findings were confirmed fixed by loading the unpacked extension in headless
Chromium and asserting behavior at runtime: no secret reaches page context
(F1), spoofing applies to navigator/screen/timezone (F9), prototype pollution
is blocked (F4), and the popup/cockpit still operate through the authenticated
message channel (F3). Service worker registered with zero console errors after
all changes.