@anonympins/fingerprint 0.0.3 → 0.0.4
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 +14 -19
- package/fingerprint.js +104 -61
- package/package.json +47 -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.
|
|
@@ -30,7 +30,7 @@ Once the challenge is solved, a clearance "ticket" is issued via a secure cookie
|
|
|
30
30
|
- **Multi-Factor Fingerprinting**: Combines client-side data (`hardwareConcurrency`, `deviceMemory`, `screen`, `canvas`, `webgl`) and server-side data (`User-Agent`, `Client-Hints`).
|
|
31
31
|
- **Secure Ticket System**: Uses HMAC-SHA256 signatures to validate clearances and prevent tampering.
|
|
32
32
|
- **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`.
|
|
33
|
+
- **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
34
|
- **Timing Attack Protection**: Uses `crypto.timingSafeEqual` for secure ticket validation.
|
|
35
35
|
- **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
36
|
|
|
@@ -75,7 +75,8 @@ const securityConfig = {
|
|
|
75
75
|
low: 20, // Score from which a CPU challenge is issued
|
|
76
76
|
medium: 45, // Score for a more difficult combined CPU/Memory challenge
|
|
77
77
|
high: 75, // Score for a very difficult challenge
|
|
78
|
-
block: 95
|
|
78
|
+
block: 95, // Score above which the request is blocked outright (HTTP 403)
|
|
79
|
+
isStaticResource: (req) => req.path.startsWith('/static/') // Optional: Custom function to identify static resources
|
|
79
80
|
}
|
|
80
81
|
};
|
|
81
82
|
|
|
@@ -222,9 +223,8 @@ const server = http.createServer(async (req, res) => {
|
|
|
222
223
|
clientIp: req.socket.remoteAddress,
|
|
223
224
|
path: req.url.split('?')[0],
|
|
224
225
|
cookies: {}, // Parse cookies from req.headers.cookie
|
|
225
|
-
query:
|
|
226
|
+
query: new URL(req.url, `http://${req.headers.host}`).searchParams,
|
|
226
227
|
headers: req.headers,
|
|
227
|
-
isStatic: /\.(js|css|png)$/.test(req.url),
|
|
228
228
|
rawReq: req, // Pass the raw request
|
|
229
229
|
rawRes: res, // Pass the raw response for cookie setting
|
|
230
230
|
};
|
|
@@ -261,35 +261,30 @@ Manually setting the `low`, `medium`, and `high` thresholds can be challenging.
|
|
|
261
261
|
|
|
262
262
|
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
263
|
|
|
264
|
-
2. **
|
|
264
|
+
2. **Enable Auto-tuning**: Add an `autotuning` property to your security configuration. The middleware will automatically start the tuning process.
|
|
265
265
|
|
|
266
266
|
```javascript
|
|
267
|
-
import { powMiddleware
|
|
267
|
+
import { powMiddleware } from './fingerprint.js';
|
|
268
268
|
|
|
269
269
|
// Array to store traffic analysis data. In a real application, this could be
|
|
270
270
|
// a more robust logging system.
|
|
271
271
|
const trafficData = [];
|
|
272
272
|
|
|
273
273
|
const securityConfig = {
|
|
274
|
-
weights: { /* ...
|
|
274
|
+
weights: { /* ... */ },
|
|
275
275
|
thresholds: {
|
|
276
276
|
low: 20, // Initial values, will be optimized
|
|
277
277
|
medium: 45,
|
|
278
278
|
high: 75
|
|
279
279
|
},
|
|
280
|
-
// The logger is required for auto-tuning
|
|
281
|
-
|
|
280
|
+
logger: (log) => trafficData.push(log), // The logger is required for auto-tuning
|
|
281
|
+
autotune: {
|
|
282
|
+
trafficData: trafficData, // The data source for the algorithm
|
|
283
|
+
interval: 1800000, // Optimization cycle every 30 minutes (optional)
|
|
284
|
+
minDataPoints: 200 // Minimum requests before starting optimization (optional)
|
|
285
|
+
}
|
|
282
286
|
};
|
|
283
287
|
|
|
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
288
|
const powMiddlewareInstance = powMiddleware(securityConfig);
|
|
294
289
|
app.use(powMiddlewareInstance);
|
|
295
290
|
```
|
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
|
+
};
|
|
8
17
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
+
}
|
|
39
|
+
|
|
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
|
|
|
@@ -369,7 +408,7 @@ function getHeaderAnomalies(context) {
|
|
|
369
408
|
|
|
370
409
|
/**
|
|
371
410
|
* Default in-memory store implementation.
|
|
372
|
-
* @type {IStore}
|
|
411
|
+
* @type {IStore}
|
|
373
412
|
*/
|
|
374
413
|
const inMemoryStore = {
|
|
375
414
|
_map: new Map(),
|
|
@@ -395,7 +434,7 @@ export const configureStore = (externalStore) => {
|
|
|
395
434
|
* Orchestrates request identification using a persistent anchor (cookie)
|
|
396
435
|
* and fingerprint verification.
|
|
397
436
|
* @param {object} context - The request context.
|
|
398
|
-
* @returns {Promise<{deviceId: string, deviceData: object, consistencyScore: number, newCookie: object|null}>}
|
|
437
|
+
* @returns {Promise<{deviceId: string, deviceData: object, consistencyScore: number, newCookie: object|null}>}
|
|
399
438
|
*/
|
|
400
439
|
async function resolveRequestIdentity(context) {
|
|
401
440
|
const existingDeviceId = context.cookies?.device_id;
|
|
@@ -470,7 +509,7 @@ async function getBehavioralIndicators(context, deviceData) {
|
|
|
470
509
|
deviceData.rapidChangeCount = Math.min(
|
|
471
510
|
deviceData.rapidChangeCount + 1,
|
|
472
511
|
MAX_RAPID_CHANGES_PER_DEVICE * 2, // Increases quickly
|
|
473
|
-
);
|
|
512
|
+
);
|
|
474
513
|
} else {
|
|
475
514
|
deviceData.rapidChangeCount = Math.max(0, deviceData.rapidChangeCount - 1); // Decreases slowly
|
|
476
515
|
}
|
|
@@ -673,7 +712,7 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
|
|
|
673
712
|
* @param {string} clientIp - The client's IP address.
|
|
674
713
|
* @returns {string} HTML content.
|
|
675
714
|
*/
|
|
676
|
-
function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp) {
|
|
715
|
+
function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret) {
|
|
677
716
|
const { nonce, target, path } = cpuChallengeDetails;
|
|
678
717
|
return `
|
|
679
718
|
<html><head><title>Advanced Security Check</title></head>
|
|
@@ -685,13 +724,14 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
685
724
|
async function solve() {
|
|
686
725
|
const nonce = "${nonce}";
|
|
687
726
|
const path = "${path}";
|
|
727
|
+
const clientSecret = "${clientSecret}"; // Secret is now available to the client
|
|
688
728
|
|
|
689
729
|
// --- CPU Challenge ---
|
|
690
730
|
document.getElementById('loader').innerText = '⚙️ Performing CPU security calculation...';
|
|
691
731
|
const cpuTarget = BigInt("0x${target}");
|
|
692
732
|
let cpuSolution = 0;
|
|
693
733
|
while (true) {
|
|
694
|
-
const msg = "${clientIp}:${nonce}:" + cpuSolution;
|
|
734
|
+
const msg = "${clientIp}:${nonce}:" + cpuSolution + ":" + clientSecret;
|
|
695
735
|
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg));
|
|
696
736
|
const hashHex = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
697
737
|
if (BigInt('0x' + hashHex) < cpuTarget) break;
|
|
@@ -704,14 +744,16 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
704
744
|
await new Promise(r => setTimeout(r, 10)); // Yield to update UI
|
|
705
745
|
|
|
706
746
|
let memSolution = 0;
|
|
747
|
+
const iterations = size / 16;
|
|
707
748
|
try {
|
|
708
749
|
const size = ${memoryDifficulty} * 1024 * 1024;
|
|
709
750
|
const buffer = new Uint32Array(size / 4);
|
|
710
|
-
|
|
751
|
+
const seed = nonce + ":" + clientSecret;
|
|
752
|
+
let h = new TextEncoder().encode(seed).reduce((acc, v) => acc + v, 0);
|
|
711
753
|
for (let i = 0; i < buffer.length; i++) {
|
|
712
|
-
buffer[i] =
|
|
754
|
+
buffer[i] = h = Math.imul(h ^ i, 1597334677);
|
|
713
755
|
}
|
|
714
|
-
for(let i = 0; i <
|
|
756
|
+
for(let i = 0; i < iterations; i++) {
|
|
715
757
|
const addr = buffer[i % buffer.length] % buffer.length;
|
|
716
758
|
memSolution ^= buffer[addr];
|
|
717
759
|
}
|
|
@@ -734,11 +776,13 @@ export function verifyCpuTargetPoWAndGenerateTicket(
|
|
|
734
776
|
nonce,
|
|
735
777
|
solution,
|
|
736
778
|
suspicionFactor,
|
|
779
|
+
clientSecret, // Le secret est maintenant requis
|
|
737
780
|
) {
|
|
738
781
|
const target = calculateTarget(suspicionFactor);
|
|
782
|
+
const message = clientSecret ? `${clientIp}:${nonce}:${solution}:${clientSecret}` : `${clientIp}:${nonce}:${solution}`;
|
|
739
783
|
const hash = crypto
|
|
740
784
|
.createHash("sha256")
|
|
741
|
-
.update(
|
|
785
|
+
.update(message)
|
|
742
786
|
.digest("hex");
|
|
743
787
|
const hashAsInt = BigInt("0x" + hash);
|
|
744
788
|
|
|
@@ -747,7 +791,7 @@ export function verifyCpuTargetPoWAndGenerateTicket(
|
|
|
747
791
|
// The proof is valid, generate the ticket
|
|
748
792
|
const expiry = Date.now() + 3600000; // 1 heure
|
|
749
793
|
const signature = crypto
|
|
750
|
-
.createHmac("sha256",
|
|
794
|
+
.createHmac("sha256", getPowSecret())
|
|
751
795
|
.update(`${clientIp}:${expiry}`)
|
|
752
796
|
.digest("hex");
|
|
753
797
|
return `${expiry}:${signature}`;
|
|
@@ -756,9 +800,11 @@ export function verifyCpuTargetPoWAndGenerateTicket(
|
|
|
756
800
|
return null;
|
|
757
801
|
}
|
|
758
802
|
|
|
759
|
-
const staticExtensions =
|
|
760
|
-
|
|
761
|
-
|
|
803
|
+
const staticExtensions = new RegExp(
|
|
804
|
+
"\\.(js|css|png|jpg|jpeg|gif|svg|mp3|webp|ico|woff|woff2|ttf|otf|map)$",
|
|
805
|
+
"i",
|
|
806
|
+
);
|
|
807
|
+
const isStaticResource = (path) => staticExtensions.test(path);
|
|
762
808
|
|
|
763
809
|
// --- Middleware Proof-of-Work (Le péage) ---
|
|
764
810
|
class FingerprintEngine {
|
|
@@ -796,13 +842,15 @@ class FingerprintEngine {
|
|
|
796
842
|
(finalScore - thresholds.low) / (thresholds.high - thresholds.low),
|
|
797
843
|
)
|
|
798
844
|
: 0;
|
|
799
|
-
const powCookie = cookies?.pow_clearance;
|
|
845
|
+
const powCookie = cookies?.pow_clearance;
|
|
800
846
|
const { pow_type, pow_nonce, pow_solution, pow_solution_cpu, pow_solution_mem } = query;
|
|
801
847
|
|
|
802
848
|
if (isSuspicious && !isTicketValid(clientIp, powCookie)) {
|
|
803
849
|
// --- CHALLENGE SOLUTION HANDLING ---
|
|
804
850
|
if (pow_nonce && (pow_solution || (pow_solution_cpu && pow_solution_mem))) {
|
|
805
851
|
let isValid = false,
|
|
852
|
+
// Retrieve the client-side secret associated with this nonce
|
|
853
|
+
clientSecret = await store.get(`secret:${pow_nonce}`),
|
|
806
854
|
ticket = null;
|
|
807
855
|
if (pow_type === "cpu_target") {
|
|
808
856
|
// Verify the new type
|
|
@@ -810,25 +858,20 @@ class FingerprintEngine {
|
|
|
810
858
|
clientIp,
|
|
811
859
|
pow_nonce,
|
|
812
860
|
pow_solution,
|
|
813
|
-
suspicionFactor, // Pass the analog factor
|
|
861
|
+
suspicionFactor, // Pass the analog factor
|
|
862
|
+
clientSecret,
|
|
814
863
|
);
|
|
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") {
|
|
864
|
+
isValid = ticket !== null; } else if (pow_type === "cpu_mem") {
|
|
823
865
|
// Verify combined challenge
|
|
824
866
|
const cpuTicket = verifyCpuTargetPoWAndGenerateTicket(
|
|
825
|
-
clientIp, pow_nonce, pow_solution_cpu, suspicionFactor
|
|
867
|
+
clientIp, pow_nonce, pow_solution_cpu, suspicionFactor, clientSecret
|
|
826
868
|
);
|
|
827
|
-
|
|
828
869
|
const minDifficulty = 16; // 16Mo
|
|
829
870
|
const maxDifficulty = 48; // 48Mo
|
|
830
|
-
const
|
|
831
|
-
const
|
|
871
|
+
const memActivationFactor = Math.max(0, (suspicionFactor - 0.25) / 0.75);
|
|
872
|
+
const memDifficulty = Math.round(minDifficulty + memActivationFactor * (maxDifficulty - minDifficulty));
|
|
873
|
+
|
|
874
|
+
const isMemValid = verifyMemoryPoW(pow_nonce, pow_solution_mem, memDifficulty, clientSecret);
|
|
832
875
|
|
|
833
876
|
isValid = cpuTicket !== null && isMemValid;
|
|
834
877
|
if (isValid) ticket = cpuTicket; // Reuse the ticket generated by the CPU verification
|
|
@@ -838,11 +881,16 @@ class FingerprintEngine {
|
|
|
838
881
|
}
|
|
839
882
|
|
|
840
883
|
if (isValid) {
|
|
884
|
+
// The secret has been used, delete it to prevent replay.
|
|
885
|
+
if (clientSecret) {
|
|
886
|
+
await store.delete(`secret:${pow_nonce}`);
|
|
887
|
+
}
|
|
888
|
+
|
|
841
889
|
if (!ticket) {
|
|
842
890
|
// If the ticket has not already been generated (CPU case)
|
|
843
891
|
const expiry = Date.now() + 3600000; // 1 heure
|
|
844
892
|
const signature = crypto
|
|
845
|
-
.createHmac("sha256",
|
|
893
|
+
.createHmac("sha256", getPowSecret())
|
|
846
894
|
.update(`${clientIp}:${expiry}`)
|
|
847
895
|
.digest("hex");
|
|
848
896
|
ticket = `${expiry}:${signature}`;
|
|
@@ -872,6 +920,10 @@ class FingerprintEngine {
|
|
|
872
920
|
|
|
873
921
|
// --- SELECTION AND SENDING OF THE APPROPRIATE CHALLENGE ---
|
|
874
922
|
const nonce = crypto.randomBytes(16).toString("hex");
|
|
923
|
+
const clientSecret = crypto.randomBytes(16).toString("hex");
|
|
924
|
+
|
|
925
|
+
// Store the secret with a short TTL (e.g., 5 minutes)
|
|
926
|
+
await store.set(`secret:${nonce}`, clientSecret, 300);
|
|
875
927
|
|
|
876
928
|
if (logger) {
|
|
877
929
|
logger({ type: 'challenge_issued', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now() });
|
|
@@ -882,23 +934,7 @@ class FingerprintEngine {
|
|
|
882
934
|
// ... logic for TSP/Captcha challenge
|
|
883
935
|
}
|
|
884
936
|
|
|
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)
|
|
937
|
+
// UNIFIED CHALLENGE LOGIC for all suspicion levels (low, medium, high)
|
|
902
938
|
if (isSuspicious) { // Couvre à la fois low et medium
|
|
903
939
|
const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path);
|
|
904
940
|
|
|
@@ -908,10 +944,10 @@ class FingerprintEngine {
|
|
|
908
944
|
|
|
909
945
|
const minMemDifficulty = 0; // Peut être 0 Mo !
|
|
910
946
|
const maxMemDifficulty = 48; // 48Mo pour les plus suspects
|
|
911
|
-
const memDifficulty = minMemDifficulty + memActivationFactor * (maxMemDifficulty - minMemDifficulty);
|
|
947
|
+
const memDifficulty = Math.round(minMemDifficulty + memActivationFactor * (maxMemDifficulty - minMemDifficulty));
|
|
912
948
|
|
|
913
|
-
//
|
|
914
|
-
const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp);
|
|
949
|
+
// Always use the combined page, even if memory difficulty is 0 (it will be almost instant).
|
|
950
|
+
const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp, clientSecret);
|
|
915
951
|
return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 429, body: page };
|
|
916
952
|
}
|
|
917
953
|
}
|
|
@@ -979,6 +1015,13 @@ class FingerprintEngine {
|
|
|
979
1015
|
export const powMiddleware = (securityConfig) => {
|
|
980
1016
|
const engine = new FingerprintEngine(securityConfig);
|
|
981
1017
|
|
|
1018
|
+
if (securityConfig.autotuning) {
|
|
1019
|
+
startThresholdAutoTuning({
|
|
1020
|
+
securityConfig: securityConfig,
|
|
1021
|
+
...securityConfig.autotuning,
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
|
|
982
1025
|
return async (req, res, next) => {
|
|
983
1026
|
const requestContext = {
|
|
984
1027
|
clientIp: req.ip || req.socket?.remoteAddress || "unknown",
|
|
@@ -986,7 +1029,7 @@ export const powMiddleware = (securityConfig) => {
|
|
|
986
1029
|
cookies: req.cookies,
|
|
987
1030
|
query: req.query,
|
|
988
1031
|
headers: req.headers,
|
|
989
|
-
isStatic: isStaticResource(req),
|
|
1032
|
+
isStatic: isStaticResource(req.path),
|
|
990
1033
|
// Add the newly required properties for full decoupling
|
|
991
1034
|
rawHeaders: req.rawHeaders,
|
|
992
1035
|
httpVersion: req.httpVersion,
|
package/package.json
CHANGED
|
@@ -1,47 +1,47 @@
|
|
|
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
|
-
"cookie-parser": "^1.4.6",
|
|
44
|
-
"express": "^4.18.2",
|
|
45
|
-
"vitest": "^4.1.11"
|
|
46
|
-
}
|
|
47
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@anonympins/fingerprint",
|
|
3
|
+
"version": "0.0.4",
|
|
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
|
+
"cookie-parser": "^1.4.6",
|
|
44
|
+
"express": "^4.18.2",
|
|
45
|
+
"vitest": "^4.1.11"
|
|
46
|
+
}
|
|
47
|
+
}
|