What this app collects, why each signal matters, and where fingerprinting's real limits are.
Any characteristic a server or a page's JavaScript can observe about the client making a request, combined into a profile that can distinguish one client from another — without relying on cookies or logins.
Fingerprinting is usually split into two families. Passive fingerprinting collects whatever the client sends anyway as a side effect of making a normal request — HTTP headers, TLS handshake characteristics, connection metadata. The client doesn't have to run any code for this; even curl produces a passive fingerprint. Active fingerprinting requires running code on the client — JavaScript that queries navigator, renders to a <canvas>, measures audio processing, etc. — and only works against clients that execute JavaScript, i.e. real or automated browsers.
This app demonstrates both. The fingerprint page shows server-observed (passive) signals for every request, and layers on browser-observed (active) signals whenever JavaScript runs.
The request method, HTTP version, target, and full header set — plus, for HTTP/1.1, the exact order headers were sent in — are all passively observable. Different browsers, browser versions, and HTTP libraries (curl, Python requests, Go's net/http, …) each have a characteristic header set/order, because it's generated by their networking stack rather than typed by a user. This is why a Python script that copies a browser's User-Agent string still doesn't fully "look like" that browser — the rest of the header shape usually gives it away.
Reference: MDN: HTTP headers
User-Agent Client Hints are a structured, opt-in replacement for parts of the User-Agent string. A small set of "low-entropy" hints (browser brand, whether the device is mobile, platform) are sent automatically by supporting browsers. Higher-entropy hints (full version, platform version, device model, architecture) are only sent once a server asks for them via the Accept-CH response header — which is why this app's first response to a fresh connection legitimately won't have them reflected back yet.
Client Hints were designed partly in response to User-Agent-string fingerprinting concerns: they intentionally include randomized "greased" fake brand entries so that naive string parsing breaks, and Chromium is progressively freezing the traditional User-Agent string's version/platform detail in favor of this mechanism.
Reference: W3C/WICG User-Agent Client Hints
Before any HTTP request is even parsed, the client's TLS library sends a ClientHello listing the cipher suites, extensions, elliptic curves, and signature algorithms it supports, in a specific order. That order and set is characteristic of the TLS library and its configuration — curl, Python's requests, and a real browser all typically produce different ClientHellos even when their HTTP headers are made to look identical, because the TLS handshake happens beneath the application code that sets headers.
JA3 hashes five ClientHello-derived fields together (version, ciphers, extensions, curves, point formats). JA4 is a newer, partly human-readable successor that addresses some JA3 weaknesses. This app computes both itself, from the raw handshake bytes — see the JA4 spec for the full algorithm and docs/TLS_CAPTURE.md in this repo for exactly how this server gets access to those raw bytes at all (Node's TLS module doesn't expose them by default).
References: Salesforce: JA3/JA3S · RFC 8446 §4.1.2
HTTP/2 clients negotiate connection-level SETTINGS (max concurrent streams, header table size, initial window size, …) and, in principle, send request pseudo-headers (:method, :path, …) in a client-characteristic order. This app reports the negotiated SETTINGS values, but — honestly, per this project's own rule against overclaiming — does not report pseudo-header order: Node's HTTP/2 "compatibility API" (which lets a familiar request/response style work over HTTP/2) normalizes headers into a plain object before application code ever sees them, discarding that order. Getting it back would mean dropping to Node's lower-level stream API. See docs/ARCHITECTURE.md.
Once a page's JavaScript runs, a much larger surface opens up: navigator properties, screen geometry, installed fonts, WebGL/canvas rendering output, audio-processing output, feature availability, and more. Two things make these interesting beyond "more data": first, many of them (canvas, WebGL, audio) reflect actual hardware/driver/OS behavior rather than a string the client chose to send, which makes them harder to spoof convincingly than a header. Second, in combination, high-entropy signals like these have repeatedly been shown by independent research to narrow a browser down to a small set — sometimes uniquely — among a large population. See uniqueness and entropy below.
Canvas fingerprinting draws fixed text/shapes to a hidden <canvas> and reads back the resulting pixels — subtly different across GPUs, drivers, OS font rasterizers, and anti-aliasing settings. WebGL exposes the actual GPU vendor/renderer string (when the WEBGL_debug_renderer_info extension is available) and a long list of supported extensions. Audio fingerprinting renders a short signal through an oscillator and a dynamics compressor on an OfflineAudioContext (never played aloud) and reads back the resulting samples — floating-point DSP implementation differences produce a measurably different, but per-machine-deterministic, result. Font detection compares text metrics rendered in a candidate font vs. generic fallbacks to infer which fonts are actually installed, without any API that lists them directly (browsers deliberately don't expose a full font list).
This app hashes the canvas/audio output rather than shipping the full raw pixel/sample data, for compactness — see the signal explorer's "collection method" column for exactly how each value was produced.
No single signal proves a client is automated. This app instead collects several independent signals of varying reliability:
true. First-party (from the browser engine itself), harder to spoof than a header, but not impossible.HeadlessChrome, Puppeteer, Selenium, or a non-browser library name (curl, python-requests). Trivially spoofable on its own.The fingerprint page reports each of these as separate, hedged assessments with an explicit confidence level and the exact evidence cited — never a single "bot: yes/no" verdict.
Reference: W3C WebDriver specification
Individual signals are each spoofable. But spoofing all of them consistently is harder — a script that sets a Chrome User-Agent but leaves Sec-CH-UA-Platform reporting Linux (or doesn't send Client Hints at all), or a browser reporting Accept-Language: en-US while navigator.languages[0] is fr-FR, is exhibiting a self-contradiction that's independent of any single signal's own spoofability. This app's interpretation layer runs a small, extensible set of rules (see src/server/interpret/rules/consistency.ts) that compare independently-collected signal pairs and report matches or conflicts as separate, evidenced assessments — new checks are just new rule functions, nothing else has to change.
A signal is "high-entropy" if, across a real population of browsers, its value varies enough to significantly narrow down which specific device/browser produced a given fingerprint. The EFF's long-running Cover Your Tracks project (formerly Panopticlick) and the original 2010 research behind it found that combinations of signals like these — screen/canvas/WebGL/font data especially — can narrow a browser down to a small handful, sometimes a single device, out of a very large sample.
This app deliberately does not compute a numeric uniqueness score for any individual fingerprint: doing that honestly requires a population dataset to compare against, which this stateless, non-persisting MVP has no way to build (see privacy). The identifiability assessment you may see on the fingerprint page names which of the commonly-cited high-entropy signal categories are present in your fingerprint, citing the general research finding — not a calculated score for you specifically.
Reference: Eckersley, "How Unique Is Your Web Browser?" (PETS 2010)
Every signal this app collects is either client-supplied (and therefore directly spoofable by a sufficiently motivated client) or derived/inferred by this app's own heuristics (and therefore only as reliable as those heuristics — see each derived attribute's method field). Even TLS-handshake-level signals like JA3/JA4, which are much harder to fake convincingly than a header because they come from the underlying TLS library rather than application code, can in principle be replicated by a sufficiently customized TLS client. Nothing here should be read as cryptographic proof of anything; it's evidence, weighted by how hard each signal is to fake, and presented as such (see cross-signal analysis).
Mouse trajectories, keystroke dynamics, scroll behavior, and interaction timing are a distinct fingerprinting/detection family that this app deliberately does not implement — it requires sustained user interaction to collect, which doesn't fit a single-page "look at your fingerprint" tool, and raises its own separate privacy considerations. The canonical schema this app uses was designed so a future behavioral signal category could be added without disturbing anything that already exists (see docs/SCHEMA.md, "Extensibility rules").
Every signal on the fingerprint page is one of four things, kept deliberately separate rather than collapsed into one field:
navigator property value).The full design rationale is in docs/SCHEMA.md in this project's repository.
This app does not persist fingerprints. Every fingerprint you see exists only for the lifetime of the request/response cycle that produced it — nothing is written to a database, and there are no tracking cookies. No outbound calls are made to third-party IP-intelligence/geolocation services (those fields are reported as not_configured rather than silently omitted, which is itself a deliberate design choice — see docs/SCHEMA.md). Proxy headers like X-Forwarded-For are shown, but never trusted to override the directly-observed TCP peer address, since anyone can send an arbitrary value in an untrusted deployment.