Skip to content

feat(stealth): undetectable JS instrumentation via Proxy/exportFunction - #1154

Open
vringar wants to merge 7 commits into
masterfrom
feat/stealth-js-instrument-v2
Open

vringar wants to merge 7 commits into
masterfrom
feat/stealth-js-instrument-v2

Conversation

@vringar

@vringar vringar commented Apr 4, 2026

Copy link
Copy Markdown
Contributor

What this is

A stealth JavaScript instrument for OpenWPM: a drop-in alternative to the legacy js_instrument that captures the same JavaScript-API surface undetectably. A page cannot tell it is being instrumented (no enumerable wrappers, native-looking .length/toString, no leaked helpers), so it resists the detection-and-then-evasion attacks that legacy instrumentation is vulnerable to.

Based on Krumnow, Jonker & Karsch, "How gullible are web measurement tools?" (arXiv:2205.08890, 2022).

How to review this PR

This PR is based directly on master (v0.36.0, Firefox 154); the crosslink/agent-tooling base PR #1214 and the JS/TS dependency upgrade #1195 have both landed, so the diff here is stealth-only. The stack is deliberately shaped so each commit stands on its own — correct as of itself, green on master, and reviewable in isolation.

Suggested reading order. Start with the docs(stealth) commit (5th of 9) for intent — in particular docs/developers/adr/0001-retain-legacy-js-instrument.rst, which records why legacy stays and stealth is additive. Then review the code commits bottom-up from feat(stealth): add the stealth JavaScript instrument, which is the engine. The four commits above docs are two small fixes, the parametric-suite scaffolding, and a Firefox 154 re-verification note.

