Skip to content

Third-party scripts detect Lighthouse via its injected globals (__nativePromise / __nativePerformance) to evade measurement #17176

Description

@jacoborbach

Summary

#15829 documents scripts detecting Lighthouse via the user agent, navigator.platform, and the moto g power string. While benchmarking chat widgets I found a technique that isn't on that list and appears undocumented: detecting Lighthouse through the globals it injects into every audited page.

ExecutionContext.cacheNativesOnNewDocument() (core/gather/driver/execution-context.js) stashes native references on the page as window.__nativePromise, window.__nativeURL, window.__nativePerformance, window.__nativeFetch, etc., via Page.addScriptToEvaluateOnNewDocument. Those names are stable and only exist on pages Lighthouse is auditing — which makes them a perfect fingerprint that survives UA spoofing.

In the wild

Crisp's chat widget loader (https://client.crisp.chat/l.js) gates initialization on exactly this check. Reproduce:

curl -s https://client.crisp.chat/l.js | grep -o 'window.__nativePerformance&&window.__nativePromise&&(t=!1)'

window.__nativePerformance&&window.__nativePromise&&(t=!1)

When both globals exist, the loader sets its "don't load" flag and the widget (~264KB beyond the loader) never initializes — so any Lighthouse run measures a page without the widget, regardless of user agent. The same loader also carries "Lighthouse", "GTmetrix", and "HeadlessChrome" in a UA blocklist, which still catches PSI (it appends Chrome-Lighthouse, per #15829).

Timeline via archive.org snapshots of the same file: the "Lighthouse" UA entry appears between the Oct and Nov 2020 snapshots; after #14384 removed the UA marker (Sept 2022), the __nativePerformance check appears between the April and May 2023 snapshots. Snapshot of the current file: https://web.archive.org/web/20260815204046/https://client.crisp.chat/l.js

I verified empirically: headful Chrome, clean emulated UA, Lighthouse 12.8.2 → widget transfers ~3KB (loader only). Same page in a non-Lighthouse browser → ~267KB and a rendered widget.

Suggested mitigation

Randomize or namespace the cached-native global names per run (e.g. a random suffix generated at session start), or move them off window into the isolated world Lighthouse already uses for most evaluation. The stable, documented-in-source names are what make this fingerprint reliable.

Disclosure

I build a competing chat widget, which is how I ran into this — happy to share the full benchmark methodology if useful.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions