Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: [S

### Added

- Plain-language **Page not working?** recovery card in the popup for the current site, with one-click **Pause this site and reload** / **Turn protection back on**. A detected compatibility warning upgrades the same card instead of using separate technical copy.
- Pausing a site now also installs a high-priority DNR `allow` rule for that initiator host, so fingerprint/CAPTCHA vendor network blocks no longer keep breaking logins after a per-site pause.
- Toolbar badge shows `!` when the current site has a fresh compatibility warning, so casual users get a cue to open the popup without an in-page banner.
- Noise mode now **learns WAR paths** from each origin's own probes and answers those exact files for persona IDs on later visits. LinkedIn-style `{id, file}` AED probes (e.g. `/inpage.js`, `/src/css/content.css`) are poisoned without answering first-visit path canaries. Learned paths are query-stripped, charset-limited, capped at 8 per ID, omitted from research exports, and never store website URLs.
- DOM/global marker coverage for KeePassXC, Dark Reader, Bitwarden overlay (`data-bw-*`), and LanguageTool, closing Castle-style side-effect detection gaps.
- KeePassXC-Browser (`oboonakemofpalcgghocfoadofidjkkk`) in the `password_manager` conflict slot, with an ID-seeded Noise manifest name.
- DNR fingerprint vendor rules for LexisNexis Risk (`lexisnexisrisk.com`), Stytch device telemetry (`telemetry.stytch.com` only, not the auth product), Group-IB RU (`group-ib.ru`), IPFingerprint (`ipfingerprint.com`), Socure IO (`socure.io`), and Nethone (`nethone.com`) (rule IDs 77–82).
- Playwright coverage for learned-path Noise decoys, path canaries, path caps, research-export omission, and new DOM markers (`tests/noise-learned-paths.spec.js`).

- Noise-mode decoy `manifest.json` bodies are now **ID-seeded**: known store IDs return real-looking extension names (e.g. Bitwarden, uBlock Origin), and unknown IDs get a stable per-ID name/version so multi-ID probe dumps no longer collapse to one generic `"Browser Extension"` string.
- Noise fetch/XHR image decoys now serve format-matched bodies and `Content-Type` for PNG, GIF, JPEG, and SVG (path extension, headers, and magic bytes stay aligned). Formats Static cannot synthesize correctly (e.g. WebP) stay fail-closed.
- DNR fingerprint vendor rules for HUMAN legacy `whiteops.com`, Sift root `sift.com`, Accertify (`accertify.com`), and Group-IB (`group-ib.com`) (rule IDs 73–76).
Expand All @@ -27,6 +29,10 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versioning: [S
- `docs/noise-behavior.md` documents the ID-seeded manifest contract, image magic-byte fail-closed policy, and learned WAR-path Noise contract.
- Firefox smoke CI starts the browser with `--remote-allow-system-access` so WebDriver can still open `about:debugging` and `moz-extension://` pages on Firefox 153+.

### Fixed

- Firefox package build no longer leaves Chrome-only DNR resource type names in the pause-allow retry path, so the `build-firefox.js` self-check passes.

## [2.4.0] — 2026-07-09

### Added
Expand Down
14 changes: 11 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,21 @@ Firefox needs MAIN-world content scripts (128+) and built-in data-collection con
7. **Replay poisoning (opt-in).** When a likely session-replay SDK is detected in page script, Static can proxy only that recorder's event listeners so they see redacted form values and jittered coordinates while ordinary page handlers still receive the real events.
8. **Iframe attribute hardening.** Extension probes and fingerprinting scripts can infer browser capabilities from the shape of `<iframe allow>` / `sandbox` / `allowfullscreen` / `allowpaymentrequest>` attributes. Static normalizes these attributes: it drops unsupported `allow` tokens, keeps only valid `sandbox` tokens, coerces legacy `allowfullscreen` / `allowpaymentrequest` into modern `allow` syntax, and works on pages with Trusted Types `require-trusted-types-for 'script'` CSP by creating a dedicated policy.
9. **Style / CSSOM vector blocking.** Extension URLs can be smuggled into `<style>` text nodes, inline `style` attributes, `CSSStyleSheet.cssText`, `CSSStyleDeclaration.setProperty` / `cssText`, and `insertRule` / `replace` / `replaceSync` / `addRule` calls. Static scrubs these sources before the browser can issue a request, and does it synchronously so the URL cannot be read back.
10. **Per-site disable.** You can turn Static off for individual sites from the popup. When a site is disabled, all extension-probe blocking, DOM scrubbing, global stripping, and opt-in poisoning modes are bypassed for that origin. The disabled state persists in `chrome.storage.local` and updates instantly on the current page. The **Disabled sites** page lists every paused origin with search, per-origin enable, and **Enable All**.
10. **Per-site disable.** You can turn Static off for individual sites from the popup, including a plain-language **Page not working?** card with one-click pause and reload. When a site is disabled, all extension-probe blocking, DOM scrubbing, global stripping, and opt-in poisoning modes are bypassed for that origin, and Static also allows that origin's requests through the fingerprint/CAPTCHA network lists. The disabled state persists in `chrome.storage.local` and updates instantly on the current page. The **Disabled sites** page lists every paused origin with search, per-origin enable, and **Enable All**.

The toolbar badge and popup show a live count of extension-enumeration probes blocked on the current tab. On sites that probe aggressively (LinkedIn runs ~4,500 per page load) the number climbs into the thousands within seconds. The popup's diagnostics also include an **Exposed browser profile** view showing the JavaScript-visible user agent, platform, locale/language, timezone, screen, hardware buckets, WebGL, network, storage, and battery signals the current site can read; when Device signal poisoning is active, this view shows Static's stable per-site persona.

## Compatibility warning

Static also watches for one high-confidence breakage signal: a Static-blocked extension-probe `fetch()` that becomes an unhandled page error. When that happens, the popup shows a local compatibility warning with a **Pause here and reload** escape hatch. The popup also links to a **Disabled sites** page where you can see every paused origin, re-enable individual sites, or enable them all at once. Static does not auto-disable itself, and the warning evidence stays local.
Static watches for a high-confidence breakage signal: a Static-blocked extension-probe `fetch()` that becomes an unhandled page error. When that happens, the toolbar badge shows `!` and the popup switches the recovery card to a direct warning.

Casual recovery does not depend on that signal. The popup always keeps a **Page not working?** card on the current site, with one button: **Pause this site and reload**. That pause:

- turns off Static's page-layer defenses for that origin
- lets the site's own requests through Static's fingerprint/CAPTCHA network lists (those lists are otherwise global)
- reloads the tab so an already-broken page can recover

Static does not auto-disable itself, does not inject an in-page banner (that would be another fingerprint), and keeps the warning evidence local. After a pause, the card becomes **Static is paused here** with **Turn protection back on**. The **Disabled sites** page lists every paused origin so a site can be re-enabled later.

## Playbook drift detection

Expand Down Expand Up @@ -329,7 +337,7 @@ static/

- JS-layer patches run only where content scripts run. Pages served from `chrome://`, `about:`, the Chrome Web Store, and a handful of other restricted schemes are not covered.
- The DOM scrubber ships with a default list of extensions whose markers are stripped. If one of those is an extension you use, its in-page UI (autofill icons, inline suggestions, etc.) may not render. Remove that extension's patterns from `lists.js` to keep it working.
- Some sites use anti-bot vendors (PerimeterX, DataDome) as part of their login / checkout flow. If a site breaks, try disabling `fingerprint_vendors` first from the popup.
- Some sites use anti-bot vendors (PerimeterX, DataDome) as part of their login / checkout flow. If a site breaks, use **Pause this site and reload** in the popup first. That pauses Static on that origin, including the fingerprinting vendor network rules. You can also disable `fingerprint_vendors` globally from **More**.
- `captcha_vendors` is disabled by default because Arkose/FunCAPTCHA, DataDome, and Cloudflare Turnstile / Challenge Platform are served as CAPTCHA or device-check flows on some logins and protected forms (X signup, Roblox, some crypto exchanges, Cloudflare-protected forms); enabling it will break sign-in there.
- Does not cover the entire browser-fingerprinting surface (canvas, WebGL, audio, fonts, font enumeration, WebRTC IP leak, etc.). Complements, doesn't replace, a dedicated anti-fingerprint extension.
- Does not block broad tracker, ad-tech, analytics, or social pixel ecosystems. Complements, doesn't replace, uBlock Origin or Privacy Badger.
Expand Down
5 changes: 5 additions & 0 deletions build-firefox.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ function stripTypesFromServiceWorker(filePath) {
// Remove lines like: "webtransport",
content = content.replace(new RegExp(`^\\s*"${typeName}",\\s*\\n`, "gm"), "");
}
// Blank any leftover quoted type names (comparisons, comments, retries)
// so the packaged worker cannot mention types Firefox DNR rejects.
for (const typeName of UNSUPPORTED_RESOURCE_TYPES) {
content = content.split(`"${typeName}"`).join('""');
}
if (content !== before) {
fs.writeFileSync(filePath, content);
return true;
Expand Down
2 changes: 1 addition & 1 deletion disabled.html
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@
<div class="header-inner">
<img src="icons/icon-128.png" alt="" />
<h1>Disabled sites</h1>
<div class="subtitle">Sites where Static is paused.</div>
<div class="subtitle">Sites where Static is paused. Turn protection back on anytime.</div>
</div>
</div>
</header>
Expand Down
2 changes: 1 addition & 1 deletion disabled.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const renderEmpty = () => {
const content = document.getElementById("content");
content.innerHTML = `<div class="empty-state">
<p class="big">No sites are paused</p>
<p>Use the site toggle in the popup to pause Static on a site.</p>
<p>Use <strong>Page not working?</strong> in the popup, or the site toggle, to pause Static.</p>
</div>`;
};

Expand Down
76 changes: 55 additions & 21 deletions popup.html
Original file line number Diff line number Diff line change
Expand Up @@ -136,43 +136,74 @@
.adaptive[hidden] {
display: none;
}
.compat {
.recovery {
margin-top: 10px;
padding: 8px;
border: 1px solid #efd4a8;
border: 1px solid var(--border-strong);
border-radius: 7px;
color: #744100;
background: #fff7e8;
color: var(--text);
background: #f6f6f6;
font-size: 10px;
line-height: 1.35;
}
.compat[hidden] {
.recovery[hidden] {
display: none;
}
.compat-title {
.recovery.is-warning {
color: #744100;
background: #fff7e8;
border-color: #efd4a8;
}
.recovery.is-paused {
color: var(--accent);
background: #fff5f5;
border-color: #ead0d0;
}
.recovery-title {
font-weight: 700;
}
.compat-detail {
.recovery-detail {
margin-top: 2px;
color: var(--text-muted);
}
.recovery.is-warning .recovery-detail {
color: #8a4b00;
}
.compat-actions {
.recovery.is-paused .recovery-detail {
color: var(--accent);
}
.recovery-actions {
margin-top: 7px;
}
.compat-button {
padding: 5px 7px;
color: #744100;
.recovery-button {
padding: 6px 8px;
color: var(--text);
background: #fff;
border: 1px solid #efd4a8;
border: 1px solid var(--border-strong);
border-radius: 6px;
font: inherit;
font-size: 10px;
font-weight: 700;
cursor: pointer;
}
.compat-button:hover {
.recovery-button:hover {
border-color: var(--text-muted);
}
.recovery.is-warning .recovery-button {
color: #744100;
border-color: #efd4a8;
}
.recovery.is-warning .recovery-button:hover {
border-color: #d88030;
}
.recovery.is-paused .recovery-button {
color: var(--accent);
border-color: #ead0d0;
}
.recovery.is-paused .recovery-button:hover {
background: #fff;
border-color: var(--accent);
}
.site-section {
padding: 12px 16px;
background: var(--surface);
Expand Down Expand Up @@ -756,12 +787,15 @@ <h1>Static</h1>
<div class="cumulative" id="cumulative"></div>
<div class="drift" id="drift" hidden></div>
<div class="adaptive" id="adaptive" hidden></div>
<div class="compat" id="compat" hidden>
<div class="compat-title">Static may be interfering with this site</div>
<div class="compat-detail" id="compat-detail"></div>
<div class="compat-actions">
<button class="compat-button" id="compat-pause" type="button">
Pause here and reload
<div class="recovery" id="recovery" hidden aria-live="polite">
<div class="recovery-title" id="recovery-title">Page not working?</div>
<div class="recovery-detail" id="recovery-detail">
Static can stop logins, checkouts, or extra features on some sites. Pause it here to
check.
</div>
<div class="recovery-actions">
<button class="recovery-button" id="recovery-action" type="button">
Pause this site and reload
</button>
</div>
</div>
Expand All @@ -771,10 +805,10 @@ <h1>Static</h1>
<div class="toggle-row">
<div class="col">
<div class="ttl" id="site-title">
<span id="site-title-text">Site status</span>
<span id="site-title-text">Protect this site</span>
</div>
<div class="desc" id="site-desc">
<span id="site-status-text">Protecting this site</span>
<span id="site-status-text">On. Pause it if the page looks broken.</span>
</div>
</div>
<label class="switch">
Expand Down
Loading
Loading