Back to Knowledge Hub
Fingerprint

CreepJS Android WebView Detection: Signal Limits

Map Android WebView signals across HTTP, page JavaScript, workers, and app bridges; see what CreepJS source checks and what it cannot prove.

BotBrowser Team

Documentation

Want the structured docs for Fingerprint?

This article lives in the editorial library. For step-by-step setup, reference material, and ongoing updates, jump into the docs section.

BotBrowser supports declared Android WebView identity for authorized compatibility checks; it cannot decide how a private anti-bot service classifies an embedded browser. The WebView configuration uses an explicit --user-agent template containing wv; it does not choose an app-specific X-Requested-With value. The useful question is narrower: which layer supplied each value, and which comparisons can the page actually make?

TL;DR

  • ; wv in a User-Agent is a WebView-family clue, not proof of a particular app.
  • X-Requested-With and window.android depend on host-app behavior; neither is universal.
  • The pinned CreepJS navigator module compares selected page values with worker values and reads User-Agent Client Hints.
  • A mismatch says that two observations differ. It does not identify the cause or establish an automation verdict.

Contents

Signal layers: HTTP headers, page JavaScript, worker values, and optional app bridges, with their interpretation limits
Signal provenance separates browser observations from application identity and automation conclusions.

Watch the video

This English-language video separates request headers, page JavaScript, worker values, and optional app bridges, then shows what a controlled browser run can and cannot establish. It does not authenticate an embedding app or reproduce an Android WebView device.

The embedding app is not the browser surface

Android WebView is a browser component hosted inside an Android application. A page can observe browser and request properties, while the host app may choose a User-Agent, add request headers, or expose a JavaScript bridge. Those are separate choices with separate scopes and should be recorded as such.

The BotBrowser Android WebView documentation covers how its declared identity settings expose a wv User-Agent form and optional app headers. The detection question is narrower: what can a detector infer from each surface, and what remains unknown? For setup and identity alignment, use the Android WebView configuration guide; for how CreepJS groups independent readings, see CreepJS signal groups and observation limits.

Where each signal comes from

SignalObservable layerWhat it can supportWhat it cannot establish alone
User-Agent with ; wvHTTP request and navigator.userAgentA WebView-style UA was presentedWhich app embedded the view, or whether the value is current
Sec-CH-UA and navigator.userAgentDataRequest headers and page JavaScript, when availableBrand and platform details exposed by that contextThat every Android WebView exposes the same hints
navigator.platformPage JavaScriptA platform-family observationA device model or embedding application
WorkerNavigator valuesWorker JavaScriptWhether selected values agree across page and worker contextsWhether either value matches physical hardware
X-Requested-WithHTTP request, when the app sends itAn application may have supplied a package-like headerA universal WebView marker or a trusted app identity
window.android or another bridgePage JavaScript, only if injectedThe host exposed a named JavaScript interfaceThat every app exposes it or that its methods are safe to call

Android exposes the User-Agent through WebSettings, and the host can configure it. Android also documents addJavascriptInterface as an explicit application operation; the bridge name is selected by the app. As a result, a missing bridge does not rule out WebView, and a present bridge name should not be treated as an authenticated package identifier.

Headers and JavaScript belong in separate rows of an observation record. A server sees a request header before page code runs; page code sees JavaScript properties in its own browsing context. A redirect, service worker, app wrapper, or version change can affect one layer without changing the others.

Browser-family labels are coarse

The CreepJS helper getReportedPlatform() reduces several operating-system labels to broad families. In its User-Agent path, android, linux, and cros map to the same Linux classifier. Its navigator.platform path also maps android, arm, and linux to that family. This is useful for a consistency check, but it deliberately loses distinctions: a matching Android-family result does not identify WebView, and it cannot name the embedding app.

The navigator module performs a separate substring check that accepts android among its recognizable platform labels. That check answers whether the string looks familiar to this source revision. That is distinct from the family comparison and does not identify which app produced the string. Keep the raw field, the normalized family, and the check result as different columns. A report that stores only Android = true discards whether that value came from the UA, navigator.platform, or a request header.

This distinction is especially useful when a page is reviewed across WebView, Chrome Custom Tabs, and full Chrome. They may all run on Android while exposing different combinations of browser identity and host-app behavior. A broad family agreement is not evidence that their brands, request headers, or JavaScript bridges are identical.

The pinned module does not treat every unusual value as the same finding. When the global chrome object exists, it checks whether navigator.userAgent contains navigator.appVersion; it also looks for leading, trailing, or repeated whitespace and applies a source-defined gibberish check. Those are User-Agent quality observations. The code then compares userAgent with the worker value and marks a cross-context difference separately.

For deviceMemory, the source recognizes the values 0.25, 0.5, 1, 2, 4, 8, 16, and 32. If performance.memory.jsHeapSizeLimit is available, it converts that limit to gigabytes and checks whether it exceeds the reported device-memory value. It also compares deviceMemory with the worker. hardwareConcurrency has its own page-to-worker equality check. These are separate conditions, not one Android score.

The language path compares the first two characters of navigator.language and the first item in navigator.languages. When both page fields are truthy, the source returns the formatted language result after that comparison, whether or not the prefixes match. The worker-language comparisons occur only outside that branch. Plugin handling is different again: the source pairs navigator.plugins with navigator.mimeTypes and runs consistency checks on that pair. A useful compatibility record therefore names the exact property and trigger instead of converting a collection of unrelated checks into a single WebView label.

Client Hints is an optional branch

The CreepJS navigator module returns no User-Agent Client Hints result when navigator.userAgentData or getHighEntropyValues() is unavailable. When both exist, the source requests platform, platformVersion, architecture, bitness, model, and uaFullVersion. It then combines those returned values with the low-entropy brands and mobile fields.

The source normalizes the brand list before presenting it: brands whose names contain Not are excluded, and Chromium is removed from the displayed list when multiple brands are present. That transformation is part of the library's output format. It is not evidence that the removed brand was absent from the browser, nor that every platform returned the same high-entropy fields.

Treat three outcomes differently: the API is absent, a requested value is omitted, or the API returns a value. The cited sources establish that Client Hints are conditional but do not assign a cause to an omitted value. Record the observed state before comparing contexts; otherwise a missing hint can be misreported as an inconsistent hint.

What the pinned CreepJS source checks

The public CreepJS revision cited here is pinned to commit 10aa6724cd33a1015db1574211890518cd04f0cc. Its navigator module accepts Android among several recognizable navigator.platform substrings. It also compares the operating-system family parsed from the User-Agent with the reported platform family. That is a consistency check, not a WebView-specific classification rule.

The same module compares selected values from the page's navigator with values collected from a worker scope. The source includes platform, userAgent, deviceMemory, hardwareConcurrency, language, and languages comparisons, with the language-to-worker comparisons outside the branch where both page-language fields are truthy. Its User-Agent Client Hints path asks for fields such as platform, platformVersion, architecture, bitness, model, and uaFullVersion when the API is available. These are concrete implementation details in the pinned navigator module, not a claim that all Android WebViews provide every field.

The separate headless module reports groups of browser-visible conditions. Its group labels and inputs should not be collapsed into an Android WebView verdict. The broader CreepJS headless analysis explains why those groups answer different questions.

The code therefore supports a bounded statement: the inspected CreepJS modules can compare selected browser surfaces and report selected headless-related conditions. They do not, in these checks, authenticate the Android package that created the WebView or reveal a third party's decision rule.

Separate WebView evidence from other conclusions

Use four labels in a compatibility report instead of one binary verdict:

Report labelMinimum evidenceSafe interpretation
Android-family contextA coherent set of Android platform and UA observationsThe visible browser context reports an Android family
WebView-style contextA WebView-style UA or brand observation, with request and page values recorded separatelyThe context presents one or more WebView-associated properties
Cross-context differenceA named page/worker or header/JavaScript comparison differsThose two observations disagree in this run
Automation conclusionIndependent evidence and a documented decision ruleA page-side signal alone is insufficient

This distinction matters when reviewing browser-brand alignment. Brand, platform, and embedded-app behavior are related, but they are not interchangeable fields. A changed navigator.platform cannot explain a changed request header unless the same run links both observations and records their provenance.

A mismatch establishes only a difference

In this source, a page-to-worker mismatch is evidence that the compared values differ in the observed contexts. It does not establish whether the page value, worker value, or both are expected for a particular browser release. It also does not establish why the difference occurred. The source code documents its own comparison; it does not provide an authenticated inventory of the app, operating system, browser provider, or third-party policy.

