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.
- package/README.md +636 -56
- package/bin/cli.js +216 -0
- package/dist/crypto-helpers.d.ts +19 -0
- package/dist/crypto-helpers.d.ts.map +1 -0
- package/dist/detections/hasCDP.d.ts +3 -0
- package/dist/detections/hasCDP.d.ts.map +1 -0
- package/dist/detections/hasContextMismatch.d.ts +3 -0
- package/dist/detections/hasContextMismatch.d.ts.map +1 -0
- package/dist/detections/hasHeadlessChromeScreenResolution.d.ts +3 -0
- package/dist/detections/hasHeadlessChromeScreenResolution.d.ts.map +1 -0
- package/dist/detections/hasHighCPUCount.d.ts +3 -0
- package/dist/detections/hasHighCPUCount.d.ts.map +1 -0
- package/dist/detections/hasImpossibleDeviceMemory.d.ts +3 -0
- package/dist/detections/hasImpossibleDeviceMemory.d.ts.map +1 -0
- package/dist/detections/hasMismatchPlatformIframe.d.ts +3 -0
- package/dist/detections/hasMismatchPlatformIframe.d.ts.map +1 -0
- package/dist/detections/hasMismatchPlatformWorker.d.ts +3 -0
- package/dist/detections/hasMismatchPlatformWorker.d.ts.map +1 -0
- package/dist/detections/hasMismatchWebGLInWorker.d.ts +3 -0
- package/dist/detections/hasMismatchWebGLInWorker.d.ts.map +1 -0
- package/dist/detections/hasMissingChromeObject.d.ts +3 -0
- package/dist/detections/hasMissingChromeObject.d.ts.map +1 -0
- package/dist/detections/hasPlaywright.d.ts +3 -0
- package/dist/detections/hasPlaywright.d.ts.map +1 -0
- package/dist/detections/hasSeleniumProperty.d.ts +3 -0
- package/dist/detections/hasSeleniumProperty.d.ts.map +1 -0
- package/dist/detections/hasSwiftshaderRenderer.d.ts +3 -0
- package/dist/detections/hasSwiftshaderRenderer.d.ts.map +1 -0
- package/dist/detections/hasUTCTimezone.d.ts +3 -0
- package/dist/detections/hasUTCTimezone.d.ts.map +1 -0
- package/dist/detections/hasWebdriver.d.ts +3 -0
- package/dist/detections/hasWebdriver.d.ts.map +1 -0
- package/dist/detections/hasWebdriverIframe.d.ts +3 -0
- package/dist/detections/hasWebdriverIframe.d.ts.map +1 -0
- package/dist/detections/hasWebdriverWorker.d.ts +3 -0
- package/dist/detections/hasWebdriverWorker.d.ts.map +1 -0
- package/dist/detections/hasWebdriverWritable.d.ts +3 -0
- package/dist/detections/hasWebdriverWritable.d.ts.map +1 -0
- package/dist/fpScanner.cjs.js +31 -0
- package/dist/fpScanner.es.js +1066 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/signals/browserExtensions.d.ts +5 -0
- package/dist/signals/browserExtensions.d.ts.map +1 -0
- package/dist/signals/browserFeatures.d.ts +14 -0
- package/dist/signals/browserFeatures.d.ts.map +1 -0
- package/dist/signals/canvas.d.ts +6 -0
- package/dist/signals/canvas.d.ts.map +1 -0
- package/dist/signals/cdp.d.ts +2 -0
- package/dist/signals/cdp.d.ts.map +1 -0
- package/dist/signals/cpuCount.d.ts +2 -0
- package/dist/signals/cpuCount.d.ts.map +1 -0
- package/dist/signals/etsl.d.ts +2 -0
- package/dist/signals/etsl.d.ts.map +1 -0
- package/dist/signals/highEntropyValues.d.ts +11 -0
- package/dist/signals/highEntropyValues.d.ts.map +1 -0
- package/dist/signals/iframe.d.ts +9 -0
- package/dist/signals/iframe.d.ts.map +1 -0
- package/dist/signals/internationalization.d.ts +5 -0
- package/dist/signals/internationalization.d.ts.map +1 -0
- package/dist/signals/languages.d.ts +5 -0
- package/dist/signals/languages.d.ts.map +1 -0
- package/dist/signals/maths.d.ts +2 -0
- package/dist/signals/maths.d.ts.map +1 -0
- package/dist/signals/mediaCodecs.d.ts +11 -0
- package/dist/signals/mediaCodecs.d.ts.map +1 -0
- package/dist/signals/mediaQueries.d.ts +13 -0
- package/dist/signals/mediaQueries.d.ts.map +1 -0
- package/dist/signals/memory.d.ts +2 -0
- package/dist/signals/memory.d.ts.map +1 -0
- package/dist/signals/multimediaDevices.d.ts +2 -0
- package/dist/signals/multimediaDevices.d.ts.map +1 -0
- package/dist/signals/navigatorPropertyDescriptors.d.ts +2 -0
- package/dist/signals/navigatorPropertyDescriptors.d.ts.map +1 -0
- package/dist/signals/nonce.d.ts +2 -0
- package/dist/signals/nonce.d.ts.map +1 -0
- package/dist/signals/platform.d.ts +2 -0
- package/dist/signals/platform.d.ts.map +1 -0
- package/dist/signals/playwright.d.ts +2 -0
- package/dist/signals/playwright.d.ts.map +1 -0
- package/dist/signals/plugins.d.ts +9 -0
- package/dist/signals/plugins.d.ts.map +1 -0
- package/dist/signals/screenResolution.d.ts +12 -0
- package/dist/signals/screenResolution.d.ts.map +1 -0
- package/dist/signals/seleniumProperties.d.ts +2 -0
- package/dist/signals/seleniumProperties.d.ts.map +1 -0
- package/dist/signals/time.d.ts +2 -0
- package/dist/signals/time.d.ts.map +1 -0
- package/dist/signals/toSourceError.d.ts +5 -0
- package/dist/signals/toSourceError.d.ts.map +1 -0
- package/dist/signals/url.d.ts +2 -0
- package/dist/signals/url.d.ts.map +1 -0
- package/dist/signals/userAgent.d.ts +2 -0
- package/dist/signals/userAgent.d.ts.map +1 -0
- package/dist/signals/utils.d.ts +11 -0
- package/dist/signals/utils.d.ts.map +1 -0
- package/dist/signals/webGL.d.ts +5 -0
- package/dist/signals/webGL.d.ts.map +1 -0
- package/dist/signals/webdriver.d.ts +2 -0
- package/dist/signals/webdriver.d.ts.map +1 -0
- package/dist/signals/webdriverWritable.d.ts +2 -0
- package/dist/signals/webdriverWritable.d.ts.map +1 -0
- package/dist/signals/webgpu.d.ts +7 -0
- package/dist/signals/webgpu.d.ts.map +1 -0
- package/dist/signals/worker.d.ts +2 -0
- package/dist/signals/worker.d.ts.map +1 -0
- package/dist/types.d.ts +207 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +54 -7
- package/scripts/build-custom.js +204 -0
- package/src/crypto-helpers.ts +50 -0
- package/src/detections/hasCDP.ts +5 -0
- package/src/detections/hasContextMismatch.ts +19 -0
- package/src/detections/hasHeadlessChromeScreenResolution.ts +10 -0
- package/src/detections/hasHighCPUCount.ts +9 -0
- package/src/detections/hasImpossibleDeviceMemory.ts +9 -0
- package/src/detections/hasMismatchPlatformIframe.ts +10 -0
- package/src/detections/hasMismatchPlatformWorker.ts +10 -0
- package/src/detections/hasMismatchWebGLInWorker.ts +13 -0
- package/src/detections/hasMissingChromeObject.ts +6 -0
- package/src/detections/hasPlaywright.ts +5 -0
- package/src/detections/hasSeleniumProperty.ts +5 -0
- package/src/detections/hasSwiftshaderRenderer.ts +5 -0
- package/src/detections/hasUTCTimezone.ts +5 -0
- package/src/detections/hasWebdriver.ts +5 -0
- package/src/detections/hasWebdriverIframe.ts +5 -0
- package/src/detections/hasWebdriverWorker.ts +5 -0
- package/src/detections/hasWebdriverWritable.ts +5 -0
- package/src/globals.d.ts +10 -0
- package/src/index.ts +644 -0
- package/src/signals/browserExtensions.ts +57 -0
- package/src/signals/browserFeatures.ts +24 -0
- package/src/signals/canvas.ts +84 -0
- package/src/signals/cdp.ts +18 -0
- package/src/signals/cpuCount.ts +5 -0
- package/src/signals/etsl.ts +3 -0
- package/src/signals/highEntropyValues.ts +48 -0
- package/src/signals/iframe.ts +34 -0
- package/src/signals/internationalization.ts +24 -0
- package/src/signals/languages.ts +6 -0
- package/src/signals/maths.ts +30 -0
- package/src/signals/mediaCodecs.ts +120 -0
- package/src/signals/mediaQueries.ts +85 -0
- package/src/signals/memory.ts +5 -0
- package/src/signals/multimediaDevices.ts +34 -0
- package/src/signals/navigatorPropertyDescriptors.ts +17 -0
- package/src/signals/nonce.ts +3 -0
- package/src/signals/platform.ts +3 -0
- package/src/signals/playwright.ts +3 -0
- package/src/signals/plugins.ts +70 -0
- package/src/signals/screenResolution.ts +15 -0
- package/src/signals/seleniumProperties.ts +40 -0
- package/src/signals/time.ts +3 -0
- package/src/signals/toSourceError.ts +27 -0
- package/src/signals/url.ts +3 -0
- package/src/signals/userAgent.ts +3 -0
- package/src/signals/utils.ts +29 -0
- package/src/signals/webGL.ts +28 -0
- package/src/signals/webdriver.ts +3 -0
- package/src/signals/webdriverWritable.ts +15 -0
- package/src/signals/webgpu.ts +28 -0
- package/src/signals/worker.ts +77 -0
- package/src/types.ts +237 -0
- package/.babelrc +0 -3
- package/.travis.yml +0 -17
- package/src/fpScanner.js +0 -223
- package/test/test.html +0 -11
- package/test/test.js +0 -116
package/README.md
CHANGED
|
@@ -1,81 +1,661 @@
|
|
|
1
1
|
# Fingerprint Scanner
|
|
2
|
-
[](https://travis-ci.org/antoinevastel/fpscanner)
|
|
3
2
|
|
|
4
|
-
|
|
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
|
-
|
|
5
|
+
[](https://github.com/antoinevastel/fpscanner/actions/workflows/ci.yml)
|
|
7
6
|
|
|
7
|
+
## Sponsor
|
|
8
8
|
|
|
9
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
Then, we can analyze the fingerprint with the scanner.
|
|
290
|
+
<details>
|
|
291
|
+
<summary><strong>Bitmask Reference</strong></summary>
|
|
28
292
|
|
|
29
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
661
|
+
MIT
|