Conversation
There was a problem hiding this comment.
Pull request overview
Adds two hermetic, reproducible experiments to measure how detectable OpenWPM’s legacy vs stealth JavaScript instrumentation is to off-the-shelf third-party fingerprinting / bot-detection libraries (FingerprintJS, BotD, fpscanner), with pinned vendored fixtures and in-repo measurement reports backing the updated claims in the stacked stealth work.
Changes:
- Vendor and SHA-pin FingerprintJS 5.2.0, BotD 2.0.0, and fpscanner 1.0.8 bundles for hermetic browser tests (with telemetry/monitoring disabled where applicable).
- Add two probe pages plus two pytest harnesses implementing validity gates, activity controls, and per-signal comparisons across baseline/legacy/stealth (and a plain-Firefox control arm for bot detection).
- Add developer-facing experiment reports documenting method, results, and caveats.
Reviewed changes
Copilot reviewed 6 out of 13 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
test/test_pages/vendor/README.md |
Documents provenance, pinning, and hermeticity constraints for vendored oracle bundles. |
test/test_pages/vendor/fingerprintjs-5.2.0.umd.min.js |
Vendored FingerprintJS oracle bundle (pinned artifact). |
test/test_pages/vendor/fingerprintjs-5.2.0.LICENSE.txt |
FingerprintJS MIT license text. |
test/test_pages/vendor/botd-2.0.0.esm.js |
Vendored BotD oracle bundle (pinned artifact). |
test/test_pages/vendor/botd-2.0.0.LICENSE.txt |
BotD MIT license text. |
test/test_pages/vendor/fpscanner-1.0.8.es.js |
Vendored fpscanner oracle bundle (pinned artifact). |
test/test_pages/vendor/fpscanner-1.0.8.LICENSE.txt |
fpscanner MIT license text. |
test/test_pages/fingerprintjs_stamp.html |
Probe page that runs FingerprintJS and publishes per-component comparison keys. |
test/test_pages/bot_detection.html |
Probe page that runs BotD + fpscanner and publishes per-signal outputs plus paper probes. |
test/test_fingerprintjs_stamp.py |
3-arm FingerprintJS harness with bundle integrity check, validity gate, and activity control. |
test/test_bot_detection.py |
4-arm bot-detection harness (incl. plain Selenium control) with bundle integrity check, validity gate, and classifications. |
docs/developers/FingerprintJS-Visibility-Experiment.md |
Recorded FingerprintJS experiment write-up and results used to justify pinned expectations. |
docs/developers/Bot-Detector-Visibility-Experiment.md |
Recorded BotD/fpscanner experiment write-up and results used to justify pinned expectations. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| try { | ||
| return JSON.stringify(value); | ||
| } catch (e) { | ||
| return "__unserializable__:" + e.message; | ||
| } |
1c3f308 to
e3c3944
Compare
05a9f97 to
92cc7ce
Compare
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## feat/stealth-js-instrument-v2 #1225 +/- ##
=================================================================
- Coverage 53.71% 49.87% -3.85%
=================================================================
Files 42 44 +2
Lines 4969 5390 +421
=================================================================
+ Hits 2669 2688 +19
- Misses 2300 2702 +402 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
92cc7ce to
2c3cb1a
Compare
1b1acbc to
aa07987
Compare
2c3cb1a to
e9955c4
Compare
aa07987 to
134f292
Compare
e9955c4 to
4a59c99
Compare
134f292 to
fdc7bd9
Compare
4a59c99 to
89c7014
Compare
fdc7bd9 to
7f92ee2
Compare
89c7014 to
2512acc
Compare
7f92ee2 to
07306a7
Compare
2512acc to
ad8b33e
Compare
07306a7 to
d98499e
Compare
ad8b33e to
cbfc48c
Compare
…h and legacy The PR body and the reviewer's guide both asserted that the FingerprintJS stamp is identical under stealth and differs under the legacy instrument, with no artifact anywhere in the repository to back it. This is that experiment. Result: the claim is half true, and the persuasive half is false. - stealth == baseline: CONFIRMED. Identical on all 42 built-in components and the 4 reproducible extra probes, visitorId included. - legacy != baseline: REFUTED. Legacy is also byte-identical to baseline on every reproducible probe. All arms produce the same visitorId. There is no component on which legacy differs from baseline. The activity control rules out a dead experiment: while FingerprintJS computes its stamp, baseline records 0 rows, stealth 124 rows over 46 symbols and legacy 92 rows over 39 symbols, including HTMLCanvasElement.toDataURL, CanvasRenderingContext2D.fillText, OscillatorNode.start and window.screen.*. Both instruments were live and watching; neither perturbed the result. Legacy's invisibility here is a property of the oracle, not of the instrument. FingerprintJS OSS reads values, and the legacy wrappers are value-transparent. The bundle contains one isFunctionNative call site, guarding window.print in a Safari-detection helper Gecko never reaches, and performs no integrity check on any instrumented surface. The vectors that do expose legacy -- toString on wrapped natives, prototype shape, arity and name drift -- are bot-detection vectors, and FingerprintJS OSS is not a bot detector. TestStealthDetectability remains the correct evidence for that half of the argument. Two probes are excluded as environment noise: both halves of the forced-render canvas. A control run outside OpenWPM entirely -- bare Selenium, stock Firefox 154, no extension -- shows Firefox re-seeds canvas noise per profile: three loads in one profile are byte-identical, two fresh profiles differ, with fingerprintingProtection, resistFingerprinting and randomization all false and a -CanvasRandomization override in place. OpenWPM gives every launch a fresh profile and no pref pins it, so canvas image data is not measurable here. If legacy leaks into rendered canvas bytes, this experiment could not see it; that limitation is recorded rather than papered over. FingerprintJS v5 stabilizes its canvas source by returning "skipped" on Firefox 120+, which would have made the comparison vacuous on the most heavily instrumented surface in the preset; the forced-render probe uses the library's own getUnstableCanvasFingerprint export to get past that, and is what surfaced the per-profile randomization. The bundle is vendored byte-identical to npm so its SHA-256 reproduces upstream, pinned and asserted in-test, and loaded with monitoring disabled to suppress the 1-in-1000 telemetry request the library otherwise makes.
…ot detectors The reviewer's guide wanted to claim that off-the-shelf bot detection triggers on the legacy instrument but cannot distinguish stealth from an uninstrumented browser. This measures that against two vendored MIT oracles, BotD 2.0.0 and fpscanner 1.0.8, over four arms: plain Firefox, OpenWPM baseline, legacy and stealth. The first half of that claim is false, and the second half is false in the other direction. Legacy vs baseline: not one of the 185 oracle signals differs. Both oracles read the legacy instrument identically to an uninstrumented OpenWPM browser, while legacy demonstrably wrapped canvas, navigator and screen during the same page load (103 rows over 32 symbols). That is a coverage property of these oracles, not innocence in the instrument. BotD's only native-toString read is Function.prototype.bind, and its detectFunctionBind never compares the string -- it fires only when bind is missing. Its detectErrorTrace provokes an error inside BotD's own code and regex-matches PhantomJS. Its distinctive-properties roster does not include getInstrumentJS. fpscanner's single prototype-shape check reads Navigator.prototype, which legacy never touches: legacy patches the navigator instance, so that check reads the same value in every arm. Legacy is still detectable, and the same page load proves it. A separate paper.* namespace of 35 probes -- never counted as an oracle signal -- fires all four instrumentation tells catalogued in Krumnow, Jonker & Karsch (arXiv:2205.08890): window.getInstrumentJS is present, [native code] is gone from four wrapped natives, a getInstrumentJS/instrumentFunction/< frame sits at the top of a provoked stack trace, and prototype flattening takes HTMLCanvasElement.prototype from 10 own properties to 351. Stealth fires none of them. Stealth vs baseline: 10 signals differ, all of them navigator.webdriver, and they move in stealth's favour -- BotD's overall verdict flips from bot to not-bot. Stealth is measurably less bot-like than an uninstrumented OpenWPM browser, not indistinguishable from it. The iframe reading flips too, so the frame protection holds in a freshly created iframe. Stealth does not make OpenWPM undetectable as automation, and a test asserts that so this module cannot be repurposed as evidence for the opposite: fpscanner's fast bot detection returns true in all four arms via the UTC timezone signal, and the display-less signature from the paper's Table 4 -- availTop and availLeft at zero, WebGL absent -- is present everywhere. The plain-Firefox arm exists to attribute signals rather than assume them. It differs from OpenWPM baseline on zero signals, which is what licenses calling every firing signal an automation tell rather than an OpenWPM tell, and shows the extension's mere presence adds nothing either oracle can see. Both bundles are vendored byte-identical to npm so their hashes reproduce upstream, pinned and asserted in-test. Both ship ESM only, so the page loads them as modules rather than repacking them. BotD is loaded with monitoring disabled; fpscanner has no network call site.
d98499e to
823e81e
Compare
cbfc48c to
a368115
Compare
Stacked on #1154. Split out of it so that PR stays focused on the instrument.
Two experiments that measure how visible the legacy and stealth JS instruments are to off-the-shelf, third-party oracles, rather than to probes we wrote ourselves. Both were built to settle a claim the #1154 description used to make with no artifact behind it. Both refuted it.
What was claimed, and what is actually true
The old #1154 body asserted a differential FingerprintJS oracle showing stealth byte-identical to vanilla while legacy differed. There was no such harness anywhere in git history. Measured:
navigator.webdriver, in stealth's favourFingerprintJS reads values, and the legacy wrappers are value-transparent, so it cannot see legacy at all. Neither can the two OSS bot detectors, for a concrete and documented reason: BotD's only native-
toStringread isFunction.prototype.bindand it never compares the string; itsdetectErrorTraceprovokes an error inside BotD's own code; its bot-globals roster has nogetInstrumentJS; and fpscanner's one prototype-shape check readsNavigator.prototype, which legacy never touches because it patches the navigator instance.This is a coverage property of these oracles, not innocence in the instrument. Measured in the same page load, via a separate namespace never counted as an oracle signal, all four instrumentation tells from Krumnow, Jonker & Karsch (arXiv:2205.08890) fire on legacy and none fire on stealth:
window.getInstrumentJSpresent,[native code]gone from four wrapped natives, agetInstrumentJS/instrumentFunction/<frame atop a provoked stack, and prototype flattening takingHTMLCanvasElement.prototypefrom 10 own properties to 351.What this does not claim
Stealth does not make OpenWPM undetectable as automation, and a test asserts that so these modules cannot be repurposed as evidence for the opposite. fpscanner's fast bot detection returns true in all four arms, and the paper's Table 4 display-less signature is present everywhere. Those are automation tells, inherent to running under Selenium in a display-less environment — not something the JS instrument causes or the stealth instrument can fix.
Method
Four arms — plain Firefox (bare Selenium, no extension), OpenWPM baseline, legacy with the project's own
collection_fingerprintingpreset, and stealth. The plain-Firefox arm exists to attribute signals rather than assume: it differs from OpenWPM baseline on zero signals, which is what licenses calling every firing signal an automation tell.Each arm runs twice first as a validity gate. Unstable signals are excluded and reported with their observed values — two canvas probes were excluded after a control run outside OpenWPM entirely showed Firefox re-seeds canvas noise per profile, with all anti-fingerprinting prefs off. Consequence, recorded rather than hidden: canvas image data is not measurable in this environment, so if legacy leaks into rendered canvas bytes, these experiments could not see it.
Every arm-set run is listed in the reports, including the discarded ones and why — including a probe defect that would have reported a vacuous canvas comparison, since FingerprintJS v5 deliberately returns
"skipped"for canvas on Firefox 120+.Contents
test/test_fingerprintjs_stamp.py(6 tests),test/test_bot_detection.py(9 tests) — 15 passing, ~5 mintest/test_pages/vendor/— FingerprintJS 5.2.0, BotD 2.0.0, fpscanner 1.0.8, all MIT, vendored byte-identical to npm with SHA-256 asserted in-test so the hashes reproduce upstream. Telemetry disabled where the library has any.docs/developers/FingerprintJS-Visibility-Experiment.md,docs/developers/Bot-Detector-Visibility-Experiment.md— in-repo, because tests citing a gitignored report is a broken citation.Review the reports first; the tests pin what the reports measured.