Keep four statements separate in a report. First, record the observed value and its layer. Second, state whether a field is present and readable. Third, report the exact comparison that differs, such as navigator.platform versus the worker's platform. Fourth, state the narrow consequence for the owned compatibility test. Only an independently documented decision rule can justify a broader automation conclusion; the public CreepJS inputs alone do not disclose such a rule for another service.

When a report shows one changed field, do not silently propagate that finding to the rest of the session. A changed X-Requested-With request header says nothing by itself about navigator.userAgentData; a worker mismatch says nothing by itself about an app bridge. This keeps a useful diagnostic signal from becoming a claim about identity that the measurement did not make.

Font results are a separate surface too: the CreepJS font-table analysis shows why a detected font group cannot identify the app embedding a WebView.

The privacy consequence is also concrete. window.android may expose callable application methods, and Android warns that JavaScript bridges enlarge the security boundary. A detector review should record whether a bridge exists without invoking unrelated methods or collecting app data that the test does not need.

A controlled comparison for owned apps

For an app you own, compare three contexts on the same Android release and app build: the app's WebView, a Chrome Custom Tab if the product uses one, and full Chrome. Keep the page origin and test action constant. The table below is a capture plan, not a report of a run:

Capture pointRecordKeep separate
RequestUser-Agent, Client Hint headers, X-Requested-With if presentInitial navigation versus later requests
Pagenavigator.userAgent, navigator.platform, available userAgentData fieldsMissing, denied, and empty are different states
WorkerThe corresponding WorkerNavigator fieldsThe worker's creation timing and origin
Host appWhether a bridge is intentionally registered and its documented nameDo not call bridge methods just to identify the container

Version the Android OS, WebView provider, app build, and test page together. A comparison across different provider versions can make a legitimate update look like an identity inconsistency. For each changed cell, record the source layer and whether the field was absent, unavailable, or different; do not convert those states into a single score.

Preserve context and minimize collection

Run the same page-owned action from the same origin and keep navigation timing comparable. Record Android release, WebView provider version, app build, browser version, locale, permission state, and whether the context is WebView, a Chrome-powered tab, or full Chrome. These fields explain what was compared; they are not themselves a verdict. If a browser update changes a value, retain both version labels rather than describing the new value as a lie.

The host app's package-like X-Requested-With header can identify an app package, so store it only when the compatibility question requires it and access is authorized. Prefer a presence flag or a redacted value in a shared report. A JavaScript bridge can expose callable methods; record whether the app intentionally registers it and its documented name, but do not invoke unrelated methods or serialize returned app data. The Android warning about addJavascriptInterface makes this a security boundary as well as a fingerprinting surface.

Keep each comparison tied to its source: request capture for headers, page capture for navigator, and worker capture for worker values. A compact record can retain the field name, observed state, browser/app versions, and comparison partner without storing unrelated user content. That is enough to reproduce an owned compatibility review while reducing accidental collection of package identifiers and application data.

Keep request timing comparable

Capture the top-level navigation separately from later subresource requests. An app can attach a header only to selected requests, so a header on one image fetch should not be compared with JavaScript values from the final document as if they came from one observation. Keep each request tied to its navigation, origin, app build, browser version, and permission state. If redirects occur, record the request at each hop instead of treating the first and final URL as the same transaction. A comparison describes the context and point in time that supplied it; it does not generalize to every device with the same Android major version.

FAQ

Does ; wv prove that a request came from Android WebView?

No. It is a recognizable string in a User-Agent, which an application can configure. It is useful evidence about the presented value, not proof of the embedding package or the request's origin.

Does CreepJS identify the Android app hosting the page?

The cited navigator checks compare browser-visible values and worker values. They do not authenticate the host application's package. An app-injected bridge or request header is conditional application behavior, not a universal CreepJS identity signal.

Does a page/worker mismatch mean the browser is automated?

No. It means the captured values differ. Browser version changes, API support, context timing, or application configuration can also explain a difference. Preserve the exact fields and environment before drawing a broader conclusion.

Sources

Practical conclusion

Treat WebView identification as a provenance question, not a single-token test. Record request headers, page properties, worker comparisons, and optional app bridges as separate observations. For owned-app QA, compare equivalent contexts and preserve missing values; a mismatch is a useful compatibility finding, but it does not establish a device identity or a third-party automation decision.

#CreepJS#Android WebView#Browser Signals

This article is part of the guide: What Is a Browser Fingerprint?

Take BotBrowser from research to production

The guides cover the model first, then move into cross-platform validation, isolated contexts, and scale-ready browser deployment.