Order Commit What to check
1 (bottom) feat(stealth): add the stealth JavaScript instrument The engine: Extension/src/stealth/*.ts (exportFunction forwarding across the Xray membrane, native-arity codegen forwarders, native toString passthrough, descriptor-preserving prototype instrumentation, interface-attributed capture), the settings type + ambient exportFunction declaration, the new nullable receiver column (SQLite + Parquet), and the detection / error-drift / frame-protection (D10) tests that prove undetectability. Bundled surface — no runtime config yet.
2 feat(stealth): make the stealth instrumentation surface configurable Researchers can override the captured surface via stealth_js_instrument_settings. Trace the wiring: BrowserParams → clean → deploy_firefox → extension. Config tests.
3 feat(stealth): add the legacy-to-stealth settings migrator The migration tool (openwpm/utilities/js_settings_migrator.py): translates a legacy js_instrument config into a stealth config, expands recursive (which the runtime rejects, pointing here) by replaying the descent over the live object graph (walker.js), and surfaces every untranslatable member as an UntranslatedEntry. Also adds a config-validation guard that rejects logSettings.recursive in stealth settings (ConfigError, pointing at this tool).
4 feat(stealth): add the --capture-universal-members migrator flag A researcher opt-in (default OFF) to additionally capture universal Object/Function/Array.prototype members (toString/valueOf/…) for strict legacy-recursive parity. Pure additive — default behavior is unchanged.
5 (top) docs(stealth): document the stealth JavaScript instrument The why: ADR-0001 (legacy/stealth coexistence + detectability and tamper resilience as independent axes), the requirements doc, the mechanism design doc, and the user guide — all reStructuredText, with claims anchored via literalinclude to the test vectors the code commits add (which is why docs sits at the top). Also adds a graphviz docs-build dependency. Read this first for intent.

Config

  • stealth_js_instrument: bool — enable the stealth instrument. Mutually exclusive with js_instrument (raises ConfigError if both set).
  • stealth_js_instrument_settings — optional custom capture surface; falls back to the bundled default. logSettings.recursive is rejected at config time (use the migrator instead).
  • Migration: python -m openwpm.utilities.js_settings_migrator <legacy_config.json> [--output <stealth_config.json>] [--firefox-binary <path>] [--capture-universal-members] emits a stealth config + a report of every member it could not translate.

Capture parity vs legacy (known, intentional differences)

  • Same javascript table; per-call fidelity is identical for instrumented APIs.
  • Shared-prototype methods (e.g. addEventListener) are captured page-wide but attributed by receiver interface, recorded in the new receiver column — filter by (symbol, receiver) in post-processing.
  • document.cookie is captured (HTTP cookies unaffected either way). window.name, localStorage, and sessionStorage property access are also captured by the bundled default surface (verified by TestStealthWindowName).
  • Full analysis: docs/developers/Stealth-Instrumentation.rst.

Verification

  • Undetectability: detection/disruption probes across the D1–D9 vectors and the D10 frame-protection suite (16 sub-requirements), re-verified on Firefox 154 (51 tests, 0 failures): 8 paired legacy controls still trip, all 30 stealth rows pass. Manual cross-realm / creation-race / indexed-frame-access probes confirm no detection gap (frame instrumentation fires synchronously inside the hooked appendChild).

    An earlier revision of this description claimed a differential FingerprintJS oracle showed stealth byte-identical to vanilla while legacy differed. That claim had no artifact behind it, and it has since been measured and is wrong in direction: FingerprintJS reads values, and the legacy wrappers are value-transparent, so legacy is byte-identical to baseline too — the oracle does not discriminate. Likewise, the off-the-shelf OSS bot detectors BotD and fpscanner do not distinguish legacy from baseline on any of 185 signals, because none of their checks read the surfaces legacy perturbs. What does discriminate is the paper's instrumentation tells (window.getInstrumentJS, [native code] loss, wrapper stack frames, prototype flattening), which fire on legacy and not on stealth. Both experiments, including their limitations, are in docs/developers/.

  • Coverage/fidelity: repeated adversarial review per commit (FAIL-before / PASS-after), including golden migration tests pinning exact legacy→stealth output, a regression test for the narrow shared-prototype config, and a guard pinning the universal-members default-OFF classification.

Changes in this revision

Cross-version narrative for reviewers who saw an earlier round — the per-commit messages describe only the final artifact, so the deltas live here:

  • Docs commit ordered above the code it documents. The .rst docs literalinclude test methods that the code commits introduce, so docs sits above those commits and every commit builds in isolation. Later commits have since been added on top, so docs is no longer the final commit — read it first for intent, then review the code bottom-up.
  • Crosslink/agent tooling split out. The .claude//.crosslink/ config went out as its own PR (chore: configure crosslink for OpenWPM (repo-scoped rules) #1214, merged 2026-08-02), so this PR's diff is stealth-only.
  • ResidueEntryUntranslatedEntry. The migrator type and all prose now read as "untranslated" / "untranslatable", saying what a member is rather than what it leftover-isn't.
  • Docs converted to reStructuredText, cross-referencing the proving tests via literalinclude; the tests back-reference the docs.
  • Recursion claim corrected. The "recursion is fundamentally impossible / Xray forbids accessor-defines" wording was false. Corrected to the honest reason: recursion is deliberately refused because it requires a page-observable instance mutation (detectable). The dead recursive code path was deleted; the runtime rejects recursive at config time.
  • Detectability leak fixed. error.ts computed a cleaned stack but never assigned it. The cleaned stack is now applied; error-drift tests assert no extension frames leak across an instrument-origin throw. (Note: the primary reason stacks stay clean is the Xray/exportFunction boundary, which strips extension frames before the page sees them; the explicit cleaning is defense-in-depth.)
  • Migrator prose reframed affirmatively ("honey prop" jargon → nonExistingPropertiesToInstrument; negatives → affirmatives; geckodriver-resolution comment reworded to the consistency rationale).
  • walker.js extracted. The inline object-graph walker JS string was pulled out of the migrator into a reviewable/lintable walker.js, shipped as package data.
  • preventSets dropped in the migrator. Blocking a page's property writes buys no capture fidelity across the Xray boundary and would distort the measurement, so the migrator warns and drops it on translation.
  • Tests added. Error-drift parity tests (instanceof/name/message, custom-subclass collapse, async rejection) and golden migration tests (exact input→output config, including the UntranslatedEntry list and the shared-prototype union path).
  • Build-green fixes. Ambient exportFunction declaration (was TS2304); regenerated lockfile for json-schema-to-typescript; removed a stale tracked Extension/bundled/stealth.js build artifact; gitignored the schema-generated js_instrument_settings.d.ts (the schema is the single source of truth).

Copilot AI review requested due to automatic review settings April 4, 2026 21:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new “stealth” JavaScript instrumentation mode for OpenWPM that aims to be less detectable by websites, adds a detection test page + test suite, and extends schema/types/build plumbing to support new instrumentation settings.

Changes:

  • Add a stealth instrumentation entry point (/stealth.js) using Firefox exportFunction + Proxy, plus associated settings/error-handling modules.
  • Add end-to-end stealth/legacy detection tests (test/test_stealth.py) and a detection harness page (stealth_detection.html), plus config validation for mutual exclusion.
  • Extend JS instrumentation settings schema and generate TypeScript typings from JSON Schema during extension build.

Reviewed changes

Copilot reviewed 16 out of 20 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
test/test_stealth.py Adds pytest coverage for stealth undetectability and data capture, plus a legacy-detectable control.
test/test_pages/stealth_detection.html Adds a local detection-vectors page that reports results via DOM attributes for Selenium.
test/test_dataclass_validations.py Adds validation test ensuring stealth_js_instrument and js_instrument are mutually exclusive.
schemas/js_instrument_settings.schema.json Extends the schema with depth and overwrittenProperties/structured propertiesToInstrument.
openwpm/config.py Adds stealth_js_instrument flag and enforces mutual exclusion with legacy JS instrumentation.
Extension/webpack.config.js Adds a new webpack entry for the stealth bundle.
Extension/src/types/js_instrument_settings.d.ts Generated typings for the updated instrumentation schema.
Extension/src/types/javascript-instrument.d.ts Declares the privileged exportFunction global for TypeScript.
Extension/src/stealth/stealth.ts New stealth content script entry that intercepts frames and wraps functions via Proxy.
Extension/src/stealth/settings.ts Defines stealth instrumentation targets/settings (e.g., Navigator.webdriver override).
Extension/src/stealth/instrument.ts Core stealth instrumentation logic: property discovery, wrapper injection, logging, and overwrite support.
Extension/src/stealth/error.ts Error cloning/stack sanitization helpers for stealth behavior.
Extension/src/feature.ts Wires stealth_js_instrument into extension startup/config dispatch.
Extension/src/background/javascript-instrument.ts Adds legacy mode toggle and registers /stealth.js vs /content.js accordingly.
Extension/package.json Adds json-schema-to-typescript and a schema→types generation build step.
Extension/package-lock.json Locks new build-time dependencies for schema→types generation.
Extension/eslint.config.mjs Ignores the new stealth bundle output.
Extension/.gitignore Ignores the new stealth bundle output.
.gitignore Adds ignore rules for Crosslink-managed local state.
.dockerignore Ignores compiled JS under Extension/src/stealth/ for Docker builds.
Files not reviewed (1)
  • Extension/package-lock.json: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Extension/src/stealth/instrument.ts Outdated
Comment thread Extension/src/stealth/instrument.ts Outdated
Comment thread Extension/src/stealth/instrument.ts Outdated
Comment thread Extension/src/stealth/instrument.ts Outdated
Comment thread Extension/src/stealth/error.ts Outdated
Comment thread Extension/src/stealth/settings.ts Outdated
Comment thread Extension/src/stealth/settings.ts Outdated
Comment thread Extension/src/stealth/stealth.ts Outdated
@codecov

codecov Bot commented Apr 4, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.21138% with 29 lines in your changes missing coverage. Please review.
✅ Project coverage is 53.71%. Comparing base (975b09c) to head (823e81e).
⚠️ Report is 2 commits behind head on master.

Files with missing lines Patch % Lines
openwpm/utilities/js_settings_migrator.py 87.69% 24 Missing ⚠️
openwpm/js_instrumentation.py 81.81% 4 Missing ⚠️
openwpm/config.py 94.73% 1 Missing ⚠️

❗ There is a different number of reports uploaded between BASE (975b09c) and HEAD (823e81e). Click for more details.

HEAD has 1 upload less than BASE
Flag BASE (975b09c) HEAD (823e81e)
2 1
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1154      +/-   ##
==========================================
- Coverage   62.31%   53.71%   -8.61%     
==========================================
  Files          40       42       +2     
  Lines        3930     4969    +1039     
==========================================
+ Hits         2449     2669     +220     
- Misses       1481     2300     +819     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@vringar
vringar force-pushed the feat/stealth-js-instrument-v2 branch from 3e82973 to eecc4f6 Compare April 7, 2026 23:37
vringar added a commit that referenced this pull request Apr 19, 2026
@vringar
vringar force-pushed the feat/stealth-js-instrument-v2 branch from eecc4f6 to 11be71e Compare April 19, 2026 21:39
@vringar
vringar force-pushed the feat/stealth-js-instrument-v2 branch from 11be71e to e94f541 Compare May 7, 2026 12:17
@vringar
vringar force-pushed the feat/stealth-js-instrument-v2 branch 3 times, most recently from 41a3d74 to a0bacce Compare June 10, 2026 23:16
@vringar
vringar requested a review from Copilot June 11, 2026 10:01

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 30 changed files in this pull request and generated 6 comments.

Files not reviewed (1)
  • Extension/package-lock.json: Language not supported

Comment thread Extension/src/stealth/settings.ts
Comment thread Extension/src/stealth/stealth.ts
Comment thread Extension/src/stealth/stealth.ts
Comment thread Extension/src/stealth/index.ts Outdated
Comment thread openwpm/utilities/platform_utils.py
Comment thread openwpm/js_instrumentation.py Outdated
@vringar
vringar force-pushed the feat/stealth-js-instrument-v2 branch from db55b0f to a095371 Compare June 11, 2026 10:34
@vringar
vringar requested a review from Copilot June 11, 2026 10:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 30 changed files in this pull request and generated 5 comments.

Files not reviewed (1)
  • Extension/package-lock.json: Language not supported

Comment thread Extension/src/stealth/stealth.ts
Comment thread Extension/src/stealth/stealth.ts
Comment thread Extension/src/stealth/stealth.ts Outdated
Comment thread Extension/src/stealth/stealth.ts Outdated
Comment thread docs/developers/Stealth-Instrumentation.md Outdated
@vringar
vringar force-pushed the feat/stealth-js-instrument-v2 branch from a095371 to 411b4af Compare June 11, 2026 11:10
@vringar
vringar requested a review from Copilot June 14, 2026 19:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 30 changed files in this pull request and generated 2 comments.

Files not reviewed (1)
  • Extension/package-lock.json: Generated file

Comment thread schemas/js_instrument_settings.schema.json
Comment thread Extension/package-lock.json
vringar added a commit that referenced this pull request Jun 14, 2026
…acy behind consent env var at 2026-06-14T22:33:56Z
vringar added a commit that referenced this pull request Jun 15, 2026
…instrument with query_db @overload — land in #1154 at 2026-06-15T18:23:49Z
@vringar
vringar force-pushed the feat/stealth-js-instrument-v2 branch 5 times, most recently from 168440a to a59a87e Compare June 16, 2026 10:20
@vringar
vringar force-pushed the chore/crosslink-config branch from 812f535 to 53de7ba Compare July 11, 2026 18:53
@vringar
vringar force-pushed the feat/stealth-js-instrument-v2 branch 2 times, most recently from 76c6a05 to a749a6c Compare July 11, 2026 20:17
@vringar
vringar force-pushed the chore/crosslink-config branch 2 times, most recently from 78af90b to 814f175 Compare July 11, 2026 22:34
@vringar
vringar force-pushed the feat/stealth-js-instrument-v2 branch from a749a6c to 77b435c Compare July 11, 2026 22:34
@vringar
vringar force-pushed the chore/crosslink-config branch from 814f175 to bb782bb Compare July 11, 2026 22:48
@vringar
vringar force-pushed the chore/crosslink-config branch from bb782bb to f9d60d6 Compare July 20, 2026 22:54
@vringar
vringar force-pushed the feat/stealth-js-instrument-v2 branch from 77b435c to 8ea8dcc Compare July 20, 2026 22:54
@vringar
vringar force-pushed the chore/crosslink-config branch from f9d60d6 to 2e9da22 Compare July 21, 2026 10:24
@vringar
vringar force-pushed the feat/stealth-js-instrument-v2 branch from 8ea8dcc to 31ea6f3 Compare July 21, 2026 17:49
@vringar
vringar force-pushed the chore/crosslink-config branch from 2e9da22 to e82c482 Compare July 26, 2026 15:19
@vringar
vringar force-pushed the feat/stealth-js-instrument-v2 branch from 31ea6f3 to a396765 Compare July 26, 2026 15:19
@vringar
vringar force-pushed the chore/crosslink-config branch from e82c482 to 81fd97c Compare August 2, 2026 13:08
@vringar
vringar force-pushed the feat/stealth-js-instrument-v2 branch 2 times, most recently from 14de4fb to 9cc1d8c Compare August 2, 2026 15:51
Base automatically changed from chore/crosslink-config to master August 2, 2026 16:06
@vringar
vringar force-pushed the feat/stealth-js-instrument-v2 branch from 9cc1d8c to c282d53 Compare August 2, 2026 19:19
Introduce an undetectable JavaScript instrument that runs in the isolated
content-script world and exposes native-looking wrappers to the page across
the Xray membrane, leaving no page-observable footprint.

- Wrappers are handed to the page via exportFunction so the page sees genuine
  native functions; their toString reports [native code] and their name/arity
  match the originals (codegen arity forwarders), defeating the standard
  detection vectors.
- Instrumentation redefines properties on interface prototypes while
  preserving the original descriptor shape (enumerability, configurability,
  getter/setter layout), so the page cannot distinguish an instrumented
  property from a native one.
- Capture is attributed by interface via a new receiver column on the
  javascript table (schema.sql + parquet_schema.py), recording which prototype
  a call was observed on.
- Add the stealth settings type, an ambient declaration for the Xray
  exportFunction global (Extension/src/types/xray.d.ts), and the
  stealth_js_instrument flag, which is mutually exclusive with js_instrument
  (ConfigError if both are enabled).

Covered by detection tests and error-drift tests in test/test_stealth.py
asserting an instrumented page is indistinguishable from an uninstrumented
one, including across thrown errors.
Add the stealth_js_instrument_settings field so a custom instrumentation
surface can be supplied at runtime; when it is left as None the instrument
falls back to its bundled fingerprinting set, so out-of-the-box behaviour is
unchanged.

- Add the BrowserParams.stealth_js_instrument_settings override field and the
  corresponding cleaned_stealth_js_instrument_settings carrier on the internal
  params.
- Validate and clean the custom surface via
  clean_stealth_js_instrumentation_settings, only when stealth is enabled, so
  a surface left on a browser with stealth off is inert.
- Wire the cleaned surface through TaskManager into browser_params.json and
  surface it in the platform configuration printout.

Unlike the legacy js_instrument_settings dotted path, stealth resolves object
as a bare global name. Covered by configurability tests in
test/test_stealth.py, including attribution, shared-prototype, function-arg,
prevent-sets, and error-drift page fixtures.
Add openwpm/utilities/js_settings_migrator.py, which translates a legacy
js_instrument_settings config (including recursive entries, which stealth
cannot honour) into an equivalent flat, non-recursive
stealth_js_instrument_settings config.

- Launches a lightweight Firefox and replays the legacy descent over the live
  object graph; the descent logic lives in openwpm/utilities/walker.js, read
  at runtime and shipped as package data.
- Every legacy member is accounted for: representable members become stealth
  entries, and members that have no hookable interface prototype (plain
  Object/Array nodes, universal-prototype inherited members) are surfaced as
  an UntranslatedEntry rather than dropped silently.
- Members that share a prototype but request disjoint symbols are consolidated
  into a single interface-attributed shared-prototype entry, recovering the
  exact requested members via the receiver column in post-processing.
- preventSets is dropped on translation: blocking a page's writes buys no
  capture fidelity across the Xray boundary and would only distort the
  measurement.
- Resolves geckodriver explicitly, the same way deploy_firefox does, so the
  two stay in sync.

Covered by golden migration tests in test/test_stealth.py asserting exact
input-to-output config, including the UntranslatedEntry list and the
shared-prototype union path.
Add an opt-in --capture-universal-members flag to the legacy-to-stealth
migrator. By default, inherited methods owned by a universal base prototype
(Object.prototype, Function.prototype, Array.prototype — toString, valueOf,
hasOwnProperty, ...) are surfaced as UntranslatedEntry rather than
instrumented, because hooking them would fire on nearly every receiver on the
page and flood the log with noise.

When the flag is set, those universal methods are instead captured as
interface-attributed shared-prototype entries hooked once on the owning
universal prototype, restoring parity with legacy's full prototype-chain walk
at the cost of the extra log volume.

Also gitignore the generated
Extension/src/types/js_instrument_settings.d.ts: the JSON schema is the single
source of truth and the .d.ts is regenerated from it on every build.

Covered by tests in test/test_stealth.py exercising both the default
(untranslated) and opt-in (captured) behaviour.
Add the reStructuredText documentation for the stealth instrument:

- docs/developers/adr/0001-retain-legacy-js-instrument.rst — an
  architecture decision record explaining why the legacy js_instrument is
  retained alongside stealth, framing detectability and tamper resilience as
  independent axes and scoping the residual detection surface to the legacy
  instrument.
- docs/developers/Stealth-Instrumentation.rst — the mechanism reference
  (isolated content-script world, exportFunction across the Xray membrane,
  native toString passthrough, descriptor-preserving prototype
  instrumentation, interface-attributed capture) with an honest
  disruptability section.
- docs/developers/Stealth-Requirements.rst — the X1-X3 / D1-D9 requirement
  and detection-vector list, each tied to its proving test in
  test/test_stealth.py via literalinclude.
- docs/Stealth-Instrument.rst — a user-facing guide on enabling and
  configuring the instrument, with the stealth_js_instrument flag and the
  stealth_js_instrument_settings override.

Detection and disruption claims are anchored to the test vectors so the docs
and the tests cross-reference each other. Wired into docs/index.rst.
…itch (scaffolding)

Groundwork for reusing the legacy JS-instrument suite as the oracle for the
stealth instrument. This commit adds the machinery only: every existing test
class is still legacy-only, so the suite asserts exactly what it did before.

The round trip is legacy-format settings -> migrator -> stealth capture ->
back-projection -> the *original* legacy assertions. Nothing about what is
asserted may change; the value of the suite is that it is the legacy oracle.
Three properties enforce that:

- The comparison body of OpenWPMJSTest._check_calls is byte-identical to its
  previous content. Only the row source moved, behind a new _js_rows() hook, so
  a reviewer can diff those lines and see that no assertion was touched.
- get_config asserts the transpile produced no untranslated members. An
  untranslated member disqualifies a class from stealth mode; it is never a
  licence to shrink the expected set.
- Back-projection drops the stealth-only `receiver` column and nothing else. Any
  further normalization is a defang and needs explicit sign-off; the contract is
  stated in the module docstring and pinned by test_js_data_backprojector.py,
  which asserts the absence of transformation (order, duplicates, every other
  column, and the drop list itself) at least as hard as its presence.

Stealth is opt-in: STEALTH_UNSUPPORTED_REASON defaults to a non-None string, so
a class runs in stealth mode only when someone removes it with a justification.
All seven classes in test_js_instrument.py carry a specific reason (the
document.cookie migrator gap, the missing stealth failure-propagation path, or
reliance on window.instrumentJS, which stealth deliberately does not provide).
A skip is a documented gap, not a pass.

Settings hoisted to a JS_INSTRUMENT_SETTINGS class attribute are deep-copied on
handout: clean_js_instrumentation_settings aliases the caller's property list
into its output and _merge_settings extends it in place, so sharing one list
across visits would let one visit mutate what the next one sees. No current
config reaches that path, but it is one duplicate `object` key away.

Transpiling launches a headless Firefox per call, so results are memoized for
the process lifetime, keyed on the serialized settings.

Verified: test_js_instrument.py 7 passed before, 7 passed + 7 stealth-skipped
after, same test names and same wall clock; test_stealth.py 104 passed;
test_js_data_backprojector.py 9 passed (pyonly).
…t's browser

D12 asserts that configuring a custom instrumentation surface does not
create a detection hook. The background script injects the surface as
window.openWpmStealthInstrumentSettings from a script registered through
browser.contentScripts.register, so the assignment lands in the extension's
isolated compartment as an Xray expando: stealth.js is registered the same
way and reads it back, while page script cannot see it. The probe checks the
page's own window four ways, and the row runs under
test_custom_settings_stay_undetectable so the global is genuinely injected
while the page looks for it.

Also records the browser the legacy_detectable ratchet was last verified
against, which was measured on Firefox 150 and re-verified since.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants