@anonympins/fingerprint 0.7.5 → 0.8.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/CHANGELOG.md +57 -0
- package/README.md +1 -1
- package/package.json +3 -2
- package/public/anonympins-bot-mitigation-pow.zip +0 -0
- package/src/js/asn-lookup.js +223 -0
- package/src/js/fingerprint.client.js +1784 -1772
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +236 -40
- package/src/js/fingerprint.utils.js +252 -2
- package/src/js/pow.worker.js +2 -2
- package/src/js/tests/asn-lookup.test.js +57 -0
- package/src/js/tests/cross_parity.test.js +3 -3
- package/src/js/tests/dns-circuit-breaker.test.js +6 -6
- package/src/js/tests/fingerprint.builder.test.js +76 -78
- package/src/js/tests/fingerprint.client.test.js +16 -17
- package/src/js/tests/fingerprint.test.js +19 -12
- package/src/js/tests/ja3AnomalyDetector.test.js +135 -136
- package/src/js/tests/library.test.js +94 -95
- package/src/js/tests/obfuscation.test.js +8 -8
- package/src/js/tests/quicFingerprint.test.js +47 -0
- package/src/js/tests/tcpFingerprint.test.js +4 -4
- package/src/js/upow-model-task.js +155 -112
- package/src/php/AsnLookupEngine.php +173 -0
- package/src/php/Challenge/ChallengeUtils.php +430 -117
- package/src/php/Config/SecurityProfiles.php +75 -62
- package/src/php/DirectFingerprint.php +80 -13
- package/src/php/FingerprintBuilder.php +13 -13
- package/src/php/FingerprintClient.php +6 -6
- package/src/php/FingerprintEngine.php +2141 -1837
- package/src/php/Ja3AnomalyDetector.php +22 -22
- package/src/php/NetworkProfile.php +68 -0
- package/src/php/Optimization/FunctionRegistry.php +4 -4
- package/src/php/Optimization/Optimization.php +6 -6
- package/src/php/ProblemManager.php +12 -2
- package/src/php/RequestContext.php +11 -11
- package/src/php/Store/InMemoryStore.php +96 -66
- package/src/php/Store/MongoDbStore.php +2 -2
- package/src/php/Store/RedisStore.php +1 -1
- package/src/php/Store/StoreManager.php +48 -35
- package/src/php/Tests/AsnLookupTest.php +61 -0
- package/src/php/Tests/ChallengeUtilsTest.php +31 -0
- package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
- package/src/php/Tests/FingerprintClientTest.php +70 -70
- package/src/php/Tests/FingerprintEngineTest.php +4 -2
- package/src/php/Tests/PowTest.php +37 -39
- package/src/php/Tests/QuicFingerprintTest.php +115 -53
- package/src/php/Tests/ThreatIntelTest.php +2 -2
- package/src/php/Utils/BigInt.php +202 -202
- package/src/php/Utils/BlockList.php +13 -13
- package/src/php/Utils/Env.php +7 -2
- package/src/php/Utils/Logger.php +19 -1
- package/src/php/Utils/MaliciousPatterns.php +6 -6
- package/src/php/Utils/MetricsManager.php +209 -209
- package/src/php/Utils/RequestUtils.php +348 -18
- package/src/php/Utils/TLSClientHelloParser.php +385 -369
- package/src/php/WordPress/WpDbStore.php +34 -7
- package/src/php/WordPress/anonympins-bot-mitigation-pow.php +1222 -0
- package/src/php/WordPress/fingerprint-anti-bot.php +0 -559
- package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.mo +0 -0
- package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.po +232 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +297 -261
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +348 -267
- package/src/php/WordPress/package.php +88 -99
- package/src/php/WordPress/readme.txt +28 -6
- package/src/php/bin/auto-tune.php +1 -1
- package/public/fingerprint-anti-bot.zip +0 -0
package/src/js/fingerprint.js
CHANGED
|
@@ -5,6 +5,7 @@ import {Worker} from "node:worker_threads";
|
|
|
5
5
|
import {getProblemManager, problemManager} from "./problem-manager.js";
|
|
6
6
|
import {Optimization} from "./library.js";
|
|
7
7
|
import {cyrb53, FingerprintBuilder} from "./fingerprint.builder.js";
|
|
8
|
+
import {defaultAsnLookup, NetworkProfile} from "./asn-lookup.js";
|
|
8
9
|
import {DynamicWasmGenerator} from "./dynamic-wasm.js";
|
|
9
10
|
import {writeFileSync, readFileSync, existsSync, promises as fsPromises} from "node:fs";
|
|
10
11
|
import {fileURLToPath} from "node:url";
|
|
@@ -13,6 +14,9 @@ import {GpuPowSolver} from "./gpu_pow.solver.js";
|
|
|
13
14
|
import {
|
|
14
15
|
verifyZkpProof,
|
|
15
16
|
sanitizeRedirectPath,
|
|
17
|
+
extractPrivateAccessTokens,
|
|
18
|
+
parsePrivateAccessToken,
|
|
19
|
+
verifyPatSignature,
|
|
16
20
|
decodePolymorphicFingerprint,
|
|
17
21
|
deepMerge,
|
|
18
22
|
getHeaderSignature,
|
|
@@ -23,7 +27,8 @@ import {
|
|
|
23
27
|
isLoopbackIp,
|
|
24
28
|
isPrivateIp,
|
|
25
29
|
parseUserAgent,
|
|
26
|
-
safeJsonStringify
|
|
30
|
+
safeJsonStringify,
|
|
31
|
+
generateIssuerPemKeys
|
|
27
32
|
} from "./fingerprint.utils.js";
|
|
28
33
|
|
|
29
34
|
|
|
@@ -400,6 +405,12 @@ export function getProtocolAnomalyScore(context) {
|
|
|
400
405
|
const ua = context.headers?.['user-agent'] || '';
|
|
401
406
|
const uaParts = parseUserAgent(ua);
|
|
402
407
|
const browser = uaParts.browser;
|
|
408
|
+
const isChromium = browser?.startsWith('Chrome') || browser?.startsWith('Edge');
|
|
409
|
+
const isFirefox = browser?.startsWith('Firefox');
|
|
410
|
+
const isSafari = browser?.startsWith('Safari');
|
|
411
|
+
const isHumanBrowser = ['Chrome', 'Firefox', 'Safari', 'Edge'].includes(
|
|
412
|
+
browser?.split('/')[0] || null
|
|
413
|
+
);
|
|
403
414
|
|
|
404
415
|
if (browser) {
|
|
405
416
|
// HTTP/2 Anomaly Logic
|
|
@@ -410,9 +421,6 @@ export function getProtocolAnomalyScore(context) {
|
|
|
410
421
|
const connWindow = parseInt(parts[1], 10);
|
|
411
422
|
const streamPriority = parts[2] || '';
|
|
412
423
|
const headerOrder = parts.length > 3 ? parts[3] : '';
|
|
413
|
-
const isChromium = browser.startsWith('Chrome') || browser.startsWith('Edge');
|
|
414
|
-
const isFirefox = browser.startsWith('Firefox');
|
|
415
|
-
const isSafari = browser.startsWith('Safari');
|
|
416
424
|
|
|
417
425
|
if (isChromium) {
|
|
418
426
|
if (headerOrder && headerOrder !== 'm,a,s,p') http2Anomaly += 60.0;
|
|
@@ -424,6 +432,33 @@ export function getProtocolAnomalyScore(context) {
|
|
|
424
432
|
if (headerOrder && headerOrder !== 'm,s,p,a') http2Anomaly += 60.0;
|
|
425
433
|
}
|
|
426
434
|
|
|
435
|
+
// Analyse de la structure des dépendances de flux HTTP/2 (RFC 7540)
|
|
436
|
+
if (streamPriority) {
|
|
437
|
+
const priorityEntries = streamPriority.split(',').map(s => s.trim()).filter(Boolean);
|
|
438
|
+
for (const entry of priorityEntries) {
|
|
439
|
+
const sub = entry.split(':');
|
|
440
|
+
if (sub.length >= 4) {
|
|
441
|
+
const streamId = parseInt(sub[0], 10);
|
|
442
|
+
const exclusive = parseInt(sub[1], 10);
|
|
443
|
+
const depStreamId = parseInt(sub[2], 10);
|
|
444
|
+
const weight = parseInt(sub[3], 10);
|
|
445
|
+
|
|
446
|
+
// Auto-dépendance interdite (RFC 7540 Section 5.3.1)
|
|
447
|
+
if (streamId === depStreamId && streamId > 0) {
|
|
448
|
+
http2Anomaly += 60.0;
|
|
449
|
+
}
|
|
450
|
+
// Poids en dehors de [1, 256] (RFC 7540 Section 5.3.2)
|
|
451
|
+
if (weight < 1 || weight > 256) {
|
|
452
|
+
http2Anomaly += 45.0;
|
|
453
|
+
}
|
|
454
|
+
// Signature scraper : Go net/http2 ou Python h2 par défaut (poids 16 sur flux 0 non-exclusif)
|
|
455
|
+
if (isChromium && depStreamId === 0 && weight === 16 && exclusive === 0) {
|
|
456
|
+
http2Anomaly += 55.0;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
427
462
|
// Analyse fine des trames (PRIORITY, WINDOW_UPDATE, CONTINUATION)
|
|
428
463
|
if (parts.length >= 5) {
|
|
429
464
|
const frameCountsStr = parts[4];
|
|
@@ -444,6 +479,13 @@ export function getProtocolAnomalyScore(context) {
|
|
|
444
479
|
if (priorityCount === 0) http2Anomaly += 25.0;
|
|
445
480
|
// Chrome est agressif avec les WINDOW_UPDATE
|
|
446
481
|
if (windowUpdateCount < 2) http2Anomaly += 20.0;
|
|
482
|
+
|
|
483
|
+
// NOUVEAU: Analyse de l'arbre de dépendances HTTP/2.
|
|
484
|
+
// Un arbre plat (peu de dépendances) est suspect pour Chromium.
|
|
485
|
+
const priorityDependencies = streamPriority.split(',').length - 1;
|
|
486
|
+
if (streamPriority.includes(',') && priorityDependencies < 2 && priorityCount > 0) {
|
|
487
|
+
http2Anomaly += 35.0;
|
|
488
|
+
}
|
|
447
489
|
} else if (isFirefox) {
|
|
448
490
|
// Firefox utilise un schéma de priorité différent, souvent avec moins de trames PRIORITY
|
|
449
491
|
if (priorityCount > 1) http2Anomaly += 20.0;
|
|
@@ -471,10 +513,6 @@ export function getProtocolAnomalyScore(context) {
|
|
|
471
513
|
const frameOrderRaw = parts[3] || context.headers?.['x-quic-frame-order'] || context.quicFrameOrder || '';
|
|
472
514
|
const frameOrder = frameOrderRaw.toLowerCase().split(',').map(s => s.trim()).filter(Boolean);
|
|
473
515
|
|
|
474
|
-
const isChromium = browser.startsWith('Chrome') || browser.startsWith('Edge');
|
|
475
|
-
const isFirefox = browser.startsWith('Firefox');
|
|
476
|
-
const isSafari = browser.startsWith('Safari');
|
|
477
|
-
|
|
478
516
|
const maxData = parseInt(params['1'] || params['0x01'] || '0', 10);
|
|
479
517
|
const maxStreams = parseInt(params['4'] || params['8'] || params['0x08'] || '0', 10);
|
|
480
518
|
const bidiLocal = parseInt(params['5'] || params['0x05'] || '0', 10);
|
|
@@ -484,7 +522,33 @@ export function getProtocolAnomalyScore(context) {
|
|
|
484
522
|
// Contrôle de flux global et nombre de flux bidirectionnels
|
|
485
523
|
if (maxData > 0 && maxData < 1048576) quicAnomaly += 40.0;
|
|
486
524
|
if (maxStreams > 0 && maxStreams !== 100) quicAnomaly += 30.0;
|
|
487
|
-
|
|
525
|
+
|
|
526
|
+
// NOUVEAU: Analyse des trames PRIORITY_UPDATE (RFC 9218)
|
|
527
|
+
if (priorityOrder) {
|
|
528
|
+
// Contrôle de l'urgence (u: 0 à 7 strict)
|
|
529
|
+
const uMatch = priorityOrder.match(/\bu=(-?\d+)\b/);
|
|
530
|
+
if (uMatch) {
|
|
531
|
+
const urgency = parseInt(uMatch[1], 10);
|
|
532
|
+
if (urgency < 0 || urgency > 7) {
|
|
533
|
+
quicAnomaly += 50.0;
|
|
534
|
+
}
|
|
535
|
+
} else if (!priorityOrder.includes('u=')) {
|
|
536
|
+
quicAnomaly += 30.0;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Paramètre incrémental invalide ou malformé
|
|
540
|
+
if (/\bi=\d+\b/.test(priorityOrder)) {
|
|
541
|
+
quicAnomaly += 40.0;
|
|
542
|
+
}
|
|
543
|
+
if (/\bw=\d+\b/.test(priorityOrder)) {
|
|
544
|
+
quicAnomaly += 45.0;
|
|
545
|
+
}
|
|
546
|
+
// Un navigateur légitime enverra probablement une mise à jour incrémentale 'i'
|
|
547
|
+
if (priorityOrder.includes('u=') && !priorityOrder.includes('i') && !/\bu=[0-7],i\b/.test(priorityOrder)) {
|
|
548
|
+
quicAnomaly += 40.0; // Urgence envoyée sans drapeau incrémental, suspect pour Chrome
|
|
549
|
+
}
|
|
550
|
+
if (priorityOrder === 'p' || priorityOrder === 'i') quicAnomaly += 50.0; // Champ malformé
|
|
551
|
+
}
|
|
488
552
|
|
|
489
553
|
// Contrôle de flux bidi (Chromium alloue 6MB = 6291456 ou au minimum 512 Ko)
|
|
490
554
|
// curl-impersonate / quiche alloue 256 Ko (262144) ou 128 Ko (131072)
|
|
@@ -506,6 +570,12 @@ export function getProtocolAnomalyScore(context) {
|
|
|
506
570
|
if (pIdx !== -1 && sIdx !== -1 && pIdx < sIdx) {
|
|
507
571
|
quicAnomaly += 60.0;
|
|
508
572
|
}
|
|
573
|
+
|
|
574
|
+
// RFC 9114: Le flux de contrôle ne doit pas contenir de doublon SETTINGS
|
|
575
|
+
const sCount = frameOrder.filter(f => f === 's' || f === 'settings' || f === '4').length;
|
|
576
|
+
if (sCount > 1) {
|
|
577
|
+
quicAnomaly += 50.0;
|
|
578
|
+
}
|
|
509
579
|
}
|
|
510
580
|
} else if (isFirefox) {
|
|
511
581
|
const maxData = parseInt(params['1'] || '0', 10);
|
|
@@ -535,6 +605,55 @@ export function getProtocolAnomalyScore(context) {
|
|
|
535
605
|
}
|
|
536
606
|
}
|
|
537
607
|
}
|
|
608
|
+
|
|
609
|
+
// NOUVEAU: Analyse du ratio de compression HPACK/QPACK
|
|
610
|
+
const compressionInfo = context.headers?.['x-compression-info'] || null;
|
|
611
|
+
if (compressionInfo) {
|
|
612
|
+
const info = {};
|
|
613
|
+
compressionInfo.split(',').forEach(p => {
|
|
614
|
+
const [key, value] = p.split(':');
|
|
615
|
+
if (key && value) info[key.trim()] = parseFloat(value);
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
const reqCount = info.req_count || 0;
|
|
619
|
+
const hpackRatio = info.hpack_ratio;
|
|
620
|
+
const qpackRatio = info.qpack_ratio;
|
|
621
|
+
const dynamicEntries = info.dynamic_table_entries !== undefined ? info.dynamic_table_entries : info.table_entries;
|
|
622
|
+
const dynamicHits = info.dynamic_hits;
|
|
623
|
+
const dynamicTableSize = info.dynamic_table_size;
|
|
624
|
+
|
|
625
|
+
// Après quelques requêtes, un vrai navigateur doit avoir un bon ratio de compression
|
|
626
|
+
if (reqCount > 3 && isHumanBrowser) {
|
|
627
|
+
if (hpackRatio !== undefined) {
|
|
628
|
+
if (hpackRatio < 0.4) {
|
|
629
|
+
http2Anomaly += (1 - hpackRatio) * 50;
|
|
630
|
+
} else if (hpackRatio > 0.75) {
|
|
631
|
+
http2Anomaly += Math.min(50.0, (hpackRatio - 0.5) * 100);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
if (qpackRatio !== undefined) {
|
|
635
|
+
if (qpackRatio < 0.4) {
|
|
636
|
+
quicAnomaly += (1 - qpackRatio) * 50;
|
|
637
|
+
} else if (qpackRatio > 0.75) {
|
|
638
|
+
quicAnomaly += Math.min(50.0, (qpackRatio - 0.5) * 100);
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Détection de réinitialisation de table dynamique
|
|
643
|
+
if (dynamicEntries !== undefined && dynamicEntries === 0) {
|
|
644
|
+
http2Anomaly += 45.0;
|
|
645
|
+
quicAnomaly += 45.0;
|
|
646
|
+
}
|
|
647
|
+
if (dynamicHits !== undefined && dynamicHits === 0) {
|
|
648
|
+
http2Anomaly += 35.0;
|
|
649
|
+
quicAnomaly += 35.0;
|
|
650
|
+
}
|
|
651
|
+
if (dynamicTableSize !== undefined && dynamicTableSize === 0) {
|
|
652
|
+
http2Anomaly += 40.0;
|
|
653
|
+
quicAnomaly += 40.0;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
538
657
|
}
|
|
539
658
|
|
|
540
659
|
return {
|
|
@@ -2369,29 +2488,17 @@ function getBehaviorScore(context) {
|
|
|
2369
2488
|
return { behaviorScore: 0 }; // Pas de données, pas de pénalité.
|
|
2370
2489
|
}
|
|
2371
2490
|
|
|
2372
|
-
// Traitement direct si un flag binaire client-side (1 = humain, 0 = bot) est envoyé
|
|
2373
|
-
if (behaviorHeader === 1 || behaviorHeader === '1') {
|
|
2374
|
-
return { behaviorScore: 0 };
|
|
2375
|
-
}
|
|
2376
|
-
if (behaviorHeader === 0 || behaviorHeader === '0') {
|
|
2377
|
-
return { behaviorScore: 100 };
|
|
2378
|
-
}
|
|
2379
|
-
|
|
2380
2491
|
// Traitement direct si un score numérique est envoyé côté client
|
|
2381
2492
|
if (typeof behaviorHeader === 'number') {
|
|
2382
2493
|
return { behaviorScore: Math.max(0, Math.min(100, behaviorHeader)) };
|
|
2383
2494
|
}
|
|
2384
2495
|
if (typeof behaviorHeader === 'string' && !isNaN(Number(behaviorHeader)) && behaviorHeader.trim() !== '') {
|
|
2385
2496
|
const trimmed = behaviorHeader.trim();
|
|
2386
|
-
if (trimmed === '1') return { behaviorScore: 0 };
|
|
2387
|
-
if (trimmed === '0') return { behaviorScore: 100 };
|
|
2388
2497
|
return { behaviorScore: Math.max(0, Math.min(100, parseFloat(trimmed))) };
|
|
2389
2498
|
}
|
|
2390
2499
|
|
|
2391
2500
|
try {
|
|
2392
2501
|
const metrics = JSON.parse(behaviorHeader);
|
|
2393
|
-
if (metrics === 1) return { behaviorScore: 0 };
|
|
2394
|
-
if (metrics === 0) return { behaviorScore: 100 };
|
|
2395
2502
|
if (typeof metrics === 'number') {
|
|
2396
2503
|
return { behaviorScore: Math.max(0, Math.min(100, metrics)) };
|
|
2397
2504
|
}
|
|
@@ -2713,7 +2820,7 @@ export function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerp
|
|
|
2713
2820
|
}
|
|
2714
2821
|
|
|
2715
2822
|
const { ja3, ja4 } = actualGetTlsFingerprintFn(context) || { ja3: null, ja4: null }; // Defensive check
|
|
2716
|
-
const ua = context.headers["user-agent"] || '';
|
|
2823
|
+
const ua = context.headers?.["user-agent"] || '';
|
|
2717
2824
|
const ja3Raw = context.headers['x-ja3-raw'] || null;
|
|
2718
2825
|
const httpVersion = context.httpVersion || '';
|
|
2719
2826
|
|
|
@@ -2735,7 +2842,8 @@ export function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerp
|
|
|
2735
2842
|
score = Math.max(score, 100);
|
|
2736
2843
|
}
|
|
2737
2844
|
|
|
2738
|
-
const
|
|
2845
|
+
const uaParts = parseUserAgent(ua);
|
|
2846
|
+
const claimedBrowser = uaParts.browser?.split('/')[0] || null;
|
|
2739
2847
|
const isHumanBrowser = ['Chrome', 'Firefox', 'Safari', 'Edge'].includes(claimedBrowser);
|
|
2740
2848
|
|
|
2741
2849
|
// --- ANALYSE 2 : CONTRÔLE PROFOND SUR L'EMPREINTE BRUTE (RAW JA3) ---
|
|
@@ -2777,8 +2885,6 @@ export function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerp
|
|
|
2777
2885
|
if (ja4) {
|
|
2778
2886
|
const parsedJa4 = parseJa4(ja4);
|
|
2779
2887
|
if (parsedJa4) {
|
|
2780
|
-
const uaParts = parseUserAgent(ua);
|
|
2781
|
-
|
|
2782
2888
|
// Check 1: Incohérence ALPN / HTTP Version
|
|
2783
2889
|
if (parsedJa4.alpn === 'h2' && (context.httpVersion === '1.1' || context.httpVersion === '1.0')) {
|
|
2784
2890
|
const hasProxy = context.headers['via'] || context.headers['forwarded'] || context.headers['x-forwarded-proto'] || context.headers['x-forwarded-for'];
|
|
@@ -2802,6 +2908,25 @@ export function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerp
|
|
|
2802
2908
|
}
|
|
2803
2909
|
}
|
|
2804
2910
|
|
|
2911
|
+
// Validation TLS 1.3 Encrypted Client Hello (ECH) & Discrépance SNI
|
|
2912
|
+
const outerSni = context.headers?.['x-ech-outer-sni'];
|
|
2913
|
+
const host = (context.headers?.['host'] || '').split(':')[0].trim();
|
|
2914
|
+
const hasEch = context.headers?.['x-ech-present'] === 'true' || context.headers?.['x-has-ech'] === 'true';
|
|
2915
|
+
const echExpected = context.headers?.['x-ech-expected'] === 'true' || Boolean(outerSni);
|
|
2916
|
+
|
|
2917
|
+
if (outerSni && host && outerSni.toLowerCase() !== host.toLowerCase() && !hasEch) {
|
|
2918
|
+
score = Math.max(score, 60.0);
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
const browserVersion = parseInt(uaParts.browser?.split('/')[1] || '0', 10);
|
|
2922
|
+
const isModernEchBrowser = (
|
|
2923
|
+
(claimedBrowser === 'Chrome' || claimedBrowser === 'Edge') && browserVersion >= 119
|
|
2924
|
+
) || (claimedBrowser === 'Firefox' && browserVersion >= 118);
|
|
2925
|
+
|
|
2926
|
+
if (context.isHttps && isModernEchBrowser && echExpected && !hasEch) {
|
|
2927
|
+
score = Math.max(score, 55.0);
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2805
2930
|
// 2. If no JA3 hash is available, we cannot perform the consistency check.
|
|
2806
2931
|
if (ja3 && ua) {
|
|
2807
2932
|
// Look up the expected browser family (or families) from our database.
|
|
@@ -2840,7 +2965,6 @@ export function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerp
|
|
|
2840
2965
|
// Create the promise for the async part (Check 4)
|
|
2841
2966
|
const promise = (async () => {
|
|
2842
2967
|
let asyncScore = score;
|
|
2843
|
-
const uaParts = parseUserAgent(ua);
|
|
2844
2968
|
|
|
2845
2969
|
if (uaParts.browser) {
|
|
2846
2970
|
const browserFamily = uaParts.browser.split('/')[0];
|
|
@@ -3315,18 +3439,26 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
|
|
|
3315
3439
|
/**
|
|
3316
3440
|
* Calcule un score d'incohérence analogique lisse plafonnant à une asymptote de 99.9.
|
|
3317
3441
|
* @param {number} consistencyScore Similarité entre 0 et 1 issue du FingerprintBuilder.
|
|
3318
|
-
* @param {number} [
|
|
3442
|
+
* @param {number|object} [inflectionOrProfile=0.72] Point d'inflexion ou profil réseau/IP.
|
|
3319
3443
|
* @param {number} [steepness=12] Raideur de la transition sigmoïdale.
|
|
3320
3444
|
* @returns {number} Score de 0 à 99.9 sans saut de palier ni certitude absolue à 100.
|
|
3321
3445
|
*/
|
|
3322
|
-
export function calculateAnalogInconsistencyScore(consistencyScore,
|
|
3446
|
+
export function calculateAnalogInconsistencyScore(consistencyScore, inflectionOrProfile = 0.72, steepness = 12) {
|
|
3323
3447
|
const s = Math.max(0.0, Math.min(1.0, consistencyScore));
|
|
3324
3448
|
if (s >= 0.98) return 0.0;
|
|
3325
3449
|
|
|
3450
|
+
let inflectionPoint = typeof inflectionOrProfile === 'number'
|
|
3451
|
+
? inflectionOrProfile
|
|
3452
|
+
: (inflectionOrProfile?.inflectionPoint ?? 0.72);
|
|
3453
|
+
|
|
3454
|
+
const k = typeof inflectionOrProfile === 'object' && inflectionOrProfile?.steepness !== undefined
|
|
3455
|
+
? inflectionOrProfile.steepness
|
|
3456
|
+
: steepness;
|
|
3457
|
+
|
|
3326
3458
|
const asymptote = 99.9;
|
|
3327
|
-
const raw = 1.0 / (1.0 + Math.exp(
|
|
3328
|
-
const minVal = 1.0 / (1.0 + Math.exp(
|
|
3329
|
-
const maxVal = 1.0 / (1.0 + Math.exp(
|
|
3459
|
+
const raw = 1.0 / (1.0 + Math.exp(k * (s - inflectionPoint)));
|
|
3460
|
+
const minVal = 1.0 / (1.0 + Math.exp(k * (1.0 - inflectionPoint)));
|
|
3461
|
+
const maxVal = 1.0 / (1.0 + Math.exp(k * (0.0 - inflectionPoint)));
|
|
3330
3462
|
const normalized = ((raw - minVal) / (maxVal - minVal)) * asymptote;
|
|
3331
3463
|
|
|
3332
3464
|
return Math.min(asymptote, Math.round(normalized * 10.0) / 10.0);
|
|
@@ -3502,13 +3634,14 @@ async function getBotnetClusterScore(context, stableFpHash) {
|
|
|
3502
3634
|
*/
|
|
3503
3635
|
async function getIpReputationScore(ip) {
|
|
3504
3636
|
const key = `ip-reputation:${ip}`;
|
|
3637
|
+
const netProfile = defaultAsnLookup.lookup(ip);
|
|
3505
3638
|
const data = await store.get(key);
|
|
3506
|
-
if (!data) return
|
|
3639
|
+
if (!data) return netProfile.baseScore;
|
|
3507
3640
|
|
|
3508
3641
|
const now = Date.now();
|
|
3509
3642
|
const hoursPassed = (now - data.lastUpdate) / (1000 * 60 * 60);
|
|
3510
3643
|
const decay = Math.floor(hoursPassed * 2); // Decay 2 points per hour of inactivity
|
|
3511
|
-
return Math.max(
|
|
3644
|
+
return Math.max(netProfile.baseScore, data.score - decay);
|
|
3512
3645
|
}
|
|
3513
3646
|
|
|
3514
3647
|
/**
|
|
@@ -3839,7 +3972,7 @@ async function resolveRequestIdentity(context, securityConfig = {}) {
|
|
|
3839
3972
|
* @param {object} deviceData - The device's activity data.
|
|
3840
3973
|
* @returns {Promise<{historyScore: number, rotationScore: number}>}
|
|
3841
3974
|
*/
|
|
3842
|
-
async function getBehavioralIndicators(context, deviceData) {
|
|
3975
|
+
async function getBehavioralIndicators(context, deviceData, netProfile = null) {
|
|
3843
3976
|
const rapidChangeThresholdMs = 2000; // 2 secondes
|
|
3844
3977
|
const maxRapidChanges = 3;
|
|
3845
3978
|
|
|
@@ -3917,13 +4050,18 @@ async function getBehavioralIndicators(context, deviceData) {
|
|
|
3917
4050
|
: MAX_DISTINCT_IPS_PER_DEVICE;
|
|
3918
4051
|
const freeIpChanges = isSharedIp ? 1 : 3;
|
|
3919
4052
|
|
|
3920
|
-
|
|
4053
|
+
let historyScore = Math.min(
|
|
3921
4054
|
100,
|
|
3922
4055
|
(Math.max(0, deviceData.ips.size - freeIpChanges) /
|
|
3923
4056
|
(maxIpsForDevice - freeIpChanges)) *
|
|
3924
4057
|
100,
|
|
3925
4058
|
);
|
|
3926
4059
|
|
|
4060
|
+
// Mobile CGNAT : désactivation/amortissement de la pénalité de rotation rapide d'IP
|
|
4061
|
+
if (netProfile?.toleranceRotation) {
|
|
4062
|
+
historyScore = Math.min(historyScore, 10.0);
|
|
4063
|
+
}
|
|
4064
|
+
|
|
3927
4065
|
// Score based on rapid identity rotation (0-100)
|
|
3928
4066
|
const rotationScore = Math.min(
|
|
3929
4067
|
100,
|
|
@@ -4054,6 +4192,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
4054
4192
|
// Execute non-interdependent asynchronous operations in parallel
|
|
4055
4193
|
const zkpProof = (context.getHeader ? context.getHeader('x-zkp-proof') : null) || (context.headers ? context.headers['x-zkp-proof'] : null) || (context.query ? context.query['pow_zkp'] : null) || (context.queryParams ? context.queryParams['pow_zkp'] : null) || '';
|
|
4056
4194
|
const zkpY = zkpProof ? zkpProof.split(":")[0] : null;
|
|
4195
|
+
const netProfile = defaultAsnLookup.lookup(clientIp);
|
|
4057
4196
|
|
|
4058
4197
|
const [
|
|
4059
4198
|
behavioral,
|
|
@@ -4064,7 +4203,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
4064
4203
|
{ botnetClusterScore },
|
|
4065
4204
|
_ // store.set result
|
|
4066
4205
|
] = await Promise.all([
|
|
4067
|
-
getBehavioralIndicators(context, deviceData), // This modifies deviceData, so it must be done before saving deviceData
|
|
4206
|
+
getBehavioralIndicators(context, deviceData, netProfile), // This modifies deviceData, so it must be done before saving deviceData
|
|
4068
4207
|
getThreatIntelScore(context, zkpY, securityConfig), // NOUVEAU: Score de Threat Intelligence Fédéré
|
|
4069
4208
|
getTlsSpoofingScore(context),
|
|
4070
4209
|
getSubnetScore(context, deviceId, securityConfig),
|
|
@@ -4075,7 +4214,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
4075
4214
|
|
|
4076
4215
|
// Synchronous calculations
|
|
4077
4216
|
const { headerAnomalyScore } = getHeaderAnomalies(context);
|
|
4078
|
-
const similarityThreshold = securityConfig?.similarityThreshold ??
|
|
4217
|
+
const similarityThreshold = securityConfig?.similarityThreshold ?? netProfile.inflectionPoint;
|
|
4079
4218
|
const inconsistencyScore = calculateAnalogInconsistencyScore(consistencyScore, similarityThreshold);
|
|
4080
4219
|
|
|
4081
4220
|
const { behaviorScore } = getBehaviorScore(context); // Appel de la fonction
|
|
@@ -4640,6 +4779,8 @@ export class FingerprintEngine {
|
|
|
4640
4779
|
// Auto-generate Ed25519 key pair on load if indicated and keys are not set
|
|
4641
4780
|
if (securityConfig && (securityConfig.useAsymmetricTickets || securityConfig.ed25519 === 'auto') && !process.env.ED25519_PRIVATE_KEY) {
|
|
4642
4781
|
const persistentKeyPath = join(configDir, 'ed25519_key.json');
|
|
4782
|
+
const privatePemPath = join(configDir, 'issuer-private.pem');
|
|
4783
|
+
const publicPemPath = join(configDir, 'issuer-public.pem');
|
|
4643
4784
|
if (existsSync(persistentKeyPath)) {
|
|
4644
4785
|
try {
|
|
4645
4786
|
const keys = JSON.parse(readFileSync(persistentKeyPath, 'utf-8'));
|
|
@@ -4649,6 +4790,14 @@ export class FingerprintEngine {
|
|
|
4649
4790
|
} catch (e) {
|
|
4650
4791
|
console.error('[Fingerprint] Failed to load persistent Ed25519 keys:', e.message);
|
|
4651
4792
|
}
|
|
4793
|
+
} else if (existsSync(privatePemPath) && existsSync(publicPemPath)) {
|
|
4794
|
+
try {
|
|
4795
|
+
process.env.ED25519_PRIVATE_KEY = readFileSync(privatePemPath, 'utf-8');
|
|
4796
|
+
process.env.ED25519_PUBLIC_KEY = readFileSync(publicPemPath, 'utf-8');
|
|
4797
|
+
this._log('Persistent Ed25519 PEM keys loaded from disk');
|
|
4798
|
+
} catch (e) {
|
|
4799
|
+
console.error('[Fingerprint] Failed to load persistent Ed25519 PEM keys:', e.message);
|
|
4800
|
+
}
|
|
4652
4801
|
} else {
|
|
4653
4802
|
try {
|
|
4654
4803
|
const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519', {
|
|
@@ -4751,6 +4900,7 @@ export class FingerprintEngine {
|
|
|
4751
4900
|
'federatedPeers', 'federationSecret', 'filterWhitelist',
|
|
4752
4901
|
'challengeRateLimit',
|
|
4753
4902
|
'differentialPrivacy', 'dpEpsilon'
|
|
4903
|
+
,'pat'
|
|
4754
4904
|
]);
|
|
4755
4905
|
|
|
4756
4906
|
// 1. Check for essential keys
|
|
@@ -5218,6 +5368,44 @@ export class FingerprintEngine {
|
|
|
5218
5368
|
return { action: 'next', score: 0, vector: {} };
|
|
5219
5369
|
}
|
|
5220
5370
|
|
|
5371
|
+
// --- PRIVATE ACCESS TOKENS (PAT / RFC 9578 & Privacy Pass) ZERO-FRICTION CHALLENGE EXEMPTION ---
|
|
5372
|
+
const rawPatTokens = extractPrivateAccessTokens(requestContext.headers);
|
|
5373
|
+
let hasValidPat = false;
|
|
5374
|
+
if (rawPatTokens.length > 0) {
|
|
5375
|
+
// Même avec un PAT valide, une attaque applicative avérée ou un terminal condamné doit être neutralisé
|
|
5376
|
+
if (deviceData?.condemned || this._hasCertainAttack(requestContext)) {
|
|
5377
|
+
return {
|
|
5378
|
+
action: 'block',
|
|
5379
|
+
status: 404,
|
|
5380
|
+
body: 'Forbidden',
|
|
5381
|
+
score: 100,
|
|
5382
|
+
vector: { honeypotScore: 100 }
|
|
5383
|
+
};
|
|
5384
|
+
}
|
|
5385
|
+
const patConfig = this.securityConfig.pat || {};
|
|
5386
|
+
for (const rawToken of rawPatTokens) {
|
|
5387
|
+
const parsedToken = parsePrivateAccessToken(rawToken);
|
|
5388
|
+
if (parsedToken) {
|
|
5389
|
+
const nonceKey = `pat-nonce:${parsedToken.nonce}`;
|
|
5390
|
+
const isReplayed = await store.has(nonceKey);
|
|
5391
|
+
if (!isReplayed) {
|
|
5392
|
+
const isValid = verifyPatSignature(parsedToken, patConfig);
|
|
5393
|
+
if (isValid) {
|
|
5394
|
+
// Anti-replay: cache nonce with 24-hour TTL
|
|
5395
|
+
const nonceTtl = patConfig.nonceTtl || 86400;
|
|
5396
|
+
await store.set(nonceKey, true, nonceTtl);
|
|
5397
|
+
this._log('Private Access Token (PAT) cryptographically verified - challenge exemption enabled', {
|
|
5398
|
+
tokenType: parsedToken.tokenType,
|
|
5399
|
+
tokenKeyId: parsedToken.tokenKeyId
|
|
5400
|
+
});
|
|
5401
|
+
hasValidPat = true;
|
|
5402
|
+
break;
|
|
5403
|
+
}
|
|
5404
|
+
}
|
|
5405
|
+
}
|
|
5406
|
+
}
|
|
5407
|
+
}
|
|
5408
|
+
|
|
5221
5409
|
// Resolve identity and check for persisted "condemned" status early.
|
|
5222
5410
|
const currentDeviceHash = getCompositeDeviceHash(requestContext);
|
|
5223
5411
|
const isNewDevice = !!newCookie;
|
|
@@ -5333,6 +5521,9 @@ export class FingerprintEngine {
|
|
|
5333
5521
|
// This is a strong signal of spoofing.
|
|
5334
5522
|
suspicionVector.tlsSpoofingScore = Math.max(suspicionVector.tlsSpoofingScore || 0, 95);
|
|
5335
5523
|
}
|
|
5524
|
+
if (hasValidPat) {
|
|
5525
|
+
suspicionVector.pat_verified = 100.0;
|
|
5526
|
+
}
|
|
5336
5527
|
let finalScore = preCalculatedScore !== null ? preCalculatedScore : this.calculateFinalScore(suspicionVector);
|
|
5337
5528
|
|
|
5338
5529
|
this._log('Suspicion vector calculated', {
|
|
@@ -5821,7 +6012,7 @@ export class FingerprintEngine {
|
|
|
5821
6012
|
// OU
|
|
5822
6013
|
// 2. La requête est très suspecte (dépasse le seuil 'high'), ce qui annule la validité du ticket actuel.
|
|
5823
6014
|
const zkpProof = requestContext.headers['x-zkp-proof'] || query.pow_zkp || '';
|
|
5824
|
-
const hasValidTicket = await isTicketValid(clientIp, powCookie, deviceId, currentDeviceHash, allowRoaming, zkpProof);
|
|
6015
|
+
const hasValidTicket = (await isTicketValid(clientIp, powCookie, deviceId, currentDeviceHash, allowRoaming, zkpProof)) || hasValidPat;
|
|
5825
6016
|
// Correction : Pour éviter une boucle infinie de challenges (qui mène à l'erreur 429),
|
|
5826
6017
|
// on fait confiance au ticket valide tant qu'il n'a pas expiré.
|
|
5827
6018
|
const maxIndicatorsCount = Object.values(suspicionVector).filter(val => typeof val === 'number' && val >= 100).length;
|
|
@@ -6426,7 +6617,8 @@ function sanitizeProxyHeaders(context, securityConfig) {
|
|
|
6426
6617
|
'x-ja4-hash',
|
|
6427
6618
|
'x-http2-fingerprint',
|
|
6428
6619
|
'x-tcp-fingerprint',
|
|
6429
|
-
'x-ja3-raw'
|
|
6620
|
+
'x-ja3-raw',
|
|
6621
|
+
'x-compression-info'
|
|
6430
6622
|
];
|
|
6431
6623
|
|
|
6432
6624
|
if (securityConfig && securityConfig.trustedProxies) {
|
|
@@ -7214,6 +7406,8 @@ export const powMiddleware = (securityConfig) => {
|
|
|
7214
7406
|
* This is a common pattern to allow mocking of ES module functions.
|
|
7215
7407
|
*/
|
|
7216
7408
|
export const __internal = {
|
|
7409
|
+
defaultAsnLookup,
|
|
7410
|
+
NetworkProfile,
|
|
7217
7411
|
get store() { return store; }, // Export the store for testing
|
|
7218
7412
|
checkChallengeRateLimit,
|
|
7219
7413
|
getDeviceHash,
|
|
@@ -7270,6 +7464,7 @@ export const __internal = {
|
|
|
7270
7464
|
findPeerInSubnet,
|
|
7271
7465
|
handleCooperativeRequest,
|
|
7272
7466
|
broadcastBannedZkp,
|
|
7467
|
+
generateIssuerPemKeys,
|
|
7273
7468
|
getMetric,
|
|
7274
7469
|
incrementCounter,
|
|
7275
7470
|
observeValue
|
|
@@ -7969,4 +8164,5 @@ export async function handleMetricsRequest(req, res, securityConfig) {
|
|
|
7969
8164
|
// 3. Si autorisé, servir les métriques.
|
|
7970
8165
|
res.set('Content-Type', 'text/plain; version=0.0.4; charset=utf-8');
|
|
7971
8166
|
res.send(MetricsManager.getPrometheusMetrics(securityConfig));
|
|
7972
|
-
}
|
|
8167
|
+
}
|
|
8168
|
+
export { generateIssuerPemKeys };
|