GoogleChrome / GoogleChrome/lighthouse

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

Open
#17,176 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
JavaScript
Stars
30.8k
Forks
9.8k
Avg merge
1d 14h
Merged PRs (30d)
20

Description

**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.

Contributor guide

Open the contributing guide

Research direction

Start in core/gather/driver/execution-context.js at ExecutionContext.cacheNativesOnNewDocument() and trace how the cached-native globals are created through Page.addScriptToEvaluateOnNewDocument. Reproduce the behavior with the Crisp loader and clean user-agent setup described in the issue. Done means the cached references remain usable while stable Lighthouse-specific globals no longer provide this fingerprint.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript
Domain
performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.