fpscanner 0.2.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/README.md +639 -55
  2. package/bin/cli.js +216 -0
  3. package/dist/crypto-helpers.d.ts +19 -0
  4. package/dist/crypto-helpers.d.ts.map +1 -0
  5. package/dist/detections/hasCDP.d.ts +3 -0
  6. package/dist/detections/hasCDP.d.ts.map +1 -0
  7. package/dist/detections/hasContextMismatch.d.ts +3 -0
  8. package/dist/detections/hasContextMismatch.d.ts.map +1 -0
  9. package/dist/detections/hasHeadlessChromeScreenResolution.d.ts +3 -0
  10. package/dist/detections/hasHeadlessChromeScreenResolution.d.ts.map +1 -0
  11. package/dist/detections/hasHighCPUCount.d.ts +3 -0
  12. package/dist/detections/hasHighCPUCount.d.ts.map +1 -0
  13. package/dist/detections/hasImpossibleDeviceMemory.d.ts +3 -0
  14. package/dist/detections/hasImpossibleDeviceMemory.d.ts.map +1 -0
  15. package/dist/detections/hasMismatchPlatformIframe.d.ts +3 -0
  16. package/dist/detections/hasMismatchPlatformIframe.d.ts.map +1 -0
  17. package/dist/detections/hasMismatchPlatformWorker.d.ts +3 -0
  18. package/dist/detections/hasMismatchPlatformWorker.d.ts.map +1 -0
  19. package/dist/detections/hasMismatchWebGLInWorker.d.ts +3 -0
  20. package/dist/detections/hasMismatchWebGLInWorker.d.ts.map +1 -0
  21. package/dist/detections/hasMissingChromeObject.d.ts +3 -0
  22. package/dist/detections/hasMissingChromeObject.d.ts.map +1 -0
  23. package/dist/detections/hasPlaywright.d.ts +3 -0
  24. package/dist/detections/hasPlaywright.d.ts.map +1 -0
  25. package/dist/detections/hasSeleniumProperty.d.ts +3 -0
  26. package/dist/detections/hasSeleniumProperty.d.ts.map +1 -0
  27. package/dist/detections/hasSwiftshaderRenderer.d.ts +3 -0
  28. package/dist/detections/hasSwiftshaderRenderer.d.ts.map +1 -0
  29. package/dist/detections/hasUTCTimezone.d.ts +3 -0
  30. package/dist/detections/hasUTCTimezone.d.ts.map +1 -0
  31. package/dist/detections/hasWebdriver.d.ts +3 -0
  32. package/dist/detections/hasWebdriver.d.ts.map +1 -0
  33. package/dist/detections/hasWebdriverIframe.d.ts +3 -0
  34. package/dist/detections/hasWebdriverIframe.d.ts.map +1 -0
  35. package/dist/detections/hasWebdriverWorker.d.ts +3 -0
  36. package/dist/detections/hasWebdriverWorker.d.ts.map +1 -0
  37. package/dist/detections/hasWebdriverWritable.d.ts +3 -0
  38. package/dist/detections/hasWebdriverWritable.d.ts.map +1 -0
  39. package/dist/fpScanner.cjs.js +31 -0
  40. package/dist/fpScanner.es.js +1066 -0
  41. package/dist/index.d.ts +39 -0
  42. package/dist/index.d.ts.map +1 -0
  43. package/dist/signals/browserExtensions.d.ts +5 -0
  44. package/dist/signals/browserExtensions.d.ts.map +1 -0
  45. package/dist/signals/browserFeatures.d.ts +14 -0
  46. package/dist/signals/browserFeatures.d.ts.map +1 -0
  47. package/dist/signals/canvas.d.ts +6 -0
  48. package/dist/signals/canvas.d.ts.map +1 -0
  49. package/dist/signals/cdp.d.ts +2 -0
  50. package/dist/signals/cdp.d.ts.map +1 -0
  51. package/dist/signals/cpuCount.d.ts +2 -0
  52. package/dist/signals/cpuCount.d.ts.map +1 -0
  53. package/dist/signals/etsl.d.ts +2 -0
  54. package/dist/signals/etsl.d.ts.map +1 -0
  55. package/dist/signals/highEntropyValues.d.ts +11 -0
  56. package/dist/signals/highEntropyValues.d.ts.map +1 -0
  57. package/dist/signals/iframe.d.ts +9 -0
  58. package/dist/signals/iframe.d.ts.map +1 -0
  59. package/dist/signals/internationalization.d.ts +5 -0
  60. package/dist/signals/internationalization.d.ts.map +1 -0
  61. package/dist/signals/languages.d.ts +5 -0
  62. package/dist/signals/languages.d.ts.map +1 -0
  63. package/dist/signals/maths.d.ts +2 -0
  64. package/dist/signals/maths.d.ts.map +1 -0
  65. package/dist/signals/mediaCodecs.d.ts +11 -0
  66. package/dist/signals/mediaCodecs.d.ts.map +1 -0
  67. package/dist/signals/mediaQueries.d.ts +13 -0
  68. package/dist/signals/mediaQueries.d.ts.map +1 -0
  69. package/dist/signals/memory.d.ts +2 -0
  70. package/dist/signals/memory.d.ts.map +1 -0
  71. package/dist/signals/multimediaDevices.d.ts +2 -0
  72. package/dist/signals/multimediaDevices.d.ts.map +1 -0
  73. package/dist/signals/navigatorPropertyDescriptors.d.ts +2 -0
  74. package/dist/signals/navigatorPropertyDescriptors.d.ts.map +1 -0
  75. package/dist/signals/nonce.d.ts +2 -0
  76. package/dist/signals/nonce.d.ts.map +1 -0
  77. package/dist/signals/platform.d.ts +2 -0
  78. package/dist/signals/platform.d.ts.map +1 -0
  79. package/dist/signals/playwright.d.ts +2 -0
  80. package/dist/signals/playwright.d.ts.map +1 -0
  81. package/dist/signals/plugins.d.ts +9 -0
  82. package/dist/signals/plugins.d.ts.map +1 -0
  83. package/dist/signals/screenResolution.d.ts +12 -0
  84. package/dist/signals/screenResolution.d.ts.map +1 -0
  85. package/dist/signals/seleniumProperties.d.ts +2 -0
  86. package/dist/signals/seleniumProperties.d.ts.map +1 -0
  87. package/dist/signals/time.d.ts +2 -0
  88. package/dist/signals/time.d.ts.map +1 -0
  89. package/dist/signals/toSourceError.d.ts +5 -0
  90. package/dist/signals/toSourceError.d.ts.map +1 -0
  91. package/dist/signals/url.d.ts +2 -0
  92. package/dist/signals/url.d.ts.map +1 -0
  93. package/dist/signals/userAgent.d.ts +2 -0
  94. package/dist/signals/userAgent.d.ts.map +1 -0
  95. package/dist/signals/utils.d.ts +11 -0
  96. package/dist/signals/utils.d.ts.map +1 -0
  97. package/dist/signals/webGL.d.ts +5 -0
  98. package/dist/signals/webGL.d.ts.map +1 -0
  99. package/dist/signals/webdriver.d.ts +2 -0
  100. package/dist/signals/webdriver.d.ts.map +1 -0
  101. package/dist/signals/webdriverWritable.d.ts +2 -0
  102. package/dist/signals/webdriverWritable.d.ts.map +1 -0
  103. package/dist/signals/webgpu.d.ts +7 -0
  104. package/dist/signals/webgpu.d.ts.map +1 -0
  105. package/dist/signals/worker.d.ts +2 -0
  106. package/dist/signals/worker.d.ts.map +1 -0
  107. package/dist/types.d.ts +207 -0
  108. package/dist/types.d.ts.map +1 -0
  109. package/package.json +58 -15
  110. package/scripts/build-custom.js +246 -0
  111. package/src/crypto-helpers.ts +50 -0
  112. package/src/detections/hasCDP.ts +5 -0
  113. package/src/detections/hasContextMismatch.ts +19 -0
  114. package/src/detections/hasHeadlessChromeScreenResolution.ts +10 -0
  115. package/src/detections/hasHighCPUCount.ts +9 -0
  116. package/src/detections/hasImpossibleDeviceMemory.ts +9 -0
  117. package/src/detections/hasMismatchPlatformIframe.ts +10 -0
  118. package/src/detections/hasMismatchPlatformWorker.ts +10 -0
  119. package/src/detections/hasMismatchWebGLInWorker.ts +13 -0
  120. package/src/detections/hasMissingChromeObject.ts +6 -0
  121. package/src/detections/hasPlaywright.ts +5 -0
  122. package/src/detections/hasSeleniumProperty.ts +5 -0
  123. package/src/detections/hasSwiftshaderRenderer.ts +5 -0
  124. package/src/detections/hasUTCTimezone.ts +5 -0
  125. package/src/detections/hasWebdriver.ts +5 -0
  126. package/src/detections/hasWebdriverIframe.ts +5 -0
  127. package/src/detections/hasWebdriverWorker.ts +5 -0
  128. package/src/detections/hasWebdriverWritable.ts +5 -0
  129. package/src/globals.d.ts +10 -0
  130. package/src/index.ts +644 -0
  131. package/src/signals/browserExtensions.ts +57 -0
  132. package/src/signals/browserFeatures.ts +24 -0
  133. package/src/signals/canvas.ts +84 -0
  134. package/src/signals/cdp.ts +18 -0
  135. package/src/signals/cpuCount.ts +5 -0
  136. package/src/signals/etsl.ts +3 -0
  137. package/src/signals/highEntropyValues.ts +48 -0
  138. package/src/signals/iframe.ts +34 -0
  139. package/src/signals/internationalization.ts +24 -0
  140. package/src/signals/languages.ts +6 -0
  141. package/src/signals/maths.ts +30 -0
  142. package/src/signals/mediaCodecs.ts +120 -0
  143. package/src/signals/mediaQueries.ts +85 -0
  144. package/src/signals/memory.ts +5 -0
  145. package/src/signals/multimediaDevices.ts +34 -0
  146. package/src/signals/navigatorPropertyDescriptors.ts +17 -0
  147. package/src/signals/nonce.ts +3 -0
  148. package/src/signals/platform.ts +3 -0
  149. package/src/signals/playwright.ts +3 -0
  150. package/src/signals/plugins.ts +70 -0
  151. package/src/signals/screenResolution.ts +15 -0
  152. package/src/signals/seleniumProperties.ts +40 -0
  153. package/src/signals/time.ts +3 -0
  154. package/src/signals/toSourceError.ts +27 -0
  155. package/src/signals/url.ts +3 -0
  156. package/src/signals/userAgent.ts +3 -0
  157. package/src/signals/utils.ts +29 -0
  158. package/src/signals/webGL.ts +28 -0
  159. package/src/signals/webdriver.ts +3 -0
  160. package/src/signals/webdriverWritable.ts +15 -0
  161. package/src/signals/webgpu.ts +28 -0
  162. package/src/signals/worker.ts +77 -0
  163. package/src/types.ts +237 -0
  164. package/.babelrc +0 -3
  165. package/.travis.yml +0 -17
  166. package/src/fpScanner.js +0 -222
  167. package/test/test.html +0 -11
  168. package/test/test.js +0 -116
