@anonympins/fingerprint 0.0.3 → 0.0.5
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 +31 -21
- package/fingerprint.js +291 -76
- package/package.json +48 -47
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
[](https://github.com/anonympins/fingerprint/actions/workflows/ci.yml)
|
|
3
3
|
[](https://github.com/anonympins/fingerprint/releases)
|
|
4
4
|
[](https://github.com/anonympins/fingerprint/blob/main/LICENSE)
|
|
5
|
-
|
|
5
|
+

|
|
6
6
|
[](https://github.com/anonympins/fingerprint/watchers)
|
|
7
7
|
|
|
8
8
|
An HTTP(S) client mitigation and anti-bot protection library for Node.js/Express, based on digital fingerprinting and dynamic Proof-of-Work (PoW) challenges.
|
|
@@ -19,6 +19,7 @@ The process unfolds in three steps:
|
|
|
19
19
|
* **Device Behavior**: Rapid fingerprint changes (User-Agent rotation).
|
|
20
20
|
* **IP Behavior**: An excessive number of different devices seen from the same IP, or a single device using a large number of IPs (proxy rotation).
|
|
21
21
|
* **Inconsistency**: A low similarity score between the current fingerprint and the initial one associated with the `device_id` (cookie theft detection).
|
|
22
|
+
* **Honeypot Trap**: Detection of bots that automatically fill hidden form fields or probe for common but unused URL parameters (e.g., `?debug=true`).
|
|
22
23
|
3. **Dynamic Challenge**: If the suspicion score exceeds a certain threshold, a challenge is presented to the user. The difficulty and type of challenge depend on the score:
|
|
23
24
|
* **Low to Medium Suspicion**: A combined CPU and Memory Proof-of-Work (PoW) challenge is issued. The difficulty of both the CPU (hash calculation) and Memory (allocation and computation) components scales progressively with the suspicion score. For low scores, the memory challenge is negligible, making it primarily a CPU task.
|
|
24
25
|
* **High Suspicion**: For the most suspicious requests, the system issues a high-difficulty combined CPU/Memory challenge. The architecture allows for plugging in more complex challenges like CAPTCHAs if needed.
|
|
@@ -30,7 +31,7 @@ Once the challenge is solved, a clearance "ticket" is issued via a secure cookie
|
|
|
30
31
|
- **Multi-Factor Fingerprinting**: Combines client-side data (`hardwareConcurrency`, `deviceMemory`, `screen`, `canvas`, `webgl`) and server-side data (`User-Agent`, `Client-Hints`).
|
|
31
32
|
- **Secure Ticket System**: Uses HMAC-SHA256 signatures to validate clearances and prevent tampering.
|
|
32
33
|
- **Pluggable Datastore**: Supports external datastores like Redis for state persistence and scalability across multiple server instances.
|
|
33
|
-
- **Express.js Middleware**: Easy integration into an Express application with `powMiddleware`.
|
|
34
|
+
- **Express.js Middleware**: Easy integration into an Express application with `powMiddleware`. The datastore must support setting a Time-To-Live (TTL) for challenge secrets.
|
|
34
35
|
- **Timing Attack Protection**: Uses `crypto.timingSafeEqual` for secure ticket validation.
|
|
35
36
|
- **Automatic Threshold Tuning**: Includes a genetic algorithm-based optimizer (`startThresholdAutoTuning`) that analyzes real traffic to dynamically adjust suspicion thresholds (`low`, `medium`, `high`), improving bot detection accuracy and reducing false positives over time.
|
|
36
37
|
|
|
@@ -40,7 +41,7 @@ This module is designed for a Node.js environment.
|
|
|
40
41
|
|
|
41
42
|
### Prerequisites
|
|
42
43
|
|
|
43
|
-
Ensure you have
|
|
44
|
+
Ensure you have middleware for parsing cookies (like `cookie-parser`) and request bodies (like `express.json` and `express.urlencoded`) set up in your Express application *before* the `powMiddleware`.
|
|
44
45
|
|
|
45
46
|
### Configuration
|
|
46
47
|
|
|
@@ -56,11 +57,14 @@ The `powMiddleware` requires a configuration object defining the weights of susp
|
|
|
56
57
|
|
|
57
58
|
```javascript
|
|
58
59
|
import express from 'express';
|
|
60
|
+
import bodyParser from 'body-parser';
|
|
59
61
|
import cookieParser from 'cookie-parser';
|
|
60
62
|
import { powMiddleware /*, configurePow */ } from './fingerprint.js'; // Adjust the path
|
|
61
63
|
|
|
62
64
|
const app = express();
|
|
63
65
|
app.use(cookieParser());
|
|
66
|
+
app.use(bodyParser.json()); // For parsing application/json
|
|
67
|
+
app.use(bodyParser.urlencoded({ extended: true })); // For parsing application/x-www-form-urlencoded
|
|
64
68
|
|
|
65
69
|
// Configuration of weights and thresholds for calculating the suspicion score.
|
|
66
70
|
// These values should be adjusted based on traffic and expected user behavior.
|
|
@@ -69,13 +73,25 @@ const securityConfig = {
|
|
|
69
73
|
historyScore: 0.3, // Penalizes IP rotation (proxy)
|
|
70
74
|
rotationScore: 0.5, // Penalizes rapid fingerprint changes (user-agent, etc.)
|
|
71
75
|
headerAnomalyScore: 0.1, // Penalizes abnormal headers (missing UA, etc.)
|
|
72
|
-
inconsistencyScore: 0.8
|
|
76
|
+
inconsistencyScore: 0.8, // Strongly penalizes inconsistency between the current and initial fingerprint (stolen cookie)
|
|
77
|
+
honeypotScore: 1.0 // Strongly penalizes bots filling hidden form fields
|
|
73
78
|
},
|
|
74
79
|
thresholds: {
|
|
75
80
|
low: 20, // Score from which a CPU challenge is issued
|
|
76
81
|
medium: 45, // Score for a more difficult combined CPU/Memory challenge
|
|
77
82
|
high: 75, // Score for a very difficult challenge
|
|
78
|
-
block: 95
|
|
83
|
+
block: 95, // Score above which the request is blocked outright (HTTP 403)
|
|
84
|
+
isStaticResource: (req) => req.path.startsWith('/static/') // Optional: Custom function to identify static resources
|
|
85
|
+
},
|
|
86
|
+
honeypot: {
|
|
87
|
+
// List of field names that are traps for bots.
|
|
88
|
+
// These should be hidden in forms for humans, or be URL parameters your app never uses.
|
|
89
|
+
fields: ['email_confirm', 'user_nickname', 'debug', 'test_mode', 'admin'], // (Optional)
|
|
90
|
+
// List of URL paths that should never be accessed by a legitimate user.
|
|
91
|
+
// A request to one of these paths will immediately flag the device as malicious.
|
|
92
|
+
trapUrls: ['/wp-admin', '/.env', '/admin.php', '/phpmyadmin'], // (Optional)
|
|
93
|
+
// Automatically detect common SQL/NoSQL injection and RCE patterns in request values. (Optional, default: true)
|
|
94
|
+
detectInjections: true
|
|
79
95
|
}
|
|
80
96
|
};
|
|
81
97
|
|
|
@@ -222,9 +238,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
222
238
|
clientIp: req.socket.remoteAddress,
|
|
223
239
|
path: req.url.split('?')[0],
|
|
224
240
|
cookies: {}, // Parse cookies from req.headers.cookie
|
|
225
|
-
query:
|
|
241
|
+
query: new URL(req.url, `http://${req.headers.host}`).searchParams,
|
|
226
242
|
headers: req.headers,
|
|
227
|
-
isStatic: /\.(js|css|png)$/.test(req.url),
|
|
228
243
|
rawReq: req, // Pass the raw request
|
|
229
244
|
rawRes: res, // Pass the raw response for cookie setting
|
|
230
245
|
};
|
|
@@ -261,35 +276,30 @@ Manually setting the `low`, `medium`, and `high` thresholds can be challenging.
|
|
|
261
276
|
|
|
262
277
|
1. **Enable Logging**: The auto-tuner needs data. You must provide a `logger` function in your security configuration. This function will be called for significant events (`challenge_issued`, `challenge_solved`, etc.).
|
|
263
278
|
|
|
264
|
-
2. **
|
|
279
|
+
2. **Enable Auto-tuning**: Add an `autotuning` property to your security configuration. The middleware will automatically start the tuning process.
|
|
265
280
|
|
|
266
281
|
```javascript
|
|
267
|
-
import { powMiddleware
|
|
282
|
+
import { powMiddleware } from './fingerprint.js';
|
|
268
283
|
|
|
269
284
|
// Array to store traffic analysis data. In a real application, this could be
|
|
270
285
|
// a more robust logging system.
|
|
271
286
|
const trafficData = [];
|
|
272
287
|
|
|
273
288
|
const securityConfig = {
|
|
274
|
-
weights: { /* ...
|
|
289
|
+
weights: { /* ... */ },
|
|
275
290
|
thresholds: {
|
|
276
291
|
low: 20, // Initial values, will be optimized
|
|
277
292
|
medium: 45,
|
|
278
293
|
high: 75
|
|
279
294
|
},
|
|
280
|
-
// The logger is required for auto-tuning
|
|
281
|
-
|
|
295
|
+
logger: (log) => trafficData.push(log), // The logger is required for auto-tuning
|
|
296
|
+
autotune: {
|
|
297
|
+
trafficData: trafficData, // The data source for the algorithm
|
|
298
|
+
interval: 1800000, // Optimization cycle every 30 minutes (optional)
|
|
299
|
+
minDataPoints: 200 // Minimum requests before starting optimization (optional)
|
|
300
|
+
}
|
|
282
301
|
};
|
|
283
302
|
|
|
284
|
-
// Start the background optimization process.
|
|
285
|
-
// The `securityConfig.thresholds` object will be mutated with optimized values.
|
|
286
|
-
startThresholdAutoTuning({
|
|
287
|
-
securityConfig: securityConfig, // The config object to be updated
|
|
288
|
-
trafficData: trafficData, // The data source for the algorithm
|
|
289
|
-
interval: 1800000, // Optimization cycle every 30 minutes
|
|
290
|
-
minDataPoints: 200 // Minimum requests before starting optimization
|
|
291
|
-
});
|
|
292
|
-
|
|
293
303
|
const powMiddlewareInstance = powMiddleware(securityConfig);
|
|
294
304
|
app.use(powMiddlewareInstance);
|
|
295
305
|
```
|
package/fingerprint.js
CHANGED
|
@@ -2,14 +2,49 @@
|
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
3
|
import { Optimization } from "./library.js";
|
|
4
4
|
import { cyrb53, FingerprintBuilder } from "./fingerprint.builder.js";
|
|
5
|
-
import { getDeviceHash } from "./fingerprint.server.js";
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Retrieves the POW_SECRET from environment variables with appropriate checks.
|
|
8
|
+
* @returns {string} The secret key.
|
|
9
|
+
*/
|
|
10
|
+
const getPowSecret = () => {
|
|
11
|
+
const secret = process.env.POW_SECRET;
|
|
12
|
+
if (!secret && process.env.NODE_ENV === 'production') {
|
|
13
|
+
throw new Error('POW_SECRET environment variable is not set. This is required for production.');
|
|
14
|
+
}
|
|
15
|
+
return secret || "fallback-dev-secret-32-chars-minimum";
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Creates a stable hash based on device characteristics, independent of the IP.
|
|
20
|
+
* This is our "level 2 fingerprint".
|
|
21
|
+
* @param {object} context - The request context.
|
|
22
|
+
* @returns {string} A hash representing the device.
|
|
23
|
+
*/
|
|
24
|
+
function getHeaderSignature(context) {
|
|
25
|
+
if (!context.rawHeaders) return '';
|
|
26
|
+
const headerKeys = [];
|
|
27
|
+
for (let i = 0; i < context.rawHeaders.length; i += 2) {
|
|
28
|
+
headerKeys.push(context.rawHeaders[i]);
|
|
29
|
+
}
|
|
30
|
+
return cyrb53(headerKeys.join(','));
|
|
31
|
+
}
|
|
32
|
+
export function getDeviceHash(context) {
|
|
33
|
+
// Prioritize the rich client-side fingerprint if provided.
|
|
34
|
+
const clientFp = context.headers['x-device-fingerprint'];
|
|
35
|
+
if (clientFp && typeof clientFp === 'string' && clientFp.includes('cvs:')) {
|
|
36
|
+
// Basic validation to ensure it looks like our client-side fingerprint.
|
|
37
|
+
return clientFp;
|
|
38
|
+
}
|
|
8
39
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
40
|
+
// Fallback to server-side only fingerprinting if the header is missing.
|
|
41
|
+
const srv = new FingerprintBuilder();
|
|
42
|
+
srv.add("ua", context.headers["user-agent"]);
|
|
43
|
+
if (context.headers["sec-ch-ua-platform"])
|
|
44
|
+
srv.add("os", context.headers["sec-ch-ua-platform"]);
|
|
45
|
+
if (context.headers["sec-ch-ua"]) srv.add("ch", context.headers["sec-ch-ua"]);
|
|
46
|
+
srv.add("h_ord", getHeaderSignature(context));
|
|
47
|
+
return srv.toString();
|
|
13
48
|
}
|
|
14
49
|
|
|
15
50
|
/**
|
|
@@ -289,7 +324,7 @@ export const verifyPoWAndGenerateTicket = (
|
|
|
289
324
|
// 2. Generate an HMAC ticket so the client doesn't have to do it again for 1 hour
|
|
290
325
|
const expiry = Date.now() + 3600000; // 1 heure
|
|
291
326
|
const signature = crypto
|
|
292
|
-
.createHmac("sha256",
|
|
327
|
+
.createHmac("sha256", getPowSecret())
|
|
293
328
|
.update(`${ip}:${expiry}`)
|
|
294
329
|
.digest("hex");
|
|
295
330
|
|
|
@@ -300,18 +335,22 @@ export const verifyPoWAndGenerateTicket = (
|
|
|
300
335
|
* Verifies a memory PoW solution.
|
|
301
336
|
* The server performs the same calculation to validate.
|
|
302
337
|
*/
|
|
303
|
-
export const verifyMemoryPoW = (nonce, solution, difficulty = 16) => {
|
|
338
|
+
export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret) => {
|
|
304
339
|
const size = difficulty * 1024 * 1024;
|
|
305
340
|
const iterations = size / 16;
|
|
306
341
|
const buffer = new Uint32Array(size / 4);
|
|
307
|
-
|
|
342
|
+
const seed = clientSecret ? `${nonce}:${clientSecret}` : nonce;
|
|
343
|
+
let h = new TextEncoder().encode(seed).reduce((acc, v) => acc + v, 0);
|
|
344
|
+
|
|
308
345
|
for (let i = 0; i < buffer.length; i++) {
|
|
309
346
|
buffer[i] = h = Math.imul(h ^ i, 1597334677);
|
|
310
347
|
}
|
|
348
|
+
|
|
311
349
|
let finalHash = 0;
|
|
350
|
+
let addr = buffer.length > 0 ? buffer[0] % buffer.length : 0;
|
|
312
351
|
for (let i = 0; i < iterations; i++) {
|
|
313
|
-
|
|
314
|
-
finalHash ^=
|
|
352
|
+
addr = buffer[addr] % buffer.length;
|
|
353
|
+
finalHash ^= addr;
|
|
315
354
|
}
|
|
316
355
|
return finalHash === parseInt(solution, 10);
|
|
317
356
|
};
|
|
@@ -320,7 +359,7 @@ export const isTicketValid = (ip, ticket) => {
|
|
|
320
359
|
const [expiry, sig] = ticket.split(":");
|
|
321
360
|
if (!expiry || !sig || Date.now() > parseInt(expiry, 10)) return false;
|
|
322
361
|
const expectedSig = crypto
|
|
323
|
-
.createHmac("sha256",
|
|
362
|
+
.createHmac("sha256", getPowSecret())
|
|
324
363
|
.update(`${ip}:${expiry}`)
|
|
325
364
|
.digest("hex");
|
|
326
365
|
|
|
@@ -359,6 +398,122 @@ function getHeaderAnomalies(context) {
|
|
|
359
398
|
};
|
|
360
399
|
}
|
|
361
400
|
|
|
401
|
+
/**
|
|
402
|
+
* Checks for submitted honeypot fields to detect bots.
|
|
403
|
+
* @param {object} context - The request context.
|
|
404
|
+
* @param {object} honeypotConfig - The honeypot configuration.
|
|
405
|
+
* @returns {{honeypotScore: number}}
|
|
406
|
+
*/
|
|
407
|
+
function getHoneypotScore(context, honeypotConfig = {}) {
|
|
408
|
+
const { fields = [], trapUrls = [], detectInjections = true } = honeypotConfig;
|
|
409
|
+
|
|
410
|
+
// 1. Check for trap URL access
|
|
411
|
+
if (trapUrls.some(trap => context.path.startsWith(trap))) {
|
|
412
|
+
return { honeypotScore: 100 };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (fields.length === 0 && !detectInjections) {
|
|
416
|
+
return { honeypotScore: 0 };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Check both query parameters (for URL probing) and the request body (for hidden form fields).
|
|
420
|
+
const queryData =
|
|
421
|
+
context.query instanceof URLSearchParams
|
|
422
|
+
? Object.fromEntries(context.query.entries())
|
|
423
|
+
: context.query || {};
|
|
424
|
+
const bodyData = context.body || {};
|
|
425
|
+
|
|
426
|
+
// 2. Check for honeypot field names
|
|
427
|
+
for (const field of fields) {
|
|
428
|
+
// A bot is trapped if the field exists in either the query OR the body.
|
|
429
|
+
if (
|
|
430
|
+
Object.prototype.hasOwnProperty.call(queryData, field) ||
|
|
431
|
+
Object.prototype.hasOwnProperty.call(bodyData, field)
|
|
432
|
+
) {
|
|
433
|
+
return { honeypotScore: 100 }; // A bot fell into the trap, maximum score.
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// 3. Check for injection attempts in values
|
|
438
|
+
if (detectInjections) {
|
|
439
|
+
// Regex for common SQL injection patterns
|
|
440
|
+
const sqlRegex = new RegExp(
|
|
441
|
+
"('|\"|;|--|#|/\\*.*\\*/)|\\b(union|select|insert|update|delete|drop|truncate)\\b",
|
|
442
|
+
"i"
|
|
443
|
+
);
|
|
444
|
+
// Regex for common NoSQL (MongoDB) injection patterns (e.g., keys starting with '$')
|
|
445
|
+
const nosqlKeyRegex = /"\$[^"]*":/;
|
|
446
|
+
// Regex for common Remote Code Execution (RCE) patterns
|
|
447
|
+
const rceRegex = new RegExp(
|
|
448
|
+
// File traversal, command execution functions, and shell commands
|
|
449
|
+
"(\\.\\./|\\.\\.\\\\)|\\b(exec|system|shell_exec|passthru|popen|proc_open|eval|assert|require|include)(_once)?\\s*\\(|\\b(wget|curl|bash|sh|powershell)\\b",
|
|
450
|
+
"i"
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
const inspect = (obj) => {
|
|
454
|
+
for (const key in obj) {
|
|
455
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
456
|
+
const value = obj[key];
|
|
457
|
+
if (typeof value === 'string') {
|
|
458
|
+
if (rceRegex.test(value) || sqlRegex.test(value)) return true;
|
|
459
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
460
|
+
// For NoSQL, we check the stringified version of the object to find keys like "$gt"
|
|
461
|
+
// This is more accurate when done on the object itself.
|
|
462
|
+
if (nosqlKeyRegex.test(JSON.stringify(value))) return true;
|
|
463
|
+
if (inspect(value)) return true;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return false;
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
if (inspect(queryData)) {
|
|
471
|
+
return { honeypotScore: 100 };
|
|
472
|
+
}
|
|
473
|
+
if (inspect(bodyData)) {
|
|
474
|
+
return { honeypotScore: 100 };
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return { honeypotScore: 0 };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const trapUrlTemplates = [
|
|
482
|
+
'/includes/config-{RANDOM}.php', // Classic PHP config file
|
|
483
|
+
'/.env.{RANDOM}', // Environment file
|
|
484
|
+
'/backups/db_backup_{RANDOM}.sql.gz', // Database backup
|
|
485
|
+
'/api/v1/internal/status?trace={RANDOM}', // Internal API endpoint
|
|
486
|
+
'/_private/deploy_key_{RANDOM}.pem', // Private key file
|
|
487
|
+
'/logs/app_error_{RANDOM}.log', // Log file
|
|
488
|
+
'/.git/config_{RANDOM}' // Exposed git config variant
|
|
489
|
+
];
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Generates a signed trap URL.
|
|
493
|
+
* @param {string} nonce - The nonce to sign the URL with.
|
|
494
|
+
* @returns {string} The trap URL.
|
|
495
|
+
*/
|
|
496
|
+
function generateTrapUrl(nonce) {
|
|
497
|
+
// Pick a random template to diversify the traps
|
|
498
|
+
const template = trapUrlTemplates[Math.floor(Math.random() * trapUrlTemplates.length)];
|
|
499
|
+
const randomPart = crypto.randomBytes(8).toString('hex');
|
|
500
|
+
const path = template.replace('{RANDOM}', randomPart);
|
|
501
|
+
|
|
502
|
+
const signature = crypto.createHmac('sha256', getPowSecret()).update(nonce + path).digest('hex').substring(0, 16);
|
|
503
|
+
return `${path}?sig=${signature}`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Verifies if a given path is a valid trap URL for a given nonce.
|
|
508
|
+
* @param {string} path - The request path.
|
|
509
|
+
* @param {string} signature - The signature from the query.
|
|
510
|
+
* @param {string} nonce - The nonce to verify against.
|
|
511
|
+
* @returns {boolean}
|
|
512
|
+
*/
|
|
513
|
+
function verifyTrapUrl(path, signature, nonce) {
|
|
514
|
+
const expectedSignature = crypto.createHmac('sha256', getPowSecret()).update(nonce + path).digest('hex').substring(0, 16);
|
|
515
|
+
return signature === expectedSignature;
|
|
516
|
+
}
|
|
362
517
|
/**
|
|
363
518
|
* @typedef {object} IStore
|
|
364
519
|
* @property {(key: string) => Promise<any>} get
|
|
@@ -369,7 +524,7 @@ function getHeaderAnomalies(context) {
|
|
|
369
524
|
|
|
370
525
|
/**
|
|
371
526
|
* Default in-memory store implementation.
|
|
372
|
-
* @type {IStore}
|
|
527
|
+
* @type {IStore}
|
|
373
528
|
*/
|
|
374
529
|
const inMemoryStore = {
|
|
375
530
|
_map: new Map(),
|
|
@@ -395,7 +550,7 @@ export const configureStore = (externalStore) => {
|
|
|
395
550
|
* Orchestrates request identification using a persistent anchor (cookie)
|
|
396
551
|
* and fingerprint verification.
|
|
397
552
|
* @param {object} context - The request context.
|
|
398
|
-
* @returns {Promise<{deviceId: string, deviceData: object, consistencyScore: number, newCookie: object|null}>}
|
|
553
|
+
* @returns {Promise<{deviceId: string, deviceData: object, consistencyScore: number, newCookie: object|null}>}
|
|
399
554
|
*/
|
|
400
555
|
async function resolveRequestIdentity(context) {
|
|
401
556
|
const existingDeviceId = context.cookies?.device_id;
|
|
@@ -404,7 +559,6 @@ async function resolveRequestIdentity(context) {
|
|
|
404
559
|
let consistencyScore = 1.0; // 1.0 = perfectly consistent
|
|
405
560
|
let deviceData = null;
|
|
406
561
|
let newCookie = null;
|
|
407
|
-
|
|
408
562
|
if (deviceId) {
|
|
409
563
|
deviceData = await store.get(`device:${deviceId}`);
|
|
410
564
|
}
|
|
@@ -470,7 +624,7 @@ async function getBehavioralIndicators(context, deviceData) {
|
|
|
470
624
|
deviceData.rapidChangeCount = Math.min(
|
|
471
625
|
deviceData.rapidChangeCount + 1,
|
|
472
626
|
MAX_RAPID_CHANGES_PER_DEVICE * 2, // Increases quickly
|
|
473
|
-
);
|
|
627
|
+
);
|
|
474
628
|
} else {
|
|
475
629
|
deviceData.rapidChangeCount = Math.max(0, deviceData.rapidChangeCount - 1); // Decreases slowly
|
|
476
630
|
}
|
|
@@ -506,10 +660,10 @@ async function getBehavioralIndicators(context, deviceData) {
|
|
|
506
660
|
/**
|
|
507
661
|
* Returns a vector of raw (unweighted) suspicion scores.
|
|
508
662
|
* @param {object} context - The request context object.
|
|
509
|
-
* @returns {Promise<{historyScore: number, rotationScore: number, headerAnomalyScore: number, inconsistencyScore: number}>}
|
|
663
|
+
* @returns {Promise<{historyScore: number, rotationScore: number, headerAnomalyScore: number, inconsistencyScore: number, honeypotScore: number}>}
|
|
510
664
|
*/
|
|
511
|
-
export const getSuspicionVector = async (context) => {
|
|
512
|
-
|
|
665
|
+
export const getSuspicionVector = async (context, securityConfig) => {
|
|
666
|
+
const { deviceId, deviceData, consistencyScore, newCookie } = await resolveRequestIdentity(context);
|
|
513
667
|
|
|
514
668
|
const clientIp = context.clientIp;
|
|
515
669
|
|
|
@@ -531,10 +685,11 @@ export const getSuspicionVector = async (context) => {
|
|
|
531
685
|
const behavioral = await getBehavioralIndicators(context, deviceData);
|
|
532
686
|
const { headerAnomalyScore } = getHeaderAnomalies(context);
|
|
533
687
|
// Calculate the inconsistency score here, separately.
|
|
534
|
-
const inconsistencyScore = Math.min(100, Math.max(0, (1 - consistencyScore) * 200));
|
|
688
|
+
const inconsistencyScore = Math.min(100, Math.max(0, (1 - consistencyScore) * 200)); // Amplified score
|
|
535
689
|
|
|
536
690
|
|
|
537
691
|
// Save the updated device state to the store
|
|
692
|
+
// Note: deviceData.ips is a Set, which may not serialize correctly in all stores (e.g., JSON). A Redis store should handle this via custom serialization or by converting to an array.
|
|
538
693
|
await store.set(`device:${deviceId}`, deviceData);
|
|
539
694
|
|
|
540
695
|
return { ...behavioral, headerAnomalyScore, inconsistencyScore };
|
|
@@ -556,17 +711,20 @@ const MAX_RAPID_CHANGES_PER_DEVICE = 3; // Number of rapid fingerprint changes a
|
|
|
556
711
|
* Uses FingerprintBuilder to create a fingerprint based on headers
|
|
557
712
|
* and IP, making spoofing more complex (requires changing the entire stack).
|
|
558
713
|
*/
|
|
559
|
-
export const identifyRequest = async (req, res) => {
|
|
714
|
+
export const identifyRequest = (securityConfig) => async (req, res) => {
|
|
560
715
|
// This function now acts as a lightweight wrapper around the engine's identifyRequest method.
|
|
561
716
|
// It requires a default configuration to work.
|
|
562
|
-
const
|
|
563
|
-
weights: { historyScore: 0.3, rotationScore: 0.5, headerAnomalyScore: 0.1, inconsistencyScore: 0.8 },
|
|
564
|
-
thresholds: { low: 20, medium: 40, high: 75 }
|
|
717
|
+
const config = securityConfig || {
|
|
718
|
+
weights: { historyScore: 0.3, rotationScore: 0.5, headerAnomalyScore: 0.1, inconsistencyScore: 0.8, honeypotScore: 1.0 },
|
|
719
|
+
thresholds: { low: 20, medium: 40, high: 75 },
|
|
720
|
+
honeypot: { fields: [] } // Ensure honeypot config exists to prevent errors
|
|
565
721
|
};
|
|
566
|
-
const engine = new FingerprintEngine(
|
|
722
|
+
const engine = new FingerprintEngine(config);
|
|
567
723
|
|
|
568
724
|
const requestContext = {
|
|
569
725
|
clientIp: req.ip || req.socket?.remoteAddress || "unknown",
|
|
726
|
+
query: req.query,
|
|
727
|
+
body: req.body,
|
|
570
728
|
cookies: req.cookies,
|
|
571
729
|
headers: req.headers,
|
|
572
730
|
rawHeaders: req.rawHeaders,
|
|
@@ -673,7 +831,7 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
|
|
|
673
831
|
* @param {string} clientIp - The client's IP address.
|
|
674
832
|
* @returns {string} HTML content.
|
|
675
833
|
*/
|
|
676
|
-
function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp) {
|
|
834
|
+
function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret) {
|
|
677
835
|
const { nonce, target, path } = cpuChallengeDetails;
|
|
678
836
|
return `
|
|
679
837
|
<html><head><title>Advanced Security Check</title></head>
|
|
@@ -685,13 +843,14 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
685
843
|
async function solve() {
|
|
686
844
|
const nonce = "${nonce}";
|
|
687
845
|
const path = "${path}";
|
|
846
|
+
const clientSecret = "${clientSecret}"; // Secret is now available to the client
|
|
688
847
|
|
|
689
848
|
// --- CPU Challenge ---
|
|
690
849
|
document.getElementById('loader').innerText = '⚙️ Performing CPU security calculation...';
|
|
691
850
|
const cpuTarget = BigInt("0x${target}");
|
|
692
851
|
let cpuSolution = 0;
|
|
693
852
|
while (true) {
|
|
694
|
-
const msg = "${clientIp}:${nonce}:" + cpuSolution;
|
|
853
|
+
const msg = "${clientIp}:${nonce}:" + cpuSolution + ":" + clientSecret;
|
|
695
854
|
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg));
|
|
696
855
|
const hashHex = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
697
856
|
if (BigInt('0x' + hashHex) < cpuTarget) break;
|
|
@@ -706,12 +865,14 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
706
865
|
let memSolution = 0;
|
|
707
866
|
try {
|
|
708
867
|
const size = ${memoryDifficulty} * 1024 * 1024;
|
|
868
|
+
const iterations = size / 16;
|
|
709
869
|
const buffer = new Uint32Array(size / 4);
|
|
710
|
-
|
|
870
|
+
const seed = nonce + ":" + clientSecret;
|
|
871
|
+
let h = new TextEncoder().encode(seed).reduce((acc, v) => acc + v, 0);
|
|
711
872
|
for (let i = 0; i < buffer.length; i++) {
|
|
712
|
-
buffer[i] =
|
|
873
|
+
buffer[i] = h = Math.imul(h ^ i, 1597334677);
|
|
713
874
|
}
|
|
714
|
-
for(let i = 0; i <
|
|
875
|
+
for(let i = 0; i < iterations; i++) {
|
|
715
876
|
const addr = buffer[i % buffer.length] % buffer.length;
|
|
716
877
|
memSolution ^= buffer[addr];
|
|
717
878
|
}
|
|
@@ -734,11 +895,13 @@ export function verifyCpuTargetPoWAndGenerateTicket(
|
|
|
734
895
|
nonce,
|
|
735
896
|
solution,
|
|
736
897
|
suspicionFactor,
|
|
898
|
+
clientSecret, // Le secret est maintenant requis
|
|
737
899
|
) {
|
|
738
900
|
const target = calculateTarget(suspicionFactor);
|
|
901
|
+
const message = clientSecret ? `${clientIp}:${nonce}:${solution}:${clientSecret}` : `${clientIp}:${nonce}:${solution}`;
|
|
739
902
|
const hash = crypto
|
|
740
903
|
.createHash("sha256")
|
|
741
|
-
.update(
|
|
904
|
+
.update(message)
|
|
742
905
|
.digest("hex");
|
|
743
906
|
const hashAsInt = BigInt("0x" + hash);
|
|
744
907
|
|
|
@@ -747,7 +910,7 @@ export function verifyCpuTargetPoWAndGenerateTicket(
|
|
|
747
910
|
// The proof is valid, generate the ticket
|
|
748
911
|
const expiry = Date.now() + 3600000; // 1 heure
|
|
749
912
|
const signature = crypto
|
|
750
|
-
.createHmac("sha256",
|
|
913
|
+
.createHmac("sha256", getPowSecret())
|
|
751
914
|
.update(`${clientIp}:${expiry}`)
|
|
752
915
|
.digest("hex");
|
|
753
916
|
return `${expiry}:${signature}`;
|
|
@@ -756,9 +919,11 @@ export function verifyCpuTargetPoWAndGenerateTicket(
|
|
|
756
919
|
return null;
|
|
757
920
|
}
|
|
758
921
|
|
|
759
|
-
const staticExtensions =
|
|
760
|
-
|
|
761
|
-
|
|
922
|
+
const staticExtensions = new RegExp(
|
|
923
|
+
"\\.(js|css|png|jpg|jpeg|gif|svg|mp3|webp|ico|woff|woff2|ttf|otf|map)$",
|
|
924
|
+
"i",
|
|
925
|
+
);
|
|
926
|
+
const isStaticResource = (path) => staticExtensions.test(path);
|
|
762
927
|
|
|
763
928
|
// --- Middleware Proof-of-Work (Le péage) ---
|
|
764
929
|
class FingerprintEngine {
|
|
@@ -776,16 +941,27 @@ class FingerprintEngine {
|
|
|
776
941
|
return { action: 'next', score: 0, vector: {} };
|
|
777
942
|
}
|
|
778
943
|
|
|
944
|
+
// Check for persisted "condemned" status early.
|
|
945
|
+
const { deviceData } = await resolveRequestIdentity(requestContext);
|
|
946
|
+
if (deviceData?.condemned) {
|
|
947
|
+
return { action: 'block', status: 403, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
|
|
948
|
+
}
|
|
949
|
+
|
|
779
950
|
// The engine now works with the context directly, no more rawReq dependency here.
|
|
780
|
-
const suspicionVector = await __internal.getSuspicionVector(requestContext);
|
|
951
|
+
const suspicionVector = await __internal.getSuspicionVector(requestContext, this.securityConfig);
|
|
952
|
+
const { honeypotScore } = getHoneypotScore(requestContext, this.securityConfig.honeypot);
|
|
953
|
+
suspicionVector.honeypotScore = honeypotScore;
|
|
781
954
|
|
|
782
955
|
const finalScore =
|
|
783
956
|
suspicionVector.historyScore * (weights.historyScore || 0) +
|
|
784
957
|
suspicionVector.rotationScore * (weights.rotationScore || 0) +
|
|
785
958
|
suspicionVector.headerAnomalyScore * (weights.headerAnomalyScore || 0) +
|
|
786
|
-
suspicionVector.inconsistencyScore * (weights.inconsistencyScore || 0)
|
|
959
|
+
suspicionVector.inconsistencyScore * (weights.inconsistencyScore || 0) +
|
|
960
|
+
honeypotScore * (weights.honeypotScore || 0);
|
|
787
961
|
|
|
788
|
-
const
|
|
962
|
+
const isBlocked = finalScore >= (thresholds.block || 95);
|
|
963
|
+
|
|
964
|
+
const isSuspiciousHigh = finalScore >= thresholds.high && !isBlocked;
|
|
789
965
|
const isSuspiciousMedium = finalScore >= thresholds.medium;
|
|
790
966
|
const isSuspicious = finalScore >= thresholds.low;
|
|
791
967
|
|
|
@@ -796,13 +972,41 @@ class FingerprintEngine {
|
|
|
796
972
|
(finalScore - thresholds.low) / (thresholds.high - thresholds.low),
|
|
797
973
|
)
|
|
798
974
|
: 0;
|
|
799
|
-
const powCookie = cookies?.pow_clearance;
|
|
975
|
+
const powCookie = cookies?.pow_clearance;
|
|
800
976
|
const { pow_type, pow_nonce, pow_solution, pow_solution_cpu, pow_solution_mem } = query;
|
|
801
977
|
|
|
978
|
+
// Honeypot: Direct probing of challenge endpoints is highly suspicious.
|
|
979
|
+
// A legitimate user only hits these endpoints via the challenge page itself.
|
|
980
|
+
if (pow_nonce && !isSuspicious) {
|
|
981
|
+
if (logger) {
|
|
982
|
+
logger({ type: 'honeypot_probe', deviceId: cookies?.device_id, score: finalScore, path: path, timestamp: Date.now() });
|
|
983
|
+
}
|
|
984
|
+
suspicionVector.honeypotScore = 100; // Bot is probing. Max penalty.
|
|
985
|
+
// Recalculate score and block immediately.
|
|
986
|
+
const newFinalScore = finalScore + (100 * (weights.honeypotScore || 0));
|
|
987
|
+
return { action: 'block', status: 403, body: 'Forbidden', score: newFinalScore, vector: suspicionVector };
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// If the action is to block, we should still include the score and vector for logging/testing.
|
|
991
|
+
if (isBlocked) {
|
|
992
|
+
return { action: 'block', status: 403, body: 'Forbidden', score: finalScore, vector: suspicionVector };
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Honeypot: Check if the request is for a trap URL generated in a previous challenge.
|
|
996
|
+
// This requires a nonce from a *previous* challenge, which we can look up via the device ID.
|
|
997
|
+
const lastNonce = deviceData?.lastChallengeNonce;
|
|
998
|
+
if (lastNonce && query.sig && verifyTrapUrl(path, query.sig, lastNonce)) {
|
|
999
|
+
deviceData.condemned = true; // This device is a bot. Condemn it.
|
|
1000
|
+
await store.set(`device:${cookies.device_id}`, deviceData);
|
|
1001
|
+
return { action: 'block', status: 403, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
|
|
1002
|
+
}
|
|
1003
|
+
|
|
802
1004
|
if (isSuspicious && !isTicketValid(clientIp, powCookie)) {
|
|
803
1005
|
// --- CHALLENGE SOLUTION HANDLING ---
|
|
804
1006
|
if (pow_nonce && (pow_solution || (pow_solution_cpu && pow_solution_mem))) {
|
|
805
1007
|
let isValid = false,
|
|
1008
|
+
// Retrieve the client-side secret associated with this nonce
|
|
1009
|
+
clientSecret = await store.get(`secret:${pow_nonce}`),
|
|
806
1010
|
ticket = null;
|
|
807
1011
|
if (pow_type === "cpu_target") {
|
|
808
1012
|
// Verify the new type
|
|
@@ -810,25 +1014,20 @@ class FingerprintEngine {
|
|
|
810
1014
|
clientIp,
|
|
811
1015
|
pow_nonce,
|
|
812
1016
|
pow_solution,
|
|
813
|
-
suspicionFactor, // Pass the analog factor
|
|
1017
|
+
suspicionFactor, // Pass the analog factor
|
|
1018
|
+
clientSecret,
|
|
814
1019
|
);
|
|
815
|
-
isValid = ticket !== null;
|
|
816
|
-
} else if (pow_type === "mem") {
|
|
817
|
-
const minDifficulty = 16; // 16Mo
|
|
818
|
-
const maxDifficulty = 48; // 48Mo
|
|
819
|
-
const difficulty =
|
|
820
|
-
minDifficulty + suspicionFactor * (maxDifficulty - minDifficulty);
|
|
821
|
-
isValid = verifyMemoryPoW(pow_nonce, pow_solution, difficulty);
|
|
822
|
-
} else if (pow_type === "cpu_mem") {
|
|
1020
|
+
isValid = ticket !== null; } else if (pow_type === "cpu_mem") {
|
|
823
1021
|
// Verify combined challenge
|
|
824
1022
|
const cpuTicket = verifyCpuTargetPoWAndGenerateTicket(
|
|
825
|
-
clientIp, pow_nonce, pow_solution_cpu, suspicionFactor
|
|
1023
|
+
clientIp, pow_nonce, pow_solution_cpu, suspicionFactor, clientSecret
|
|
826
1024
|
);
|
|
827
|
-
|
|
828
1025
|
const minDifficulty = 16; // 16Mo
|
|
829
1026
|
const maxDifficulty = 48; // 48Mo
|
|
830
|
-
const
|
|
831
|
-
const
|
|
1027
|
+
const memActivationFactor = Math.max(0, (suspicionFactor - 0.25) / 0.75);
|
|
1028
|
+
const memDifficulty = Math.round(minDifficulty + memActivationFactor * (maxDifficulty - minDifficulty));
|
|
1029
|
+
|
|
1030
|
+
const isMemValid = verifyMemoryPoW(pow_nonce, pow_solution_mem, memDifficulty, clientSecret);
|
|
832
1031
|
|
|
833
1032
|
isValid = cpuTicket !== null && isMemValid;
|
|
834
1033
|
if (isValid) ticket = cpuTicket; // Reuse the ticket generated by the CPU verification
|
|
@@ -838,11 +1037,16 @@ class FingerprintEngine {
|
|
|
838
1037
|
}
|
|
839
1038
|
|
|
840
1039
|
if (isValid) {
|
|
1040
|
+
// The secret has been used, delete it to prevent replay.
|
|
1041
|
+
if (clientSecret) {
|
|
1042
|
+
await store.delete(`secret:${pow_nonce}`);
|
|
1043
|
+
}
|
|
1044
|
+
|
|
841
1045
|
if (!ticket) {
|
|
842
1046
|
// If the ticket has not already been generated (CPU case)
|
|
843
1047
|
const expiry = Date.now() + 3600000; // 1 heure
|
|
844
1048
|
const signature = crypto
|
|
845
|
-
.createHmac("sha256",
|
|
1049
|
+
.createHmac("sha256", getPowSecret())
|
|
846
1050
|
.update(`${clientIp}:${expiry}`)
|
|
847
1051
|
.digest("hex");
|
|
848
1052
|
ticket = `${expiry}:${signature}`;
|
|
@@ -872,6 +1076,16 @@ class FingerprintEngine {
|
|
|
872
1076
|
|
|
873
1077
|
// --- SELECTION AND SENDING OF THE APPROPRIATE CHALLENGE ---
|
|
874
1078
|
const nonce = crypto.randomBytes(16).toString("hex");
|
|
1079
|
+
const clientSecret = crypto.randomBytes(16).toString("hex");
|
|
1080
|
+
|
|
1081
|
+
// Store the secret with a short TTL (e.g., 5 minutes)
|
|
1082
|
+
await store.set(`secret:${nonce}`, clientSecret, 300);
|
|
1083
|
+
|
|
1084
|
+
// Associate the current challenge nonce with the device for trap URL verification later.
|
|
1085
|
+
if (deviceData) {
|
|
1086
|
+
deviceData.lastChallengeNonce = nonce;
|
|
1087
|
+
await store.set(`device:${cookies.device_id}`, deviceData);
|
|
1088
|
+
}
|
|
875
1089
|
|
|
876
1090
|
if (logger) {
|
|
877
1091
|
logger({ type: 'challenge_issued', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now() });
|
|
@@ -882,24 +1096,14 @@ class FingerprintEngine {
|
|
|
882
1096
|
// ... logic for TSP/Captcha challenge
|
|
883
1097
|
}
|
|
884
1098
|
|
|
885
|
-
//
|
|
886
|
-
if (isSuspiciousMedium) {
|
|
887
|
-
// Utilisons notre nouveau challenge combiné !
|
|
888
|
-
const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path);
|
|
889
|
-
|
|
890
|
-
const minMemDifficulty = 16; // 16Mo
|
|
891
|
-
const maxMemDifficulty = 48; // 48Mo
|
|
892
|
-
const memDifficulty = minMemDifficulty + suspicionFactor * (maxMemDifficulty - minMemDifficulty);
|
|
893
|
-
|
|
894
|
-
const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp);
|
|
895
|
-
return {
|
|
896
|
-
action: 'challenge', score: finalScore, vector: suspicionVector,
|
|
897
|
-
status: 429, body: page
|
|
898
|
-
};
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
// NOUVELLE LOGIQUE UNIFIÉE POUR TOUS LES NIVEAUX DE SUSPICION (low et medium)
|
|
1099
|
+
// UNIFIED CHALLENGE LOGIC for all suspicion levels (low, medium, high)
|
|
902
1100
|
if (isSuspicious) { // Couvre à la fois low et medium
|
|
1101
|
+
// Generate some trap URLs to embed in the challenge page.
|
|
1102
|
+
// These links are visually hidden but present in the DOM to trap bots.
|
|
1103
|
+
const trapUrls = Array.from({ length: 3 }, () => generateTrapUrl(nonce));
|
|
1104
|
+
const trapLinksHtml = trapUrls.map(url => `<a href="${url}" tabindex="-1">config</a>`).join(' ');
|
|
1105
|
+
|
|
1106
|
+
|
|
903
1107
|
const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path);
|
|
904
1108
|
|
|
905
1109
|
// La difficulté mémoire démarre à 0 et augmente seulement après un certain seuil de suspicion.
|
|
@@ -908,10 +1112,11 @@ class FingerprintEngine {
|
|
|
908
1112
|
|
|
909
1113
|
const minMemDifficulty = 0; // Peut être 0 Mo !
|
|
910
1114
|
const maxMemDifficulty = 48; // 48Mo pour les plus suspects
|
|
911
|
-
const memDifficulty = minMemDifficulty + memActivationFactor * (maxMemDifficulty - minMemDifficulty);
|
|
1115
|
+
const memDifficulty = Math.round(minMemDifficulty + memActivationFactor * (maxMemDifficulty - minMemDifficulty));
|
|
912
1116
|
|
|
913
|
-
//
|
|
914
|
-
const
|
|
1117
|
+
// Always use the combined page, even if memory difficulty is 0 (it will be almost instant).
|
|
1118
|
+
const trapContainer = `<div style="position:absolute;left:-9999px;top:-9999px;" aria-hidden="true">${trapLinksHtml}</div>`;
|
|
1119
|
+
const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp, clientSecret).replace('</body>', `${trapContainer}</body>`);
|
|
915
1120
|
return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 429, body: page };
|
|
916
1121
|
}
|
|
917
1122
|
}
|
|
@@ -956,12 +1161,14 @@ class FingerprintEngine {
|
|
|
956
1161
|
}
|
|
957
1162
|
await store.set(`ip:${clientIp}`, ipProfile);
|
|
958
1163
|
|
|
959
|
-
const vector = await __internal.getSuspicionVector(requestContext);
|
|
1164
|
+
const vector = await __internal.getSuspicionVector(requestContext, this.securityConfig); // Pass the config
|
|
1165
|
+
const { honeypotScore } = getHoneypotScore(requestContext, this.securityConfig.honeypot);
|
|
960
1166
|
const score =
|
|
961
1167
|
vector.historyScore * (this.securityConfig.weights.historyScore || 0.3) +
|
|
962
1168
|
vector.rotationScore * (this.securityConfig.weights.rotationScore || 0.5) +
|
|
963
1169
|
vector.headerAnomalyScore * (this.securityConfig.weights.headerAnomalyScore || 0.1) +
|
|
964
|
-
vector.inconsistencyScore * (this.securityConfig.weights.inconsistencyScore || 0.8)
|
|
1170
|
+
vector.inconsistencyScore * (this.securityConfig.weights.inconsistencyScore || 0.8) +
|
|
1171
|
+
honeypotScore * (this.securityConfig.weights.honeypotScore || 0);
|
|
965
1172
|
|
|
966
1173
|
if (score >= this.securityConfig.thresholds.high) return `suspicious_high:${clientIp}`;
|
|
967
1174
|
if (score >= this.securityConfig.thresholds.low) return `suspicious_medium:${clientIp}`; // Use medium for any suspicion
|
|
@@ -979,14 +1186,22 @@ class FingerprintEngine {
|
|
|
979
1186
|
export const powMiddleware = (securityConfig) => {
|
|
980
1187
|
const engine = new FingerprintEngine(securityConfig);
|
|
981
1188
|
|
|
1189
|
+
if (securityConfig.autotuning) {
|
|
1190
|
+
startThresholdAutoTuning({
|
|
1191
|
+
securityConfig: securityConfig,
|
|
1192
|
+
...securityConfig.autotuning,
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
|
|
982
1196
|
return async (req, res, next) => {
|
|
983
1197
|
const requestContext = {
|
|
984
1198
|
clientIp: req.ip || req.socket?.remoteAddress || "unknown",
|
|
985
1199
|
path: req.path,
|
|
986
1200
|
cookies: req.cookies,
|
|
987
1201
|
query: req.query,
|
|
1202
|
+
body: req.body,
|
|
988
1203
|
headers: req.headers,
|
|
989
|
-
isStatic: isStaticResource(req),
|
|
1204
|
+
isStatic: isStaticResource(req.path),
|
|
990
1205
|
// Add the newly required properties for full decoupling
|
|
991
1206
|
rawHeaders: req.rawHeaders,
|
|
992
1207
|
httpVersion: req.httpVersion,
|
package/package.json
CHANGED
|
@@ -1,47 +1,48 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@anonympins/fingerprint",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "An HTTP(S) client mitigation and anti-bot protection library for Node.js/Express, based on digital fingerprinting and dynamic Proof-of-Work (PoW) challenges.",
|
|
5
|
-
"main": "fingerprint.js",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"engines": {
|
|
8
|
-
"node": ">=20.0.0"
|
|
9
|
-
},
|
|
10
|
-
"scripts": {
|
|
11
|
-
"test": "vitest run"
|
|
12
|
-
},
|
|
13
|
-
"files": [
|
|
14
|
-
"fingerprint.js",
|
|
15
|
-
"library.js",
|
|
16
|
-
"README.md",
|
|
17
|
-
"LICENSE"
|
|
18
|
-
],
|
|
19
|
-
"repository": {
|
|
20
|
-
"type": "git",
|
|
21
|
-
"url": "git+https://github.com/anonympins/fingerprint.git"
|
|
22
|
-
},
|
|
23
|
-
"keywords": [
|
|
24
|
-
"fingerprint",
|
|
25
|
-
"bot",
|
|
26
|
-
"anti-bot",
|
|
27
|
-
"security",
|
|
28
|
-
"express",
|
|
29
|
-
"middleware",
|
|
30
|
-
"proof-of-work",
|
|
31
|
-
"pow",
|
|
32
|
-
"rate-limiting",
|
|
33
|
-
"mitigation",
|
|
34
|
-
"captcha"
|
|
35
|
-
],
|
|
36
|
-
"author": "anonympins",
|
|
37
|
-
"license": "MIT",
|
|
38
|
-
"bugs": {
|
|
39
|
-
"url": "https://github.com/anonympins/fingerprint/issues"
|
|
40
|
-
},
|
|
41
|
-
"homepage": "https://github.com/anonympins/fingerprint#readme",
|
|
42
|
-
"devDependencies": {
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@anonympins/fingerprint",
|
|
3
|
+
"version": "0.0.5",
|
|
4
|
+
"description": "An HTTP(S) client mitigation and anti-bot protection library for Node.js/Express, based on digital fingerprinting and dynamic Proof-of-Work (PoW) challenges.",
|
|
5
|
+
"main": "fingerprint.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20.0.0"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "vitest run"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"fingerprint.js",
|
|
15
|
+
"library.js",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/anonympins/fingerprint.git"
|
|
22
|
+
},
|
|
23
|
+
"keywords": [
|
|
24
|
+
"fingerprint",
|
|
25
|
+
"bot",
|
|
26
|
+
"anti-bot",
|
|
27
|
+
"security",
|
|
28
|
+
"express",
|
|
29
|
+
"middleware",
|
|
30
|
+
"proof-of-work",
|
|
31
|
+
"pow",
|
|
32
|
+
"rate-limiting",
|
|
33
|
+
"mitigation",
|
|
34
|
+
"captcha"
|
|
35
|
+
],
|
|
36
|
+
"author": "anonympins",
|
|
37
|
+
"license": "MIT",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/anonympins/fingerprint/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/anonympins/fingerprint#readme",
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"body-parser": "^1.20.2",
|
|
44
|
+
"cookie-parser": "^1.4.6",
|
|
45
|
+
"express": "^4.18.2",
|
|
46
|
+
"vitest": "^4.1.11"
|
|
47
|
+
}
|
|
48
|
+
}
|