fpscanner 0.1.5 → 0.9.1

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 +636 -56
  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 +54 -7
  110. package/scripts/build-custom.js +204 -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 -223
  167. package/test/test.html +0 -11
  168. package/test/test.js +0 -116
package/README.md CHANGED
@@ -1,81 +1,661 @@
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
- You can see it in action on [this page.](https://antoinevastel.com/bots/)
5
+ [![CI](https://github.com/antoinevastel/fpscanner/actions/workflows/ci.yml/badge.svg)](https://github.com/antoinevastel/fpscanner/actions/workflows/ci.yml)
7
6
 
7
+ ## Sponsor
8
8
 
9
- ## Attributes collected
10
- Fingerprint Scanner relies on [Fp-Collect](https://github.com/antoinevastel/fp-collect) to collect a browser fingerprint.
11
- Since the purpose of the library is bot detection, it doesn't detect collect
12
- unnecessary fingerprint attributes used for tracking.
9
+ This project is sponsored by <a href="https://castle.io/?utm_source=github&utm_medium=oss&utm_campaign=fpscanner">Castle.</a>
13
10
 
14
- ## Usage
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
15
98
 
16
99
  ### Installation
17
100
 
18
- ```
101
+ ```bash
19
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
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
+ });
121
+ ```
122
+
123
+ ### Server-Side (Node.js)
124
+
125
+ ```javascript
126
+ // Decrypt and validate the fingerprint
127
+ const key = 'my-shared-secret'; // Default key, or your custom key
128
+
129
+ function decryptFingerprint(ciphertext, key) {
130
+ const encrypted = Buffer.from(ciphertext, 'base64');
131
+ const keyBytes = Buffer.from(key, 'utf8');
132
+ const decrypted = Buffer.alloc(encrypted.length);
133
+
134
+ for (let i = 0; i < encrypted.length; i++) {
135
+ decrypted[i] = encrypted[i] ^ keyBytes[i % keyBytes.length];
136
+ }
137
+
138
+ return JSON.parse(decrypted.toString('utf8'));
139
+ }
140
+
141
+ app.post('/api/fingerprint', (req, res) => {
142
+ const fingerprint = decryptFingerprint(req.body.fingerprint, key);
143
+
144
+ // Check bot detection
145
+ if (fingerprint.fastBotDetection) {
146
+ console.log('🤖 Bot detected!', fingerprint.fastBotDetectionDetails);
147
+ return res.status(403).json({ error: 'Bot detected' });
148
+ }
149
+
150
+ // Validate timestamp (prevent replay attacks)
151
+ const ageMs = Date.now() - fingerprint.time;
152
+ if (ageMs > 60000) { // 60 seconds
153
+ return res.status(400).json({ error: 'Fingerprint expired' });
154
+ }
155
+
156
+ // Use fingerprint.fsid for session correlation
157
+ console.log('Fingerprint ID:', fingerprint.fsid);
158
+ res.json({ ok: true });
159
+ });
160
+ ```
161
+
162
+ That's it! For most use cases, this is all you need.
163
+
164
+ ---
165
+
166
+ ## API Reference
167
+
168
+ ### `collectFingerprint(options?)`
169
+
170
+ Collects browser signals and returns a fingerprint.
171
+
172
+ ```javascript
173
+ const scanner = new FingerprintScanner();
174
+
175
+ // Default: returns encrypted base64 string
176
+ const encrypted = await scanner.collectFingerprint();
177
+
178
+ // Explicit encryption
179
+ const encrypted = await scanner.collectFingerprint({ encrypt: true });
180
+
181
+ // Raw object (no library encryption)
182
+ const fingerprint = await scanner.collectFingerprint({ encrypt: false });
183
+ ```
184
+
185
+ | Option | Type | Default | Description |
186
+ |--------|------|---------|-------------|
187
+ | `encrypt` | `boolean` | `true` | Whether to encrypt the payload |
188
+ | `skipWorker` | `boolean` | `false` | Skip Web Worker signals (use if CSP blocks blob: URLs) |
189
+
190
+ ### Fingerprint Object
191
+
192
+ When decrypted (or with `encrypt: false`), the fingerprint contains:
193
+
194
+ ```typescript
195
+ interface Fingerprint {
196
+ // Bot detection
197
+ fastBotDetection: boolean; // true if any bot signal detected
198
+ fastBotDetectionDetails: {
199
+ hasWebdriver: boolean; // navigator.webdriver === true
200
+ hasWebdriverWritable: boolean; // webdriver property is writable
201
+ hasSeleniumProperty: boolean; // Selenium-specific properties
202
+ hasCDP: boolean; // Chrome DevTools Protocol signals
203
+ hasPlaywright: boolean; // Playwright-specific signals
204
+ hasWebdriverIframe: boolean; // webdriver in iframe context
205
+ hasWebdriverWorker: boolean; // webdriver in worker context
206
+ // ... more detection flags
207
+ };
208
+
209
+ // Fingerprint
210
+ fsid: string; // JA4-inspired fingerprint ID
211
+ signals: { /* raw signal data */ };
212
+
213
+ // Anti-replay
214
+ time: number; // Unix timestamp (ms)
215
+ nonce: string; // Random value for replay detection
216
+ }
217
+ ```
218
+
219
+ ---
220
+
221
+ ## What It Detects
222
+
223
+ The library focuses on **strong, reliable signals** from major automation frameworks:
224
+
225
+ | Detection | Signal | Frameworks |
226
+ |-----------|--------|------------|
227
+ | `hasWebdriver` | `navigator.webdriver === true` | Selenium, Puppeteer, Playwright |
228
+ | `hasWebdriverWritable` | webdriver property descriptor | Puppeteer, Playwright |
229
+ | `hasSeleniumProperty` | `document.$cdc_`, `$wdc_` | Selenium WebDriver |
230
+ | `hasCDP` | CDP runtime markers | Chrome DevTools Protocol |
231
+ | `hasPlaywright` | `__playwright`, `__pw_*` | Playwright |
232
+ | `hasMissingChromeObject` | Missing `window.chrome` | Headless Chrome |
233
+ | `headlessChromeScreenResolution` | 800x600 default | Headless browsers |
234
+ | `hasHighCPUCount` | Unrealistic core count | VM/container environments |
235
+ | `hasImpossibleDeviceMemory` | Unrealistic memory values | Spoofed environments |
236
+
237
+ ### Cross-Context Validation
238
+
239
+ Bots often fail to maintain consistency across execution contexts:
240
+
241
+ | Detection | Description |
242
+ |-----------|-------------|
243
+ | `hasWebdriverIframe` | webdriver detected in iframe but not main |
244
+ | `hasWebdriverWorker` | webdriver detected in web worker |
245
+ | `hasMismatchPlatformIframe` | Platform differs between main and iframe |
246
+ | `hasMismatchPlatformWorker` | Platform differs between main and worker |
247
+ | `hasMismatchWebGLInWorker` | WebGL renderer differs in worker |
248
+
249
+ ---
250
+
251
+ ## Fingerprint ID (fsid) Format
252
+
253
+ 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.
254
+
255
+ ### Format
256
+
257
+ ```
258
+ FS1_<det>_<auto>_<dev>_<brw>_<gfx>_<cod>_<loc>_<ctx>
259
+ ```
260
+
261
+ ### Example
20
262
 
21
263
  ```
264
+ FS1_00000100000000_10010h3f2a_1728x1117c14m08b01011h4e7a9f_f1101011001e00000000p1100h2c8b1e_0h9d3f7a_1h6a2e4c_en4tEurope-Paris_hab12_0000h3e9f
265
+ ```
266
+
267
+ ### Section Breakdown
268
+
269
+ | # | Section | Format | Example | Description |
270
+ |---|---------|--------|---------|-------------|
271
+ | 1 | **Version** | `FS1` | `FS1` | Fingerprint Scanner version 1 |
272
+ | 2 | **Detection** | 14-bit bitmask | `00000100000000` | All fastBotDetectionDetails booleans |
273
+ | 3 | **Automation** | `<5-bit>h<hash>` | `10010h3f2a` | Automation booleans + hash |
274
+ | 4 | **Device** | `<W>x<H>c<cpu>m<mem>b<5-bit>h<hash>` | `1728x1117c14m08b01011h4e7a9f` | Screen, cpu, memory, device booleans + hash |
275
+ | 5 | **Browser** | `f<10-bit>e<8-bit>p<4-bit>h<hash>` | `f1101011001e00000000p1100h2c8b1e` | Features + extensions + plugins bitmasks + hash |
276
+ | 6 | **Graphics** | `<1-bit>h<hash>` | `0h9d3f7a` | hasModifiedCanvas + hash |
277
+ | 7 | **Codecs** | `<1-bit>h<hash>` | `1h6a2e4c` | hasMediaSource + hash |
278
+ | 8 | **Locale** | `<lang><n>t<tz>_h<hash>` | `en4tEurope-Paris_hab12` | Language code + count + timezone + hash |
279
+ | 9 | **Contexts** | `<4-bit>h<hash>` | `0000h3e9f` | Mismatch + webdriver flags + hash |
280
+
281
+ ### Why This Format?
282
+
283
+ Inspired by [JA4+](https://github.com/FoxIO-LLC/ja4), this format enables:
22
284
 
23
- ### Detect bots
285
+ 1. **Partial Matching** — Compare specific sections across fingerprints (same GPU but different screen?)
286
+ 2. **Human Readability** — `1728x1117c14m08` = 1728×1117 screen, 14 cores, 8GB RAM
287
+ 3. **Extensibility** — Adding a new boolean check appends a bit without breaking existing positions
288
+ 4. **Similarity Detection** — Bots from the same framework often share automation/browser hashes
24
289
 
25
- In order to use Fingerprint-Scanner your need to pass a fingerprint
26
- collected using the fp-collect library.
27
- Then, we can analyze the fingerprint with the scanner.
290
+ <details>
291
+ <summary><strong>Bitmask Reference</strong></summary>
28
292
 
29
- ```js
30
- const scanner = require('fpScanner');
31
- //fingerprint is the fingerprint collected with fp-collect
32
- scannerResults = scanner.analyseFingerprint(fingerprint);
293
+ #### Detection Bitmask (14 bits)
33
294
 
34
- // Name of the first test
35
- console.log(scannerResults[0].name);
36
- // PHANTOM_UA
295
+ ```
296
+ Bit 0: headlessChromeScreenResolution
297
+ Bit 1: hasWebdriver
298
+ Bit 2: hasWebdriverWritable
299
+ Bit 3: hasSeleniumProperty
300
+ Bit 4: hasCDP
301
+ Bit 5: hasPlaywright
302
+ Bit 6: hasImpossibleDeviceMemory
303
+ Bit 7: hasHighCPUCount
304
+ Bit 8: hasMissingChromeObject
305
+ Bit 9: hasWebdriverIframe
306
+ Bit 10: hasWebdriverWorker
307
+ Bit 11: hasMismatchWebGLInWorker
308
+ Bit 12: hasMismatchPlatformIframe
309
+ Bit 13: hasMismatchPlatformWorker
310
+ ```
37
311
 
38
- // Result of the test
39
- console.log(scannerResults[0].consistent);
40
- // Either 1 (Inconsistent), 2 (Unsure) or 3 (Consistent)
312
+ #### Automation Bitmask (5 bits)
41
313
 
42
- // Data related with the test
43
- console.log(scannerResults[0].data);
44
- // User agent of the browser
45
314
  ```
315
+ Bit 0: webdriver
316
+ Bit 1: webdriverWritable
317
+ Bit 2: selenium
318
+ Bit 3: cdp
319
+ Bit 4: playwright
320
+ ```
321
+
322
+ #### Device Bitmask (5 bits)
323
+
324
+ ```
325
+ Bit 0: hasMultipleDisplays
326
+ Bit 1: prefersReducedMotion
327
+ Bit 2: prefersReducedTransparency
328
+ Bit 3: hover
329
+ Bit 4: anyHover
330
+ ```
331
+
332
+ #### Browser Features Bitmask (10 bits)
333
+
334
+ ```
335
+ Bit 0: chrome
336
+ Bit 1: brave
337
+ Bit 2: applePaySupport
338
+ Bit 3: opera
339
+ Bit 4: serial
340
+ Bit 5: attachShadow
341
+ Bit 6: caches
342
+ Bit 7: webAssembly
343
+ Bit 8: buffer
344
+ Bit 9: showModalDialog
345
+ ```
346
+
347
+ #### Browser Extensions Bitmask (8 bits)
348
+
349
+ ```
350
+ Bit 0: grammarly
351
+ Bit 1: metamask
352
+ Bit 2: couponBirds
353
+ Bit 3: deepL
354
+ Bit 4: monicaAI
355
+ Bit 5: siderAI
356
+ Bit 6: requestly
357
+ Bit 7: veepn
358
+ ```
359
+
360
+ #### Plugins Bitmask (4 bits)
361
+
362
+ ```
363
+ Bit 0: isValidPluginArray
364
+ Bit 1: pluginConsistency1
365
+ Bit 2: pluginOverflow
366
+ Bit 3: hasToSource
367
+ ```
368
+
369
+ #### Contexts Bitmask (4 bits)
370
+
371
+ ```
372
+ Bit 0: iframe mismatch
373
+ Bit 1: worker mismatch
374
+ Bit 2: iframe.webdriver
375
+ Bit 3: webWorker.webdriver
376
+ ```
377
+
378
+ </details>
379
+
380
+ ---
381
+
382
+ ## Server-Side Decryption
383
+
384
+ The library uses a simple XOR cipher with Base64 encoding. This is easy to implement in any language.
385
+
386
+ ### Node.js
387
+
388
+ ```javascript
389
+ function decryptFingerprint(ciphertext, key) {
390
+ const encrypted = Buffer.from(ciphertext, 'base64');
391
+ const keyBytes = Buffer.from(key, 'utf8');
392
+ const decrypted = Buffer.alloc(encrypted.length);
393
+
394
+ for (let i = 0; i < encrypted.length; i++) {
395
+ decrypted[i] = encrypted[i] ^ keyBytes[i % keyBytes.length];
396
+ }
397
+
398
+ let fingerprint = JSON.parse(decrypted.toString('utf8'));
399
+ // Handle double-JSON-encoding if present
400
+ if (typeof fingerprint === 'string') {
401
+ fingerprint = JSON.parse(fingerprint);
402
+ }
403
+ return fingerprint;
404
+ }
405
+ ```
406
+
407
+ ### Python
408
+
409
+ ```python
410
+ import base64
411
+ import json
412
+
413
+ def decrypt_fingerprint(ciphertext: str, key: str) -> dict:
414
+ encrypted = base64.b64decode(ciphertext)
415
+ key_bytes = key.encode('utf-8')
416
+
417
+ decrypted = bytearray(len(encrypted))
418
+ for i in range(len(encrypted)):
419
+ decrypted[i] = encrypted[i] ^ key_bytes[i % len(key_bytes)]
420
+
421
+ fingerprint = json.loads(decrypted.decode('utf-8'))
422
+ # Handle double-JSON-encoding if present
423
+ if isinstance(fingerprint, str):
424
+ fingerprint = json.loads(fingerprint)
425
+ return fingerprint
426
+ ```
427
+
428
+ ### Other Languages
429
+
430
+ The algorithm is straightforward to port:
431
+
432
+ 1. **Base64 decode** the ciphertext to get raw bytes
433
+ 2. **XOR each byte** with the corresponding key byte (cycling through the key)
434
+ 3. **Decode** the result as UTF-8 to get the JSON string
435
+ 4. **Parse** the JSON to get the fingerprint object
436
+
437
+ See the [`examples/`](./examples/) folder for complete Node.js and Python server examples.
438
+
439
+ ---
440
+
441
+ ## Advanced: Custom Builds
442
+
443
+ By default, fpscanner uses a placeholder key (`my-shared-secret`) and no obfuscation. This is fine for development, but for production you should use encryption and obfuscation to make it harder for attackers to forge payloads.
444
+
445
+ ### Bring Your Own Encryption/Obfuscation
446
+
447
+ The library provides built-in encryption and obfuscation, but **you're not required to use them**. If you prefer:
448
+
449
+ - Use `collectFingerprint({ encrypt: false })` to get the raw fingerprint object
450
+ - Apply your own encryption, signing, or encoding before sending to your server
451
+ - Run your own obfuscation tool (Terser, JavaScript Obfuscator, etc.) on your bundle
452
+
453
+ 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.
454
+
455
+ ### Why Custom Builds?
456
+
457
+ | Threat | Without Protection | With Encryption + Obfuscation |
458
+ |--------|---------------------|-------------------|
459
+ | Payload forgery | Attacker can craft fake fingerprints | Key is hidden in obfuscated code |
460
+ | Replay attacks | Attacker captures and replays fingerprints | Server validates timestamp + nonce |
461
+ | Code inspection | Detection logic is readable | Control flow obfuscation makes analysis harder |
462
+
463
+ > **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.
464
+
465
+ ### Build with Your Key
466
+
467
+ ```bash
468
+ npx fpscanner build --key=your-secret-key-here
469
+ ```
470
+
471
+ This will:
472
+ 1. Rebuild the library with your key baked in
473
+ 2. Obfuscate the output to protect the key
474
+ 3. Overwrite the files in `node_modules/fpscanner/dist/`
475
+
476
+ ### Key Injection Methods
477
+
478
+ The CLI supports multiple methods (in order of priority):
479
+
480
+ ```bash
481
+ # 1. Command line argument (highest priority)
482
+ npx fpscanner build --key=your-secret-key
483
+
484
+ # 2. Environment variable
485
+ export FINGERPRINT_KEY=your-secret-key
486
+ npx fpscanner build
487
+
488
+ # 3. .env file
489
+ echo "FINGERPRINT_KEY=your-secret-key" >> .env
490
+ npx fpscanner build
491
+
492
+ # 4. Custom env file
493
+ npx fpscanner build --env-file=.env.production
494
+ ```
495
+
496
+ ### CI/CD Integration
497
+
498
+ Add a `postinstall` script to automatically build with your key:
499
+
500
+ ```json
501
+ {
502
+ "scripts": {
503
+ "postinstall": "fpscanner build"
504
+ }
505
+ }
506
+ ```
507
+
508
+ Then set `FINGERPRINT_KEY` as a secret in your CI/CD:
509
+
510
+ **GitHub Actions:**
511
+
512
+ ```yaml
513
+ env:
514
+ FINGERPRINT_KEY: ${{ secrets.FINGERPRINT_KEY }}
515
+
516
+ steps:
517
+ - run: npm install # postinstall runs fpscanner build automatically
518
+ ```
519
+
520
+ ### Build Options
521
+
522
+ | Option | Description |
523
+ |--------|-------------|
524
+ | `--key=KEY` | Encryption key (highest priority) |
525
+ | `--env-file=FILE` | Load key from custom env file |
526
+ | `--no-obfuscate` | Skip obfuscation (faster, for development) |
527
+
528
+ #### Skip Obfuscation
529
+
530
+ Obfuscation is enabled by default. For faster builds during development:
531
+
532
+ ```bash
533
+ # Via CLI flag
534
+ npx fpscanner build --key=dev-key --no-obfuscate
535
+
536
+ # Via environment variable
537
+ FINGERPRINT_OBFUSCATE=false npx fpscanner build
538
+
539
+ # In .env file
540
+ FINGERPRINT_OBFUSCATE=false
541
+ ```
542
+
543
+ > ⚠️ **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.
544
+
545
+ ---
546
+
547
+ ## Development
548
+
549
+ ### Local Development Scripts
550
+
551
+ ```bash
552
+ # Quick build (default key, no obfuscation)
553
+ npm run build
554
+
555
+ # Build with dev-key, no obfuscation
556
+ npm run build:dev
557
+
558
+ # Build + serve test/dev-source.html at localhost:3000
559
+ npm run dev
560
+
561
+ # Build with obfuscation
562
+ npm run build:obfuscate
563
+
564
+ # Production build (key from .env, with obfuscation)
565
+ npm run build:prod
566
+
567
+ # Watch mode (rebuilds on changes)
568
+ npm run watch
569
+ ```
570
+
571
+ ### Testing
572
+
573
+ ```bash
574
+ npm test
575
+ ```
576
+
577
+ ---
578
+
579
+ ## Security Best Practices
580
+
581
+ 1. **Use a strong, random key and rotate it regularly**
582
+ 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.
583
+
584
+ 2. **Use obfuscation in production**
585
+ 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.
586
+
587
+ 3. **Validate timestamps server-side**
588
+ 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.
589
+
590
+ 4. **Track nonces**
591
+ Optionally store recently seen nonces and reject duplicates. This provides an additional layer of replay protection, especially for high-value or abuse-prone endpoints.
592
+
593
+ 5. **Monitor fingerprint distributions over time**
594
+ 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.
595
+
596
+ 6. **Defense in depth on sensitive endpoints**
597
+ 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.
598
+
599
+
600
+ ---
601
+
602
+ ## Troubleshooting
603
+
604
+ ### "No encryption key found!"
605
+
606
+ Provide a key via one of the supported methods:
607
+
608
+ ```bash
609
+ npx fpscanner build --key=your-key
610
+ # or
611
+ export FINGERPRINT_KEY=your-key && npx fpscanner build
612
+ # or
613
+ echo "FINGERPRINT_KEY=your-key" >> .env && npx fpscanner build
614
+ ```
615
+
616
+ ### Decryption returns garbage
617
+
618
+ Make sure you're using the **exact same key** on your server that you used when building. Keys must match exactly.
619
+
620
+ ### Obfuscation is slow
621
+
622
+ Use `--no-obfuscate` during development. Only enable obfuscation for production builds.
623
+
624
+ ### `postinstall` fails in CI
625
+
626
+ Ensure `FINGERPRINT_KEY` is set as an environment variable before `npm install` runs.
627
+
628
+ ---
629
+
630
+ ## Limits and non-goals
631
+
632
+ 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.
633
+
634
+ ### Open source and attacker adaptation
635
+
636
+ 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.
637
+
638
+ 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.
639
+
640
+ ### Obfuscation is not a silver bullet
641
+
642
+ 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.
643
+
644
+ ### Limits of client-side detection
645
+
646
+ 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.
647
+
648
+ 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.
649
+
650
+ ### Not an end-to-end solution
46
651
 
47
- **analyseFingerprint** returns an array of analysisResult's objects.
48
- Each object contains the following information:
49
- - *name*: the name of the test;
50
- - *consistent*: the result of the test (CONSISTENT, UNSURE, INCONSISTENT)
51
- - *data*: data related to the test
652
+ 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.
52
653
 
53
- ## Detection tests
654
+ 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/).
54
655
 
55
- Summary of the tests used to detect bots. For more details, visit
56
- the documentation page (coming soon).
57
656
 
58
- - **PHANTOM_UA:** Detect PhantomJS user agent
59
- - **PHANTOM_PROPERTIES:** Test the presence of properties introduced by PhantomJS
60
- - **PHANTOM_ETSL:** Runtime verification for PhantomJS
61
- - **PHANTOM_LANGUAGE:** Use *navigator.languages* to detect PhantomJS
62
- - **PHANTOM_WEBSOCKET:** Analyze the error thrown when creating a websocket
63
- - **MQ_SCREEN:** Use media query related to the screen
64
- - **PHANTOM_OVERFLOW:** Analyze error thrown when a stack overflow occurs
65
- - **PHANTOM_WINDOW_HEIGHT:** Analyze window screen dimension
66
- - **HEADCHR_UA:** Detect Chrome Headless user agent
67
- - **WEBDRIVER:** Test the presence of *webriver* attributes
68
- - **HEADCHR_CHROME_OBJ:** Test the presence of the *window.chrome* object
69
- - **HEADCHR_PERMISSIONS:** Test permissions management
70
- - **HEADCHR_PLUGINS:** Verify the number of plugins
71
- - **HEADCHR_IFRAME:** Test presence of Chrome Headless using an iframe
72
- - **CHR_DEBUG_TOOLS:** Test if debug tools are opened
73
- - **SELENIUM_DRIVER:** Test the presence of Selenium drivers
74
- - **CHR_BATTERY:** Test the presence of *battery*
75
- - **CHR_MEMORY:** Verify if *navigator.deviceMemory* is consistent
76
- - **TRANSPARENT_PIXEL:** Verify if a canvas pixel is transparent
657
+ ---
77
658
 
78
- # Acknowledgements
79
- 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.
659
+ ## License
80
660
 
81
- ![Logo of CrossBrowserTesting](https://seeklogo.com/images/C/cross-browser-testing-logo-300E2AF44B-seeklogo.com.png)
661
+ MIT