package/README.md CHANGED
@@ -1,78 +1,662 @@
1
1
  # Fingerprint Scanner
2
- [![Build Status](https://travis-ci.org/antoinevastel/fpscanner.svg?branch=master)](https://travis-ci.org/antoinevastel/fpscanner)
3
2
 
4
- Library to detect bots and crawlers using browser fingerprinting.
3
+ > **News:** After more than 7 years without any updates, I'm releasing a completely new version of FPScanner! This version includes both the fingerprinting code and detection logic in a single library. Consider this a beta release — feel free to use it in your projects. The overall API should remain stable, but expect some small changes as we refine the library based on feedback.
5
4
 
6
- ## Attributes collected
7
- Fingerprint Scanner relies on [Fp-Collect](https://github.com/antoinevastel/fp-collect) to collect a browser fingerprint.
8
- Since the purpose of the library is bot detection, it doesn't detect collect
9
- unnecessary fingerprint attributes used for tracking.
5
+ [![CI](https://github.com/antoinevastel/fpscanner/actions/workflows/ci.yml/badge.svg)](https://github.com/antoinevastel/fpscanner/actions/workflows/ci.yml)
10
6
 
11
- ## Usage
7
+ ## Sponsor
8
+
9
+ This project is sponsored by <a href="https://castle.io/?utm_source=github&utm_medium=oss&utm_campaign=fpscanner">Castle.</a>
10
+
11
+ <a href="https://castle.io/?utm_source=github&utm_medium=oss&utm_campaign=fpscanner"><img src="assets/castle-logo.png" alt="Castle" height="48" style="vertical-align: middle;"></a>
12
+
13
+ This library focuses on self-hosted fingerprinting and bot detection primitives. In real-world fraud and bot prevention, teams often need additional capabilities such as traffic observability, historical analysis, rule iteration, and correlation across device, network, and behavioral signals.
14
+
15
+ Castle provides a production-grade platform for bot and fraud detection, designed to operate at scale and handle these operational challenges end to end.
16
+
17
+ For a deeper explanation of what this library intentionally does not cover, see the **“Limits and non-goals”** section at the end of this README.
18
+
19
+
20
+ ## FPScanner: description
21
+
22
+ A lightweight browser fingerprinting library for bot detection.
23
+
24
+ Scraping has become mainstream. AI and LLM-driven companies now crawl the web at a scale that was previously limited to specialized actors, often without clearly respecting `robots.txt` or rate limits. At the same time, fraudsters do not need to rely solely on public frameworks like OpenBullet or generic automation stacks anymore. With LLMs, writing a custom bot tailored to a specific website has become significantly easier, faster, and cheaper.
25
+
26
+ The result is a much broader and more diverse bot ecosystem:
27
+ - More actors scraping content, training models, or extracting data
28
+ - More custom automation, harder to fingerprint with outdated heuristics
29
+ - More abuse at signup, login, and sensitive workflows, not just simple scraping
30
+
31
+ On the defender side, the situation is much more constrained.
32
+
33
+ You often have two options:
34
+ - Very basic open source libraries that focus on naive or outdated signals
35
+ - Expensive, black-box bot and fraud solutions that require routing traffic through third-party CDNs or vendors
36
+
37
+ Not every website can afford enterprise-grade bot management products. And even when cost is not the main issue, you may not want to route all your traffic through a CDN or outsource all detection logic to a third party.
38
+
39
+ This library exists to fill that gap.
40
+
41
+ It is a **self-hosted, lightweight, and up-to-date** browser fingerprinting and bot detection library, designed with real-world constraints in mind. The goal is not to promise perfect detection, but to give you solid building blocks that reflect how bots actually behave today.
42
+
43
+ This includes practical considerations that are often ignored in toy implementations:
44
+ - Anti-replay protections (timestamp + nonce)
45
+ - Payload encryption to prevent trivial forgery
46
+ - Optional obfuscation to raise the cost of reverse-engineering
47
+ - Focus on strong, low-noise signals rather than brittle tricks
48
+
49
+ The design and trade-offs behind this library are directly inspired by real production experience and by the ideas discussed in these articles:
50
+ - [Roll your own bot detection: fingerprinting (JavaScript)](https://blog.castle.io/roll-your-own-bot-detection-fingerprinting-javascript-part-1/)
51
+ - [Roll your own bot detection: server-side detection](https://blog.castle.io/roll-your-own-bot-detection-server-side-detection-part-2/)
52
+
53
+ Those articles are not documentation for this library, but they reflect the same philosophy: understand what attackers actually do, accept that no single signal is perfect, and build simple, composable primitives that you fully control.
54
+
55
+ ### Open Source, Production-Ready
56
+
57
+ This library is open source, but it is not naive about the implications of being open.
58
+
59
+ In bot detection, openness cuts both ways. Publishing detection logic makes it easier for attackers to study how they are detected. At the same time, defenders routinely study open and closed automation frameworks, anti-detect browsers, and bot tooling to discover new signals and weaknesses. This asymmetry already exists in the ecosystem, regardless of whether this library is open source or not.
60
+
61
+ The goal here is not to rely on obscurity. It is to acknowledge that attackers will read the code and still make abuse operationally expensive.
62
+
63
+ This is why the library combines transparency with pragmatic hardening:
64
+ - **Anti-replay mechanisms** ensure that a valid fingerprint cannot simply be captured once and reused at scale.
65
+ - **Build-time key injection** means attackers cannot trivially generate valid encrypted payloads without access to your specific build.
66
+ - **Optional obfuscation** raises the cost of reverse-engineering and makes automated payload forgery harder without executing the code in a real browser.
67
+
68
+ These controls are not meant to be perfect or unbreakable. Their purpose is to remove the easy shortcuts. An attacker should not be able to look at the repository, reimplement a serializer, and start sending convincing fingerprints from a headless script.
69
+
70
+ More importantly, detection does not stop at a single boolean flag.
71
+
72
+ Even if an attacker focuses on bypassing individual bot detection checks, producing **fully consistent fingerprints** over time is significantly harder. Fingerprints encode relationships between signals, contexts, and environments. Maintaining that consistency across sessions, IPs, and accounts requires real execution, careful state management, and stable tooling.
73
+
74
+ In practice, this creates leverage on the server side:
75
+ - Fingerprints can be tracked over time
76
+ - Reuse patterns and drift become visible
77
+ - Inconsistencies surface when attackers partially emulate environments or rotate tooling incorrectly
78
+
79
+ This is how fingerprinting is used in production systems: not as a one-shot verdict, but as a way to observe structure, reuse, and anomalies at scale.
80
+
81
+ Open source does not weaken this approach. It makes the trade-offs explicit. Attackers are assumed to be capable and adaptive, not careless. The library is designed accordingly: to force real execution, limit replay, and preserve enough structure in the signals that automation leaves traces once you observe it over time.
82
+
83
+
84
+ ## Features
85
+
86
+ | Feature | Description |
87
+ |---------|-------------|
88
+ | **Fast bot detection** | Client-side detection of strong automation signals such as `navigator.webdriver`, CDP usage, Playwright markers, and other common automation artifacts |
89
+ | **Browser fingerprinting** | Short-lived fingerprint designed for attack detection, clustering, and session correlation rather than long-term device tracking |
90
+ | **Encrypted payloads** | Optional payload encryption to prevent trivial forgery, with the encryption key injected at build time |
91
+ | **Obfuscation** | Optional code obfuscation to increase the cost of reverse-engineering and make it harder to forge valid fingerprints without actually executing the collection code |
92
+ | **Cross-context validation** | Detects inconsistencies across different JavaScript execution contexts (main page, iframes, and web workers) |
93
+
94
+
95
+ ---
96
+
97
+ ## Quick Start
12
98
 
13
99
  ### Installation
14
100
 
15
- ```
101
+ ```bash
16
102
  npm install fpscanner
103
+ ```
104
+
105
+ > **Note**: Out of the box, fpscanner uses a default placeholder encryption key and no obfuscation. This is fine for development and testing, but for production deployments you should build with your own key and enable obfuscation. See [Advanced: Custom Builds](#advanced-custom-builds) for details.
106
+
107
+ ### Basic Usage
17
108
 
109
+ ```javascript
110
+ import FingerprintScanner from 'fpscanner';
111
+
112
+ const scanner = new FingerprintScanner();
113
+ const payload = await scanner.collectFingerprint();
114
+
115
+ // Send payload to your server
116
+ fetch('/api/fingerprint', {
117
+ method: 'POST',
118
+ body: JSON.stringify({ fingerprint: payload }),
119
+ headers: { 'Content-Type': 'application/json' }
120
+ });
18
121
  ```
19
122
 
20
- ### Detect bots
123
+ ### Server-Side (Node.js)
124
+
125
+ ```javascript
126
+ // Decrypt and validate the fingerprint
127
+ // Use the same key you provided when building: npx fpscanner build --key=your-key
128
+ const key = 'your-secret-key'; // Your custom key
21
129
 
22
- In order to use Fingerprint-Scanner your need to pass a fingerprint
23
- collected using the fp-collect library.
24
- Then, we can analyze the fingerprint with the scanner.
130
+ function decryptFingerprint(ciphertext, key) {
131
+ const encrypted = Buffer.from(ciphertext, 'base64');
132
+ const keyBytes = Buffer.from(key, 'utf8');
133
+ const decrypted = Buffer.alloc(encrypted.length);
25
134
 
26
- ```js
27
- const scanner = require('fpScanner');
28
- //fingerprint is the fingerprint collected with fp-collect
29
- scannerResults = scanner.analyseFingerprint(fingerprint);
135
+ for (let i = 0; i < encrypted.length; i++) {
136
+ decrypted[i] = encrypted[i] ^ keyBytes[i % keyBytes.length];
137
+ }
30
138
 
31
- // Name of the first test
32
- console.log(scannerResults[0].name);
33
- // PHANTOM_UA
139
+ return JSON.parse(decrypted.toString('utf8'));
140
+ }
34
141
 
35
- // Result of the test
36
- console.log(scannerResults[0].consistent);
37
- // Either 1 (Inconsistent), 2 (Unsure) or 3 (Consistent)
142
+ app.post('/api/fingerprint', (req, res) => {
143
+ const fingerprint = decryptFingerprint(req.body.fingerprint, key);
38
144
 
39
- // Data related with the test
40
- console.log(scannerResults[0].data);
41
- // User agent of the browser
145
+ // Check bot detection
146
+ if (fingerprint.fastBotDetection) {
147
+ console.log('🤖 Bot detected!', fingerprint.fastBotDetectionDetails);
148
+ return res.status(403).json({ error: 'Bot detected' });
149
+ }
150
+
151
+ // Validate timestamp (prevent replay attacks)
152
+ const ageMs = Date.now() - fingerprint.time;
153
+ if (ageMs > 60000) { // 60 seconds
154
+ return res.status(400).json({ error: 'Fingerprint expired' });
155
+ }
156
+
157
+ // Use fingerprint.fsid for session correlation
158
+ console.log('Fingerprint ID:', fingerprint.fsid);
159
+ res.json({ ok: true });
160
+ });
161
+ ```
162
+
163
+ That's it! For most use cases, this is all you need.
164
+
165
+ ---
166
+
167
+ ## API Reference
168
+
169
+ ### `collectFingerprint(options?)`
170
+
171
+ Collects browser signals and returns a fingerprint.
172
+
173
+ ```javascript
174
+ const scanner = new FingerprintScanner();
175
+
176
+ // Default: returns encrypted base64 string
177
+ const encrypted = await scanner.collectFingerprint();
178
+
179
+ // Explicit encryption
180
+ const encrypted = await scanner.collectFingerprint({ encrypt: true });
181
+
182
+ // Raw object (no library encryption)
183
+ const fingerprint = await scanner.collectFingerprint({ encrypt: false });
184
+ ```
185
+
186
+ | Option | Type | Default | Description |
187
+ |--------|------|---------|-------------|
188
+ | `encrypt` | `boolean` | `true` | Whether to encrypt the payload |
189
+ | `skipWorker` | `boolean` | `false` | Skip Web Worker signals (use if CSP blocks blob: URLs) |
190
+
191
+ ### Fingerprint Object
192
+
193
+ When decrypted (or with `encrypt: false`), the fingerprint contains:
194
+
195
+ ```typescript
196
+ interface Fingerprint {
197
+ // Bot detection
198
+ fastBotDetection: boolean; // true if any bot signal detected
199
+ fastBotDetectionDetails: {
200
+ hasWebdriver: boolean; // navigator.webdriver === true
201
+ hasWebdriverWritable: boolean; // webdriver property is writable
202
+ hasSeleniumProperty: boolean; // Selenium-specific properties
203
+ hasCDP: boolean; // Chrome DevTools Protocol signals
204
+ hasPlaywright: boolean; // Playwright-specific signals
205
+ hasWebdriverIframe: boolean; // webdriver in iframe context
206
+ hasWebdriverWorker: boolean; // webdriver in worker context
207
+ // ... more detection flags
208
+ };
209
+
210
+ // Fingerprint
211
+ fsid: string; // JA4-inspired fingerprint ID
212
+ signals: { /* raw signal data */ };
213
+
214
+ // Anti-replay
215
+ time: number; // Unix timestamp (ms)
216
+ nonce: string; // Random value for replay detection
217
+ }
218
+ ```
219
+
220
+ ---
221
+
222
+ ## What It Detects
223
+
224
+ The library focuses on **strong, reliable signals** from major automation frameworks:
225
+
226
+ | Detection | Signal | Frameworks |
227
+ |-----------|--------|------------|
228
+ | `hasWebdriver` | `navigator.webdriver === true` | Selenium, Puppeteer, Playwright |
229
+ | `hasWebdriverWritable` | webdriver property descriptor | Puppeteer, Playwright |
230
+ | `hasSeleniumProperty` | `document.$cdc_`, `$wdc_` | Selenium WebDriver |
231
+ | `hasCDP` | CDP runtime markers | Chrome DevTools Protocol |
232
+ | `hasPlaywright` | `__playwright`, `__pw_*` | Playwright |
233
+ | `hasMissingChromeObject` | Missing `window.chrome` | Headless Chrome |
234
+ | `headlessChromeScreenResolution` | 800x600 default | Headless browsers |
235
+ | `hasHighCPUCount` | Unrealistic core count | VM/container environments |
236
+ | `hasImpossibleDeviceMemory` | Unrealistic memory values | Spoofed environments |
237
+
238
+ ### Cross-Context Validation
239
+
240
+ Bots often fail to maintain consistency across execution contexts:
241
+
242
+ | Detection | Description |
243
+ |-----------|-------------|
244
+ | `hasWebdriverIframe` | webdriver detected in iframe but not main |
245
+ | `hasWebdriverWorker` | webdriver detected in web worker |
246
+ | `hasMismatchPlatformIframe` | Platform differs between main and iframe |
247
+ | `hasMismatchPlatformWorker` | Platform differs between main and worker |
248
+ | `hasMismatchWebGLInWorker` | WebGL renderer differs in worker |
249
+
250
+ ---
251
+
252
+ ## Fingerprint ID (fsid) Format
253
+
254
+ The `fsid` is a JA4-inspired, locality-preserving fingerprint identifier. Unlike a simple hash, it's structured into semantic sections, making it both human-readable and useful for partial matching.
255
+
256
+ ### Format
257
+
258
+ ```
259
+ FS1_<det>_<auto>_<dev>_<brw>_<gfx>_<cod>_<loc>_<ctx>
260
+ ```
261
+
262
+ ### Example
263
+
264
+ ```
265
+ FS1_00000100000000_10010h3f2a_1728x1117c14m08b01011h4e7a9f_f1101011001e00000000p1100h2c8b1e_0h9d3f7a_1h6a2e4c_en4tEurope-Paris_hab12_0000h3e9f
266
+ ```
267
+
268
+ ### Section Breakdown
269
+
270
+ | # | Section | Format | Example | Description |
271
+ |---|---------|--------|---------|-------------|
272
+ | 1 | **Version** | `FS1` | `FS1` | Fingerprint Scanner version 1 |
273
+ | 2 | **Detection** | 14-bit bitmask | `00000100000000` | All fastBotDetectionDetails booleans |
274
+ | 3 | **Automation** | `<5-bit>h<hash>` | `10010h3f2a` | Automation booleans + hash |
275
+ | 4 | **Device** | `<W>x<H>c<cpu>m<mem>b<5-bit>h<hash>` | `1728x1117c14m08b01011h4e7a9f` | Screen, cpu, memory, device booleans + hash |
276
+ | 5 | **Browser** | `f<10-bit>e<8-bit>p<4-bit>h<hash>` | `f1101011001e00000000p1100h2c8b1e` | Features + extensions + plugins bitmasks + hash |
277
+ | 6 | **Graphics** | `<1-bit>h<hash>` | `0h9d3f7a` | hasModifiedCanvas + hash |
278
+ | 7 | **Codecs** | `<1-bit>h<hash>` | `1h6a2e4c` | hasMediaSource + hash |
279
+ | 8 | **Locale** | `<lang><n>t<tz>_h<hash>` | `en4tEurope-Paris_hab12` | Language code + count + timezone + hash |
280
+ | 9 | **Contexts** | `<4-bit>h<hash>` | `0000h3e9f` | Mismatch + webdriver flags + hash |
281
+
282
+ ### Why This Format?
283
+
284
+ Inspired by [JA4+](https://github.com/FoxIO-LLC/ja4), this format enables:
285
+
286
+ 1. **Partial Matching** — Compare specific sections across fingerprints (same GPU but different screen?)
287
+ 2. **Human Readability** — `1728x1117c14m08` = 1728×1117 screen, 14 cores, 8GB RAM
288
+ 3. **Extensibility** — Adding a new boolean check appends a bit without breaking existing positions
289
+ 4. **Similarity Detection** — Bots from the same framework often share automation/browser hashes
290
+
291
+ <details>
292
+ <summary><strong>Bitmask Reference</strong></summary>
293
+
294
+ #### Detection Bitmask (14 bits)
295
+
296
+ ```
297
+ Bit 0: headlessChromeScreenResolution
298
+ Bit 1: hasWebdriver
299
+ Bit 2: hasWebdriverWritable
300
+ Bit 3: hasSeleniumProperty
301
+ Bit 4: hasCDP
302
+ Bit 5: hasPlaywright
303
+ Bit 6: hasImpossibleDeviceMemory
304
+ Bit 7: hasHighCPUCount
305
+ Bit 8: hasMissingChromeObject
306
+ Bit 9: hasWebdriverIframe
307
+ Bit 10: hasWebdriverWorker
308
+ Bit 11: hasMismatchWebGLInWorker
309
+ Bit 12: hasMismatchPlatformIframe
310
+ Bit 13: hasMismatchPlatformWorker
311
+ ```
312
+
313
+ #### Automation Bitmask (5 bits)
314
+
315
+ ```
316
+ Bit 0: webdriver
317
+ Bit 1: webdriverWritable
318
+ Bit 2: selenium
319
+ Bit 3: cdp
320
+ Bit 4: playwright
321
+ ```
322
+
323
+ #### Device Bitmask (5 bits)
324
+
325
+ ```
326
+ Bit 0: hasMultipleDisplays
327
+ Bit 1: prefersReducedMotion
328
+ Bit 2: prefersReducedTransparency
329
+ Bit 3: hover
330
+ Bit 4: anyHover
331
+ ```
332
+
333
+ #### Browser Features Bitmask (10 bits)
334
+
335
+ ```
336
+ Bit 0: chrome
337
+ Bit 1: brave
338
+ Bit 2: applePaySupport
339
+ Bit 3: opera
340
+ Bit 4: serial
341
+ Bit 5: attachShadow
342
+ Bit 6: caches
343
+ Bit 7: webAssembly
344
+ Bit 8: buffer
345
+ Bit 9: showModalDialog
346
+ ```
347
+
348
+ #### Browser Extensions Bitmask (8 bits)
349
+
350
+ ```
351
+ Bit 0: grammarly
352
+ Bit 1: metamask
353
+ Bit 2: couponBirds
354
+ Bit 3: deepL
355
+ Bit 4: monicaAI
356
+ Bit 5: siderAI
357
+ Bit 6: requestly
358
+ Bit 7: veepn
359
+ ```
360
+
361
+ #### Plugins Bitmask (4 bits)
362
+
363
+ ```
364
+ Bit 0: isValidPluginArray
365
+ Bit 1: pluginConsistency1
366
+ Bit 2: pluginOverflow
367
+ Bit 3: hasToSource
42
368
  ```
43
369
 
44
- **analyseFingerprint** returns an array of analysisResult's objects.
45
- Each object contains the following information:
46
- - *name*: the name of the test;
47
- - *consistent*: the result of the test (CONSISTENT, UNSURE, INCONSISTENT)
48
- - *data*: data related to the test
370
+ #### Contexts Bitmask (4 bits)
371
+
372
+ ```
373
+ Bit 0: iframe mismatch
374
+ Bit 1: worker mismatch
375
+ Bit 2: iframe.webdriver
376
+ Bit 3: webWorker.webdriver
377
+ ```
378
+
379
+ </details>
380
+
381
+ ---
382
+
383
+ ## Server-Side Decryption
384
+
385
+ The library uses a simple XOR cipher with Base64 encoding. This is easy to implement in any language.
386
+
387
+ ### Node.js
388
+
389
+ ```javascript
390
+ function decryptFingerprint(ciphertext, key) {
391
+ const encrypted = Buffer.from(ciphertext, 'base64');
392
+ const keyBytes = Buffer.from(key, 'utf8');
393
+ const decrypted = Buffer.alloc(encrypted.length);
394
+
395
+ for (let i = 0; i < encrypted.length; i++) {
396
+ decrypted[i] = encrypted[i] ^ keyBytes[i % keyBytes.length];
397
+ }
398
+
399
+ let fingerprint = JSON.parse(decrypted.toString('utf8'));
400
+ // Handle double-JSON-encoding if present
401
+ if (typeof fingerprint === 'string') {
402
+ fingerprint = JSON.parse(fingerprint);
403
+ }
404
+ return fingerprint;
405
+ }
406
+ ```
407
+
408
+ ### Python
409
+
410
+ ```python
411
+ import base64
412
+ import json
413
+
414
+ def decrypt_fingerprint(ciphertext: str, key: str) -> dict:
415
+ encrypted = base64.b64decode(ciphertext)
416
+ key_bytes = key.encode('utf-8')
417
+
418
+ decrypted = bytearray(len(encrypted))
419
+ for i in range(len(encrypted)):
420
+ decrypted[i] = encrypted[i] ^ key_bytes[i % len(key_bytes)]
421
+
422
+ fingerprint = json.loads(decrypted.decode('utf-8'))
423
+ # Handle double-JSON-encoding if present
424
+ if isinstance(fingerprint, str):
425
+ fingerprint = json.loads(fingerprint)
426
+ return fingerprint
427
+ ```
428
+
429
+ ### Other Languages
430
+
431
+ The algorithm is straightforward to port:
432
+
433
+ 1. **Base64 decode** the ciphertext to get raw bytes
434
+ 2. **XOR each byte** with the corresponding key byte (cycling through the key)
435
+ 3. **Decode** the result as UTF-8 to get the JSON string
436
+ 4. **Parse** the JSON to get the fingerprint object
437
+
438
+ See the [`examples/`](./examples/) folder for complete Node.js and Python server examples.
439
+
440
+ ---
441
+
442
+ ## Advanced: Custom Builds
443
+
444
+ By default, fpscanner uses a placeholder key that gets replaced when you run the build command. For production, you should use your own encryption key and enable obfuscation to make it harder for attackers to forge payloads.
445
+
446
+ ### Bring Your Own Encryption/Obfuscation
447
+
448
+ The library provides built-in encryption and obfuscation, but **you're not required to use them**. If you prefer:
449
+
450
+ - Use `collectFingerprint({ encrypt: false })` to get the raw fingerprint object
451
+ - Apply your own encryption, signing, or encoding before sending to your server
452
+ - Run your own obfuscation tool (Terser, JavaScript Obfuscator, etc.) on your bundle
453
+
454
+ The recommended approach is to use **some form of** encryption + obfuscation — whether that's the library's built-in solution or your own. The key is to prevent attackers from easily forging payloads without executing the actual collection code.
455
+
456
+ ### Why Custom Builds?
457
+
458
+ | Threat | Without Protection | With Encryption + Obfuscation |
459
+ |--------|---------------------|-------------------|
460
+ | Payload forgery | Attacker can craft fake fingerprints | Key is hidden in obfuscated code |
461
+ | Replay attacks | Attacker captures and replays fingerprints | Server validates timestamp + nonce |
462
+ | Code inspection | Detection logic is readable | Control flow obfuscation makes analysis harder |
463
+
464
+ > **Note**: Obfuscation is not encryption. A determined attacker can still reverse-engineer the code. The goal is to raise the bar and force attackers to invest significant effort, not to create an impenetrable system.
465
+
466
+ ### Build with Your Key
467
+
468
+ ```bash
469
+ npx fpscanner build --key=your-secret-key-here
470
+ ```
471
+
472
+ This will:
473
+ 1. Rebuild the library with your key baked in
474
+ 2. Obfuscate the output to protect the key
475
+ 3. Overwrite the files in `node_modules/fpscanner/dist/`
476
+
477
+ ### Key Injection Methods
478
+
479
+ The CLI supports multiple methods (in order of priority):
480
+
481
+ ```bash
482
+ # 1. Command line argument (highest priority)
483
+ npx fpscanner build --key=your-secret-key
484
+
485
+ # 2. Environment variable
486
+ export FINGERPRINT_KEY=your-secret-key
487
+ npx fpscanner build
488
+
489
+ # 3. .env file
490
+ echo "FINGERPRINT_KEY=your-secret-key" >> .env
491
+ npx fpscanner build
492
+
493
+ # 4. Custom env file
494
+ npx fpscanner build --env-file=.env.production
495
+ ```
496
+
497
+ ### CI/CD Integration
498
+
499
+ Add a `postinstall` script to automatically build with your key:
500
+
501
+ ```json
502
+ {
503
+ "scripts": {
504
+ "postinstall": "fpscanner build"
505
+ }
506
+ }
507
+ ```
508
+
509
+ Then set `FINGERPRINT_KEY` as a secret in your CI/CD:
510
+
511
+ **GitHub Actions:**
512
+
513
+ ```yaml
514
+ env:
515
+ FINGERPRINT_KEY: ${{ secrets.FINGERPRINT_KEY }}
516
+
517
+ steps:
518
+ - run: npm install # postinstall runs fpscanner build automatically
519
+ ```
520
+
521
+ ### Build Options
522
+
523
+ | Option | Description |
524
+ |--------|-------------|
525
+ | `--key=KEY` | Encryption key (highest priority) |
526
+ | `--env-file=FILE` | Load key from custom env file |
527
+ | `--no-obfuscate` | Skip obfuscation (faster, for development) |
528
+
529
+ #### Skip Obfuscation
530
+
531
+ Obfuscation is enabled by default. For faster builds during development:
532
+
533
+ ```bash
534
+ # Via CLI flag
535
+ npx fpscanner build --key=dev-key --no-obfuscate
536
+
537
+ # Via environment variable
538
+ FINGERPRINT_OBFUSCATE=false npx fpscanner build
539
+
540
+ # In .env file
541
+ FINGERPRINT_OBFUSCATE=false
542
+ ```
543
+
544
+ > ⚠️ **Warning**: Without obfuscation, the encryption key is visible in plain text in the source code. This means attackers can easily extract the key and forge fingerprint payloads without running the actual collection code. If you skip the library's obfuscation, make sure you apply your own obfuscation to the final bundle.
545
+
546
+ ---
547
+
548
+ ## Development
549
+
550
+ ### Local Development Scripts
551
+
552
+ ```bash
553
+ # Quick build (default key, no obfuscation)
554
+ npm run build
555
+
556
+ # Build with dev-key, no obfuscation
557
+ npm run build:dev
558
+
559
+ # Build + serve test/dev-source.html at localhost:3000
560
+ npm run dev
561
+
562
+ # Build with obfuscation
563
+ npm run build:obfuscate
564
+
565
+ # Production build (key from .env, with obfuscation)
566
+ npm run build:prod
567
+
568
+ # Watch mode (rebuilds on changes)
569
+ npm run watch
570
+ ```
571
+
572
+ ### Testing
573
+
574
+ ```bash
575
+ npm test
576
+ ```
577
+
578
+ ---
579
+
580
+ ## Security Best Practices
581
+
582
+ 1. **Use a strong, random key and rotate it regularly**
583
+ Use a high-entropy key (at least 32 random characters) and rotate it periodically. Because the encryption key is shipped client-side in the JavaScript bundle, long-lived keys give attackers more time to extract and reuse them. Rotating the key forces attackers to re-analyze and re-adapt, and requires rebuilding and redeploying the fingerprinting script.
584
+
585
+ 2. **Use obfuscation in production**
586
+ Enable the library’s built-in obfuscation or apply your own obfuscation step to the final bundle. Without obfuscation, the encryption key is visible in plain text in the client-side code, making it trivial to forge payloads without executing the fingerprinting logic. Obfuscation raises the cost of key extraction and payload forgery.
587
+
588
+ 3. **Validate timestamps server-side**
589
+ Reject fingerprints that are older than a reasonable threshold (for example, 60 seconds). This limits the usefulness of captured payloads and reduces the impact of replay attacks.
590
+
591
+ 4. **Track nonces**
592
+ Optionally store recently seen nonces and reject duplicates. This provides an additional layer of replay protection, especially for high-value or abuse-prone endpoints.
593
+
594
+ 5. **Monitor fingerprint distributions over time**
595
+ Do not treat fingerprinting as a one-shot decision. Monitor how fingerprints evolve and distribute over time. Sudden spikes, new dominant fingerprints, or unusual reuse patterns can indicate automated or malicious activity, even if individual requests do not trigger explicit bot detection flags.
596
+
597
+ 6. **Defense in depth on sensitive endpoints**
598
+ When protecting sensitive flows (signup, login, password reset, API access), combine this library with other controls such as fingerprint-based rate limiting, behavioral analysis, disposable emails detection and challenge mechanisms like CAPTCHAs or risk-based authentication. Fingerprinting works best as one layer in a broader detection and mitigation strategy.
599
+
600
+
601
+ ---
602
+
603
+ ## Troubleshooting
604
+
605
+ ### "No encryption key found!"
606
+
607
+ Provide a key via one of the supported methods:
608
+
609
+ ```bash
610
+ npx fpscanner build --key=your-key
611
+ # or
612
+ export FINGERPRINT_KEY=your-key && npx fpscanner build
613
+ # or
614
+ echo "FINGERPRINT_KEY=your-key" >> .env && npx fpscanner build
615
+ ```
616
+
617
+ ### Decryption returns garbage
618
+
619
+ Make sure you're using the **exact same key** on your server that you used when building. Keys must match exactly.
620
+
621
+ ### Obfuscation is slow
622
+
623
+ Use `--no-obfuscate` during development. Only enable obfuscation for production builds.
624
+
625
+ ### `postinstall` fails in CI
626
+
627
+ Ensure `FINGERPRINT_KEY` is set as an environment variable before `npm install` runs.
628
+
629
+ ---
630
+
631
+ ## Limits and non-goals
632
+
633
+ This library provides building blocks, not a complete bot or fraud detection system. It is important to understand its limits before using it in production.
634
+
635
+ ### Open source and attacker adaptation
636
+
637
+ The library is open source, which means attackers can inspect the code and adapt their tooling. This is expected and reflects how the ecosystem already works. Defenders routinely analyze automation frameworks and anti-detect browsers, and attackers do the same with detection logic.
638
+
639
+ The goal is not secrecy, but to make abuse operationally expensive by forcing real execution, limiting replay, and preserving consistency constraints that are difficult to fake at scale.
640
+
641
+ ### Obfuscation is not a silver bullet
642
+
643
+ The optional obfuscation relies on an open source obfuscator, and some attackers maintain deobfuscation tooling for it. Obfuscation is a friction mechanism, not a guarantee. It slows down analysis and discourages low-effort abuse, but motivated attackers can still reverse-engineer the code.
644
+
645
+ ### Limits of client-side detection
646
+
647
+ All client-side fingerprinting and bot detection techniques can be spoofed or emulated. This library focuses on strong, low-noise signals, but no individual signal or fingerprint should be treated as definitive.
648
+
649
+ Fingerprints are representations, not verdicts. Their value comes from observing how they behave over time, how often they appear, and how they correlate with actions, IPs, and accounts.
650
+
651
+ ### Not an end-to-end solution
652
+
653
+ Real-world bot and fraud detection requires server-side context, observability, and iteration: the ability to monitor traffic, build and test rules, and adapt over time. This library intentionally does not provide dashboards, rule engines, or managed mitigation.
49
654
 
50
- ## Detection tests
655
+ If you need a production-grade, end-to-end solution with observability and ongoing maintenance, consider using a dedicated platform like [Castle](https://castle.io/).
51
656
 
52
- Summary of the tests used to detect bots. For more details, visit
53
- the documentation page (coming soon).
54
657
 
55
- - **PHANTOM_UA:** Detect PhantomJS user agent
56
- - **PHANTOM_PROPERTIES:** Test the presence of properties introduced by PhantomJS
57
- - **PHANTOM_ETSL:** Runtime verification for PhantomJS
58
- - **PHANTOM_LANGUAGE:** Use *navigator.languages* to detect PhantomJS
59
- - **PHANTOM_WEBSOCKET:** Analyze the error thrown when creating a websocket
60
- - **MQ_SCREEN:** Use media query related to the screen
61
- - **PHANTOM_OVERFLOW:** Analyze error thrown when a stack overflow occurs
62
- - **PHANTOM_WINDOW_HEIGHT:** Analyze window screen dimension
63
- - **HEADCHR_UA:** Detect Chrome Headless user agent
64
- - **WEBDRIVER:** Test the presence of *webriver* attributes
65
- - **HEADCHR_CHROME_OBJ:** Test the presence of the *window.chrome* object
66
- - **HEADCHR_PERMISSIONS:** Test permissions management
67
- - **HEADCHR_PLUGINS:** Verify the number of plugins
68
- - **HEADCHR_IFRAME:** Test presence of Chrome Headless using an iframe
69
- - **CHR_DEBUG_TOOLS:** Test if debug tools are opened
70
- - **SELENIUM_DRIVER:** Test the presence of Selenium drivers
71
- - **CHR_BATTERY:** Test the presence of *battery*
72
- - **CHR_MEMORY:** Verify if *navigator.deviceMemory* is consistent
73
- - **TRANSPARENT_PIXEL:** Verify if a canvas pixel is transparent
658
+ ---
74
659
 
75
- # Acknowledgements
76
- We would like to thank [CrossBrowserTesting](https://crossbrowsertesting.com) for providing us an easy way to test our scanner on different platforms to reduce false positives.
660
+ ## License
77
661
 
78
- ![Logo of CrossBrowserTesting](https://seeklogo.com/images/C/cross-browser-testing-logo-300E2AF44B-seeklogo.com.png)
662
+ MIT