@anonympins/fingerprint 0.7.2 → 0.7.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/CHANGELOG.md +36 -9
- package/README.md +1 -1
- package/package.json +109 -107
- package/public/fingerprint-anti-bot.zip +0 -0
- package/src/js/fingerprint.client.js +252 -45
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +265 -185
- package/src/js/fingerprint.utils.js +32 -5
- package/src/js/fingerprint.worker.js +81 -0
- package/src/js/library.js +6 -2
- package/src/js/pow.solver.js +2 -3
- package/src/js/problem-manager.js +113 -113
- package/src/js/tests/fingerprint.client.init.test.js +30 -0
- package/src/js/tests/fingerprint.test.js +136 -4
- package/src/js/tests/threatIntel.test.js +76 -0
- package/src/php/AutoTuner.php +36 -18
- package/src/php/Challenge/ChallengeUtils.php +121 -240
- package/src/php/DirectFingerprint.php +16 -4
- package/src/php/FingerprintClient.php +17 -19
- package/src/php/FingerprintEngine.php +77 -39
- package/src/php/Ja3AnomalyDetector.php +20 -12
- package/src/php/Optimization/Optimization.php +5 -1
- package/src/php/Optimization/OptimizationOperators.php +5 -1
- package/src/php/Optimization/ProblemInitializers.php +16 -4
- package/src/php/ProblemManager.php +20 -14
- package/src/php/Tests/ChallengeUtilsTest.php +469 -453
- package/src/php/Tests/FingerprintEngineTest.php +6 -1
- package/src/php/Tests/RequestUtilsTest.php +1 -1
- package/src/php/Tests/ThreatIntelTest.php +64 -0
- package/src/php/Utils/Env.php +51 -49
- package/src/php/Utils/MaliciousPatterns.php +74 -74
- package/src/php/Utils/RequestUtils.php +231 -132
- package/src/php/WordPress/WpDbStore.php +4 -0
- package/src/php/WordPress/{fingerprint-wordpress.php → fingerprint-anti-bot.php} +131 -126
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +1 -1
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +1 -1
- package/src/php/WordPress/package.php +6 -3
- package/src/php/WordPress/readme.txt +36 -0
- package/public/fingerprint-wordpress.zip +0 -0
package/src/js/fingerprint.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
modPow,
|
|
21
21
|
hashNetwork,
|
|
22
22
|
normalizeReferer,
|
|
23
|
+
isLoopbackIp,
|
|
23
24
|
isPrivateIp,
|
|
24
25
|
parseUserAgent,
|
|
25
26
|
safeJsonStringify
|
|
@@ -157,25 +158,12 @@ const loadBotWhitelist = (filename, fallbackEntries) => {
|
|
|
157
158
|
};
|
|
158
159
|
|
|
159
160
|
const googlebotEntries = loadBotWhitelist('googlebot.json', [
|
|
160
|
-
"2001:4860:4801:10::/64",
|
|
161
|
-
"2001:4860:4801:11::/64",
|
|
162
|
-
"2001:4860:4801:12::/64",
|
|
163
|
-
// ... [Keep fallback inline values for safety]
|
|
164
|
-
"66.249.79.64"
|
|
165
161
|
]);
|
|
166
162
|
|
|
167
163
|
const bingbotEntries = loadBotWhitelist('bingbot.json', [
|
|
168
|
-
"157.55.39.0/24",
|
|
169
|
-
"207.46.13.0/24",
|
|
170
|
-
// ... [Keep fallback inline values for safety]
|
|
171
|
-
"40.77.178.0/23"
|
|
172
164
|
]);
|
|
173
165
|
|
|
174
166
|
const yandexEntries = loadBotWhitelist('yandex.json', [
|
|
175
|
-
"2a02:6b8::/29",
|
|
176
|
-
"5.45.192.0/18",
|
|
177
|
-
// ... [Keep fallback inline values for safety]
|
|
178
|
-
"213.180.192.0/19"
|
|
179
167
|
]);
|
|
180
168
|
|
|
181
169
|
function generateSessionMapping() {
|
|
@@ -360,7 +348,7 @@ export function generateStatelessTicket(payload) {
|
|
|
360
348
|
* @param {object} context - Le contexte de la requête.
|
|
361
349
|
* @returns {{protocolAnomalyScore: number}}
|
|
362
350
|
*/
|
|
363
|
-
function getProtocolAnomalyScore(context) {
|
|
351
|
+
export function getProtocolAnomalyScore(context) {
|
|
364
352
|
let http2Anomaly = 0.0;
|
|
365
353
|
let quicAnomaly = 0.0;
|
|
366
354
|
|
|
@@ -373,9 +361,10 @@ function getProtocolAnomalyScore(context) {
|
|
|
373
361
|
const h2Fp = context.headers?.['x-http2-fingerprint'] || context.http2Fingerprint || null;
|
|
374
362
|
if (h2Fp && typeof h2Fp === 'string') {
|
|
375
363
|
const parts = h2Fp.split('|');
|
|
376
|
-
if (parts.length >=
|
|
364
|
+
if (parts.length >= 3) {
|
|
377
365
|
const connWindow = parseInt(parts[1], 10);
|
|
378
|
-
const
|
|
366
|
+
const streamPriority = parts[2] || '';
|
|
367
|
+
const headerOrder = parts.length > 3 ? parts[3] : '';
|
|
379
368
|
const isChromium = browser.startsWith('Chrome') || browser.startsWith('Edge');
|
|
380
369
|
const isFirefox = browser.startsWith('Firefox');
|
|
381
370
|
const isSafari = browser.startsWith('Safari');
|
|
@@ -383,11 +372,43 @@ function getProtocolAnomalyScore(context) {
|
|
|
383
372
|
if (isChromium) {
|
|
384
373
|
if (headerOrder && headerOrder !== 'm,a,s,p') http2Anomaly += 60.0;
|
|
385
374
|
if (connWindow === 65535 || connWindow === 65536) http2Anomaly += 40.0;
|
|
375
|
+
if (streamPriority === '0' || streamPriority === '') http2Anomaly += 50.0;
|
|
386
376
|
} else if (isFirefox) {
|
|
387
377
|
if (headerOrder && headerOrder !== 'm,s,p,a') http2Anomaly += 60.0;
|
|
388
378
|
} else if (isSafari) {
|
|
389
379
|
if (headerOrder && headerOrder !== 'm,s,p,a') http2Anomaly += 60.0;
|
|
390
380
|
}
|
|
381
|
+
|
|
382
|
+
// Analyse fine des trames (PRIORITY, WINDOW_UPDATE, CONTINUATION)
|
|
383
|
+
if (parts.length >= 5) {
|
|
384
|
+
const frameCountsStr = parts[4];
|
|
385
|
+
const frameCounts = {};
|
|
386
|
+
frameCountsStr.split(',').forEach(item => {
|
|
387
|
+
const kv = item.split(':');
|
|
388
|
+
if (kv.length === 2) {
|
|
389
|
+
frameCounts[kv[0]] = parseInt(kv[1], 10);
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
const priorityCount = frameCounts.p || 0;
|
|
394
|
+
const windowUpdateCount = frameCounts.w || 0;
|
|
395
|
+
const continuationCount = frameCounts.c || 0;
|
|
396
|
+
|
|
397
|
+
if (isChromium) {
|
|
398
|
+
// Chrome envoie des trames PRIORITY pour l'arbre de dépendances
|
|
399
|
+
if (priorityCount === 0) http2Anomaly += 25.0;
|
|
400
|
+
// Chrome est agressif avec les WINDOW_UPDATE
|
|
401
|
+
if (windowUpdateCount < 2) http2Anomaly += 20.0;
|
|
402
|
+
} else if (isFirefox) {
|
|
403
|
+
// Firefox utilise un schéma de priorité différent, souvent avec moins de trames PRIORITY
|
|
404
|
+
if (priorityCount > 1) http2Anomaly += 20.0;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Les bots génériques n'envoient souvent pas de trames CONTINUATION pour les en-têtes longs
|
|
408
|
+
if (continuationCount === 0 && (headerOrder.match(/,/g) || []).length > 3) {
|
|
409
|
+
http2Anomaly += 30.0;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
391
412
|
}
|
|
392
413
|
}
|
|
393
414
|
|
|
@@ -489,7 +510,7 @@ function getProtocolAnomalyScore(context) {
|
|
|
489
510
|
*/
|
|
490
511
|
function getRenderingAnomalyScore(context) {
|
|
491
512
|
const behaviorHeader = context.headers?.['x-behavior-metrics'];
|
|
492
|
-
if (!behaviorHeader) {
|
|
513
|
+
if (!behaviorHeader || !isNaN(Number(behaviorHeader))) {
|
|
493
514
|
return { renderingAnomalyScore: 0.0 };
|
|
494
515
|
}
|
|
495
516
|
try {
|
|
@@ -579,33 +600,45 @@ export function parseStatelessTicket(ticket) {
|
|
|
579
600
|
}
|
|
580
601
|
|
|
581
602
|
/**
|
|
582
|
-
* Vérifie le limiteur de débit Token Bucket pour les demandes de challenge d'un sous-réseau.
|
|
603
|
+
* Vérifie le limiteur de débit Token Bucket pour les demandes de challenge d'un sous-réseau par domaine.
|
|
583
604
|
* @param {string} clientIp - L'adresse IP du client.
|
|
605
|
+
* @param {string} [domain='default'] - Le domaine/hôte ciblé (en-tête Host).
|
|
606
|
+
* @param {object} [rateLimitConfig] - Configuration optionnelle du limiteur.
|
|
584
607
|
* @returns {Promise<boolean>} True si la requête est autorisée, false si elle est limitée.
|
|
585
608
|
*/
|
|
586
|
-
async function checkChallengeRateLimit(clientIp) {
|
|
587
|
-
|
|
609
|
+
async function checkChallengeRateLimit(clientIp, domain = 'default', rateLimitConfig = {}) {
|
|
610
|
+
if (rateLimitConfig && rateLimitConfig.enabled === false) {
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
613
|
+
if (!clientIp || isLoopbackIp(clientIp)) {
|
|
614
|
+
return true; // Bypass local/dev
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const subnet = getIpSubnet(clientIp) || clientIp;
|
|
588
618
|
if (!subnet) return false;
|
|
589
619
|
|
|
590
|
-
const
|
|
591
|
-
const
|
|
592
|
-
tokens: 5.0,
|
|
593
|
-
lastRefill: Date.now() / 1000
|
|
594
|
-
};
|
|
620
|
+
const host = (domain || 'default').toLowerCase().split(':')[0];
|
|
621
|
+
const key = `rate-limit:${host}:${subnet}`;
|
|
595
622
|
|
|
596
|
-
const capacity =
|
|
597
|
-
const refillRate = 0
|
|
623
|
+
const capacity = Number(rateLimitConfig?.capacity ?? 30.0);
|
|
624
|
+
const refillRate = Number(rateLimitConfig?.refillRate ?? 1.0); // 1 token par seconde (au lieu de 0.1)
|
|
598
625
|
const now = Date.now() / 1000;
|
|
599
626
|
|
|
600
|
-
const
|
|
627
|
+
const rateLimitData = (await store.get(key)) || {
|
|
628
|
+
tokens: capacity,
|
|
629
|
+
lastRefill: now
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
const elapsed = Math.max(0, now - rateLimitData.lastRefill);
|
|
601
633
|
const tokens = Math.min(capacity, rateLimitData.tokens + elapsed * refillRate);
|
|
634
|
+
const ttl = Math.max(60, Math.ceil(capacity / Math.max(0.1, refillRate)));
|
|
602
635
|
|
|
603
636
|
if (tokens < 1.0) {
|
|
604
|
-
await store.set(key, { tokens, lastRefill: now },
|
|
637
|
+
await store.set(key, { tokens, lastRefill: now }, ttl);
|
|
605
638
|
return false;
|
|
606
639
|
}
|
|
607
640
|
|
|
608
|
-
await store.set(key, { tokens: tokens - 1.0, lastRefill: now },
|
|
641
|
+
await store.set(key, { tokens: tokens - 1.0, lastRefill: now }, ttl);
|
|
609
642
|
return true;
|
|
610
643
|
}
|
|
611
644
|
|
|
@@ -655,18 +688,22 @@ const securityProfiles = {
|
|
|
655
688
|
headerAnomalyScore: 0.1,
|
|
656
689
|
requestPatternScore: 0.6,
|
|
657
690
|
inconsistencyScore: 0.8,
|
|
658
|
-
behaviorScore: 0.7,
|
|
691
|
+
behaviorScore: 0.7,
|
|
659
692
|
honeypotScore: 1.0,
|
|
693
|
+
botScore: 1.0,
|
|
694
|
+
cookieDroppingScore: 0.9,
|
|
660
695
|
crossLayerInconsistencyScore: 0.4,
|
|
661
696
|
timeInconsistencyScore: 0.9,
|
|
662
|
-
tlsSpoofingScore: 0.8,
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
697
|
+
tlsSpoofingScore: 0.8,
|
|
698
|
+
clientHintsInconsistencyScore: 0.7,
|
|
699
|
+
clickVarianceScore: 0.6,
|
|
700
|
+
subnetScore: 0.4,
|
|
701
|
+
ipReputationScore: 0.5,
|
|
702
|
+
botnetClusterScore: 0.6,
|
|
703
|
+
tcpAnomalyScore: 0.8,
|
|
667
704
|
quicAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie QUIC
|
|
668
|
-
protocolAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie HTTP/2
|
|
669
705
|
renderingAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie de rendu
|
|
706
|
+
http2AnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie HTTP/2
|
|
670
707
|
threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
|
|
671
708
|
virtualizationScore: 0.8,
|
|
672
709
|
},
|
|
@@ -702,15 +739,22 @@ const securityProfiles = {
|
|
|
702
739
|
inconsistencyScore: 1.0,
|
|
703
740
|
behaviorScore: 0.8,
|
|
704
741
|
honeypotScore: 1.0,
|
|
742
|
+
botScore: 1.0,
|
|
743
|
+
cookieDroppingScore: 1.0,
|
|
705
744
|
crossLayerInconsistencyScore: 0.6,
|
|
706
745
|
timeInconsistencyScore: 1.0,
|
|
707
|
-
tlsSpoofingScore: 1.0,
|
|
746
|
+
tlsSpoofingScore: 1.0,
|
|
747
|
+
clientHintsInconsistencyScore: 0.9,
|
|
748
|
+
clickVarianceScore: 0.7,
|
|
708
749
|
subnetScore: 0.5,
|
|
709
|
-
ipReputationScore: 0.6,
|
|
710
|
-
botnetClusterScore: 0.8,
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
750
|
+
ipReputationScore: 0.6,
|
|
751
|
+
botnetClusterScore: 0.8,
|
|
752
|
+
tcpAnomalyScore: 1.0,
|
|
753
|
+
quicAnomalyScore: 1.0,
|
|
754
|
+
renderingAnomalyScore: 1.0,
|
|
755
|
+
http2AnomalyScore: 1.0,
|
|
756
|
+
threatIntelScore: 1.0,
|
|
757
|
+
virtualizationScore: 1.0,
|
|
714
758
|
},
|
|
715
759
|
thresholds: { low: 10, medium: 35, high: 65, block: 90 },
|
|
716
760
|
patterns: {
|
|
@@ -745,15 +789,22 @@ const securityProfiles = {
|
|
|
745
789
|
inconsistencyScore: 0.7,
|
|
746
790
|
behaviorScore: 0.2, // Lower weight, as browser behavior is not applicable
|
|
747
791
|
honeypotScore: 1.0,
|
|
792
|
+
botScore: 0.8,
|
|
793
|
+
cookieDroppingScore: 0.8,
|
|
748
794
|
crossLayerInconsistencyScore: 0.5,
|
|
749
795
|
timeInconsistencyScore: 0.8,
|
|
750
|
-
tlsSpoofingScore: 0.7,
|
|
796
|
+
tlsSpoofingScore: 0.7,
|
|
797
|
+
clientHintsInconsistencyScore: 0.6,
|
|
798
|
+
clickVarianceScore: 0.3,
|
|
751
799
|
subnetScore: 0.4,
|
|
752
|
-
ipReputationScore: 0.5,
|
|
753
|
-
botnetClusterScore: 0.7,
|
|
754
|
-
tcpAnomalyScore: 0.8,
|
|
755
|
-
|
|
756
|
-
|
|
800
|
+
ipReputationScore: 0.5,
|
|
801
|
+
botnetClusterScore: 0.7,
|
|
802
|
+
tcpAnomalyScore: 0.8,
|
|
803
|
+
quicAnomalyScore: 0.8,
|
|
804
|
+
http2AnomalyScore: 0.8,
|
|
805
|
+
renderingAnomalyScore: 0.2,
|
|
806
|
+
threatIntelScore: 0.6,
|
|
807
|
+
virtualizationScore: 0.8,
|
|
757
808
|
},
|
|
758
809
|
thresholds: { low: 25, medium: 50, high: 80, block: 95 },
|
|
759
810
|
patterns: {
|
|
@@ -800,9 +851,10 @@ const securityProfiles = {
|
|
|
800
851
|
ipReputationScore: 0.3, // NOUVEAU: Poids pour la réputation IP
|
|
801
852
|
botnetClusterScore: 0.5, // NOUVEAU: Poids pour le clustering botnet
|
|
802
853
|
tcpAnomalyScore: 0.5, // NEW: Anomalie de pile TCP/IP
|
|
803
|
-
protocolAnomalyScore: 0.5,
|
|
804
854
|
quicAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie QUIC
|
|
855
|
+
http2AnomalyScore: 0.5,
|
|
805
856
|
renderingAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie de rendu
|
|
857
|
+
virtualizationScore: 0.8,
|
|
806
858
|
},
|
|
807
859
|
thresholds: { low: 25, medium: 55, high: 80, block: 95 },
|
|
808
860
|
patterns: {
|
|
@@ -831,7 +883,6 @@ const securityProfiles = {
|
|
|
831
883
|
historyScore: 0.4,
|
|
832
884
|
rotationScore: 0.6,
|
|
833
885
|
headerAnomalyScore: 0.2,
|
|
834
|
-
// Utilisation d'un score de pattern unifié avec un poids très élevé
|
|
835
886
|
requestPatternScore: 0.9,
|
|
836
887
|
inconsistencyScore: 1.0, // Crucial for preventing account takeover
|
|
837
888
|
behaviorScore: 0.8, // Important for checkout/login forms
|
|
@@ -848,9 +899,10 @@ const securityProfiles = {
|
|
|
848
899
|
ipReputationScore: 0.6, // NOUVEAU: Poids pour la réputation IP
|
|
849
900
|
botnetClusterScore: 0.9, // NOUVEAU: Poids pour le clustering botnet
|
|
850
901
|
tcpAnomalyScore: 0.9, // NEW: Anomalie de pile TCP/IP
|
|
851
|
-
protocolAnomalyScore: 0.9,
|
|
852
902
|
quicAnomalyScore: 0.9, // NOUVEAU: Poids pour l'anomalie QUIC
|
|
853
|
-
|
|
903
|
+
http2AnomalyScore: 0.9,
|
|
904
|
+
renderingAnomalyScore: 0.9,
|
|
905
|
+
virtualizationScore: 0.8,
|
|
854
906
|
},
|
|
855
907
|
thresholds: { low: 15, medium: 40, high: 70, block: 90 },
|
|
856
908
|
patterns: {
|
|
@@ -1465,88 +1517,6 @@ export const verifyTspChallenge = (
|
|
|
1465
1517
|
}
|
|
1466
1518
|
};
|
|
1467
1519
|
|
|
1468
|
-
/**
|
|
1469
|
-
* Generates the HTML content for a memory-intensive PoW challenge.
|
|
1470
|
-
*/
|
|
1471
|
-
const generateMemoryPoWChallenge = (
|
|
1472
|
-
clientIp,
|
|
1473
|
-
nonce,
|
|
1474
|
-
difficulty = 16,
|
|
1475
|
-
path = "",
|
|
1476
|
-
) => {
|
|
1477
|
-
const safePath = sanitizeRedirectPath(path);
|
|
1478
|
-
// difficulty here is the buffer size in MB.
|
|
1479
|
-
return `
|
|
1480
|
-
<html>
|
|
1481
|
-
<head><title>Advanced Security Check</title></head>
|
|
1482
|
-
<body style="font-family:sans-serif; text-align:center; padding-top:50px;">
|
|
1483
|
-
<h1>Enhanced Verification... (Level 2)</h1>
|
|
1484
|
-
<p>Your activity requires an additional security check.</p>
|
|
1485
|
-
<div id="loader" style="margin:20px;">⚙️ Performing memory allocation and calculation... (${difficulty} MB)</div>
|
|
1486
|
-
<script>
|
|
1487
|
-
async function solve() {
|
|
1488
|
-
const nonce = ${safeJsonStringify(nonce)};
|
|
1489
|
-
const size = ${difficulty} * 1024 * 1024; // en octets
|
|
1490
|
-
const iterations = size / 16;
|
|
1491
|
-
|
|
1492
|
-
try {
|
|
1493
|
-
const buffer = new Uint32Array(size / 4);
|
|
1494
|
-
let h = new TextEncoder().encode(nonce).reduce((acc, v) => acc + v, 0);
|
|
1495
|
-
for (let i = 0; i < buffer.length; i++) {
|
|
1496
|
-
buffer[i] = (h = Math.imul(h ^ i, 1597334677));
|
|
1497
|
-
}
|
|
1498
|
-
|
|
1499
|
-
let finalHash = 0;
|
|
1500
|
-
for(let i = 0; i < iterations; i++) {
|
|
1501
|
-
const addr = buffer[i % buffer.length] % buffer.length;
|
|
1502
|
-
finalHash ^= buffer[addr];
|
|
1503
|
-
}
|
|
1504
|
-
window.location.href = "${path}" + "?pow_type=mem&pow_nonce=" + nonce + "&pow_solution=" + finalHash;
|
|
1505
|
-
} catch(e) {
|
|
1506
|
-
window.location.href = ${safeJsonStringify(safePath)} + "?pow_type=mem&pow_nonce=" + nonce + "&pow_solution=" + finalHash;
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
solve();
|
|
1510
|
-
</script>
|
|
1511
|
-
</body>
|
|
1512
|
-
</html>`;
|
|
1513
|
-
};
|
|
1514
|
-
|
|
1515
|
-
/**
|
|
1516
|
-
* Verifies if a PoW solution is valid and generates a clearance ticket.
|
|
1517
|
-
*/
|
|
1518
|
-
export const verifyPoWAndGenerateTicket = async (
|
|
1519
|
-
ip,
|
|
1520
|
-
nonce,
|
|
1521
|
-
solution,
|
|
1522
|
-
difficulty = 4,
|
|
1523
|
-
deviceId = '',
|
|
1524
|
-
deviceHash = ''
|
|
1525
|
-
) => {
|
|
1526
|
-
// 1. Verify the solution: hash(ip + nonce + solution) must start with N zeros
|
|
1527
|
-
const hash = crypto
|
|
1528
|
-
.createHash("sha256")
|
|
1529
|
-
.update(`${ip}:${nonce}:${solution}`)
|
|
1530
|
-
.digest("hex");
|
|
1531
|
-
|
|
1532
|
-
if (!hash.startsWith("0".repeat(difficulty))) {
|
|
1533
|
-
return null;
|
|
1534
|
-
}
|
|
1535
|
-
|
|
1536
|
-
// 2. Generate a signed and encrypted stateless ticket
|
|
1537
|
-
const expiry = Date.now() + 3600000; // 1 hour
|
|
1538
|
-
const payload = {
|
|
1539
|
-
expiry,
|
|
1540
|
-
originalIp: ip,
|
|
1541
|
-
deviceId,
|
|
1542
|
-
deviceHash
|
|
1543
|
-
};
|
|
1544
|
-
|
|
1545
|
-
return generateStatelessTicket(payload);
|
|
1546
|
-
};
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
1520
|
/**
|
|
1551
1521
|
* Verifies a memory PoW solution.
|
|
1552
1522
|
*
|
|
@@ -2334,13 +2304,37 @@ function analyzeTouchMovements(history) {
|
|
|
2334
2304
|
* @returns {{behaviorScore: number}}
|
|
2335
2305
|
*/
|
|
2336
2306
|
function getBehaviorScore(context) {
|
|
2337
|
-
const behaviorHeader = context.headers["x-behavior-metrics"];
|
|
2338
|
-
if (
|
|
2307
|
+
const behaviorHeader = context.headers ? (context.headers["x-behavior-metrics"] ?? context.headers["X-Behavior-Metrics"]) : undefined;
|
|
2308
|
+
if (behaviorHeader === undefined || behaviorHeader === null || behaviorHeader === '') {
|
|
2339
2309
|
return { behaviorScore: 0 }; // Pas de données, pas de pénalité.
|
|
2340
2310
|
}
|
|
2341
2311
|
|
|
2312
|
+
// Traitement direct si un flag binaire client-side (1 = humain, 0 = bot) est envoyé
|
|
2313
|
+
if (behaviorHeader === 1 || behaviorHeader === '1') {
|
|
2314
|
+
return { behaviorScore: 0 };
|
|
2315
|
+
}
|
|
2316
|
+
if (behaviorHeader === 0 || behaviorHeader === '0') {
|
|
2317
|
+
return { behaviorScore: 100 };
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2320
|
+
// Traitement direct si un score numérique est envoyé côté client
|
|
2321
|
+
if (typeof behaviorHeader === 'number') {
|
|
2322
|
+
return { behaviorScore: Math.max(0, Math.min(100, behaviorHeader)) };
|
|
2323
|
+
}
|
|
2324
|
+
if (typeof behaviorHeader === 'string' && !isNaN(Number(behaviorHeader)) && behaviorHeader.trim() !== '') {
|
|
2325
|
+
const trimmed = behaviorHeader.trim();
|
|
2326
|
+
if (trimmed === '1') return { behaviorScore: 0 };
|
|
2327
|
+
if (trimmed === '0') return { behaviorScore: 100 };
|
|
2328
|
+
return { behaviorScore: Math.max(0, Math.min(100, parseFloat(trimmed))) };
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2342
2331
|
try {
|
|
2343
2332
|
const metrics = JSON.parse(behaviorHeader);
|
|
2333
|
+
if (metrics === 1) return { behaviorScore: 0 };
|
|
2334
|
+
if (metrics === 0) return { behaviorScore: 100 };
|
|
2335
|
+
if (typeof metrics === 'number') {
|
|
2336
|
+
return { behaviorScore: Math.max(0, Math.min(100, metrics)) };
|
|
2337
|
+
}
|
|
2344
2338
|
let score = 0;
|
|
2345
2339
|
|
|
2346
2340
|
// 1. Pénalité maximale si un honeypot client a été déclenché.
|
|
@@ -2354,9 +2348,18 @@ function getBehaviorScore(context) {
|
|
|
2354
2348
|
const { avgSpeed, avgAcceleration, straightness, pauses, segments } = analyzeMouseMovements(metrics.mouseMovementsHistory);
|
|
2355
2349
|
const touchAnalysis = analyzeTouchMovements(metrics.touchMovementsHistory);
|
|
2356
2350
|
|
|
2357
|
-
// Pénalité pour absence totale d'interaction
|
|
2351
|
+
// Pénalité pour absence totale d'interaction. Un utilisateur légitime peut simplement lire la page.
|
|
2352
|
+
// On applique donc une pénalité de base faible, qui est amplifiée uniquement si d'autres
|
|
2353
|
+
// signaux passifs de bot (ex: rendu offscreen) sont présents.
|
|
2358
2354
|
if (avgSpeed === 0 && touchAnalysis.avgSpeed === 0 && metrics.keystrokeLatency === 0) {
|
|
2359
|
-
|
|
2355
|
+
let noInteractionPenalty = 5; // Pénalité de base très faible.
|
|
2356
|
+
|
|
2357
|
+
// Amplification si d'autres signaux passifs de bot sont présents.
|
|
2358
|
+
if (metrics.rendering?.offscreenAnom) {
|
|
2359
|
+
noInteractionPenalty += 40;
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
score += noInteractionPenalty;
|
|
2360
2363
|
}
|
|
2361
2364
|
|
|
2362
2365
|
// 3. (NOUVEAU) Analyse de la longueur de l'historique de navigation.
|
|
@@ -2396,21 +2399,19 @@ function getBehaviorScore(context) {
|
|
|
2396
2399
|
if (touch.avgRadius > 0 && touch.radiusVariance === 0) {
|
|
2397
2400
|
score += 30; // Spoofed pointer area size
|
|
2398
2401
|
}
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
if (benfordDev > 0.18) score += 35;
|
|
2403
|
-
}
|
|
2404
|
-
|
|
2405
|
-
// Détection de ferme mobile : Touch actif sur mobile sans aucune vibration physique (châssis/rack ADB)
|
|
2406
|
-
const isMobileDevice = (context.headers['user-agent'] || '').includes('Mobile');
|
|
2407
|
-
if (isMobileDevice && touchHistory.length >= 5 && typeof metrics.motionVariance === 'number') {
|
|
2408
|
-
if (metrics.motionVariance === 0) {
|
|
2409
|
-
score += 50; // Terminal fixé sur un châssis mécanique (rack ADB)
|
|
2402
|
+
if (touch.segments.length > 10) {
|
|
2403
|
+
const benfordDev = Optimization.Operators.benfordTest(touch.segments);
|
|
2404
|
+
if (benfordDev > 0.18) score += 35;
|
|
2410
2405
|
}
|
|
2411
2406
|
}
|
|
2412
2407
|
}
|
|
2413
2408
|
|
|
2409
|
+
// Détection de ferme mobile : un appareil mobile parfaitement immobile est suspect, indépendamment des interactions tactiles.
|
|
2410
|
+
const isMobileDevice = (context.headers['user-agent'] || '').includes('Mobile');
|
|
2411
|
+
if (isMobileDevice && typeof metrics.motionVariance === 'number' && metrics.motionVariance === 0) {
|
|
2412
|
+
score += 50; // Terminal fixé sur un châssis mécanique (rack ADB)
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2414
2415
|
// Plausibilité de la latence de frappe
|
|
2415
2416
|
if (metrics.keystrokeLatency > 0 && metrics.keystrokeLatency < 40) score += 25; // Frappe trop rapide pour un humain.
|
|
2416
2417
|
if (metrics.keystrokeLatency > 1000) score += 15; // Latence très élevée, peut être un script lent.
|
|
@@ -2666,9 +2667,6 @@ export function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerp
|
|
|
2666
2667
|
|
|
2667
2668
|
// Check for known spoofed/suspicious JA4 fingerprints
|
|
2668
2669
|
const spoofedJa4s = [
|
|
2669
|
-
't13d1516h2_8daaf6152771_390237aa04be', // Chrome classique (curl-impersonate / tls-client)
|
|
2670
|
-
't13d1413h2_bc66258908f0_bc2531da1615', // Firefox statique (curl-impersonate-ff / curl_cffi)
|
|
2671
|
-
't13d1515h2_8daaf6152771_a729e2f67de4', // Safari statique (curl-impersonate-safari / tls-client)
|
|
2672
2670
|
't13d1516h2_8daaf6152771_4be0df930c2c', // Alternatif Chrome (tls-client Go)
|
|
2673
2671
|
't12d1516h2_8daaf6152771_390237aa04be', // Chrome usurpé dégradé en TLS 1.2
|
|
2674
2672
|
't13d1516h2_e822d36d892d_93ec3f0b2f5b' // Scraping bot OpenSSL customisé
|
|
@@ -2983,6 +2981,10 @@ function analyzeClickPositions(history) {
|
|
|
2983
2981
|
* @returns {{clickVarianceScore: number}}
|
|
2984
2982
|
*/
|
|
2985
2983
|
function getClickVarianceScore(context) {
|
|
2984
|
+
const rawMetrics = context.headers?.['x-behavior-metrics'];
|
|
2985
|
+
if (!rawMetrics || !isNaN(Number(rawMetrics))) {
|
|
2986
|
+
return { clickVarianceScore: 0 };
|
|
2987
|
+
}
|
|
2986
2988
|
const metrics = JSON.parse(context.headers['x-behavior-metrics'] || '{}');
|
|
2987
2989
|
const score = analyzeClickPositions(metrics.clicksHistory);
|
|
2988
2990
|
return { clickVarianceScore: score };
|
|
@@ -3162,6 +3164,9 @@ function decaySubnetData(subnetData, now) {
|
|
|
3162
3164
|
* @param {number} finalScore The final suspicion score.
|
|
3163
3165
|
*/
|
|
3164
3166
|
async function updateSubnetMetrics(context, deviceId, finalScore) {
|
|
3167
|
+
if (!context.clientIp || isLoopbackIp(context.clientIp)) {
|
|
3168
|
+
return;
|
|
3169
|
+
}
|
|
3165
3170
|
const subnet = getIpSubnet(context.clientIp);
|
|
3166
3171
|
if (!subnet) return;
|
|
3167
3172
|
|
|
@@ -3194,7 +3199,8 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
|
|
|
3194
3199
|
|
|
3195
3200
|
// Utilisation d'un identifiant d'appareil stable (fingerprint matériel) plutôt que l'ID de cookie volatil
|
|
3196
3201
|
const currentDeviceHash = getCompositeDeviceHash(context);
|
|
3197
|
-
const
|
|
3202
|
+
const stablePart = extractStablePart(currentDeviceHash);
|
|
3203
|
+
const stableFpId = stablePart ? cyrb53(stablePart).toString() : (deviceId || cyrb53(currentDeviceHash).toString());
|
|
3198
3204
|
|
|
3199
3205
|
const currentDeviceContributions = subnetData.highScoreDevices[stableFpId] || 0;
|
|
3200
3206
|
if (currentDeviceContributions < 1) {
|
|
@@ -3212,7 +3218,9 @@ async function updateSubnetMetrics(context, deviceId, finalScore) {
|
|
|
3212
3218
|
|
|
3213
3219
|
const userAgent = context.headers?.['user-agent'] || '';
|
|
3214
3220
|
|
|
3215
|
-
|
|
3221
|
+
// Ne pas enregistrer une IP en attaquant si le score élevé provient uniquement d'un seul appareil isolé
|
|
3222
|
+
const isConfirmedClusterAttack = (subnetData.deviceIds.length > 1 && finalScore >= 70) || finalScore >= 95;
|
|
3223
|
+
if (isConfirmedClusterAttack) {
|
|
3216
3224
|
const existingAttacker = subnetData.attackerIps.find(a => a.ip === context.clientIp);
|
|
3217
3225
|
if (existingAttacker) {
|
|
3218
3226
|
existingAttacker.lastSeen = now;
|
|
@@ -3272,6 +3280,9 @@ export function calculateAnalogInconsistencyScore(consistencyScore, inflectionPo
|
|
|
3272
3280
|
* @returns {Promise<{subnetScore: number}>}
|
|
3273
3281
|
*/
|
|
3274
3282
|
async function getSubnetScore(context, deviceId = '', securityConfig = null) {
|
|
3283
|
+
if (!context.clientIp || isLoopbackIp(context.clientIp)) {
|
|
3284
|
+
return { subnetScore: 0.0 };
|
|
3285
|
+
}
|
|
3275
3286
|
if (typeof deviceId === 'object' && deviceId !== null && !securityConfig) {
|
|
3276
3287
|
securityConfig = deviceId;
|
|
3277
3288
|
}
|
|
@@ -3299,7 +3310,7 @@ async function getSubnetScore(context, deviceId = '', securityConfig = null) {
|
|
|
3299
3310
|
}
|
|
3300
3311
|
let uaCount = subnetData.uas ? subnetData.uas.length : 1;
|
|
3301
3312
|
|
|
3302
|
-
if (deviceCount
|
|
3313
|
+
if (deviceCount <= 1 && highScoreCount <= 1) {
|
|
3303
3314
|
return { subnetScore: 0.0 };
|
|
3304
3315
|
}
|
|
3305
3316
|
|
|
@@ -3825,7 +3836,7 @@ async function getBehavioralIndicators(context, deviceData) {
|
|
|
3825
3836
|
// --- VALIDATION DE L'ANCRAGE MATÉRIEL WEBAUTHN ---
|
|
3826
3837
|
const behaviorHeader = context.headers?.['x-behavior-metrics'];
|
|
3827
3838
|
let webauthnVerified = false;
|
|
3828
|
-
if (behaviorHeader) {
|
|
3839
|
+
if (behaviorHeader && typeof behaviorHeader === 'string' && behaviorHeader.startsWith('{')) {
|
|
3829
3840
|
try {
|
|
3830
3841
|
const metrics = JSON.parse(behaviorHeader);
|
|
3831
3842
|
if (metrics && metrics.webauthnAnchor) {
|
|
@@ -3948,7 +3959,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
3948
3959
|
_ // store.set result
|
|
3949
3960
|
] = await Promise.all([
|
|
3950
3961
|
getBehavioralIndicators(context, deviceData), // This modifies deviceData, so it must be done before saving deviceData
|
|
3951
|
-
getThreatIntelScore(context, zkpY), // NOUVEAU: Score de Threat Intelligence Fédéré
|
|
3962
|
+
getThreatIntelScore(context, zkpY, securityConfig), // NOUVEAU: Score de Threat Intelligence Fédéré
|
|
3952
3963
|
getTlsSpoofingScore(context),
|
|
3953
3964
|
getSubnetScore(context, deviceId, securityConfig),
|
|
3954
3965
|
getIpReputationScore(clientIp),
|
|
@@ -3969,7 +3980,12 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
3969
3980
|
const { botScore } = getBotScore(context);
|
|
3970
3981
|
|
|
3971
3982
|
// NOUVEAU: On calcule le score d'incohérence temporelle.
|
|
3972
|
-
|
|
3983
|
+
let parsedBehaviorMetrics = {};
|
|
3984
|
+
const rawBehaviorHeader = context.headers?.['x-behavior-metrics'];
|
|
3985
|
+
if (rawBehaviorHeader && typeof rawBehaviorHeader === 'string' && rawBehaviorHeader.startsWith('{')) {
|
|
3986
|
+
try { parsedBehaviorMetrics = JSON.parse(rawBehaviorHeader); } catch (e) {}
|
|
3987
|
+
}
|
|
3988
|
+
const { timeInconsistencyScore } = getTimeInconsistencyScore(context, parsedBehaviorMetrics, deviceData);
|
|
3973
3989
|
|
|
3974
3990
|
// NOUVEAU: On calcule le score d'incohérence entre les couches.
|
|
3975
3991
|
const { crossLayerInconsistencyScore } = getCrossLayerInconsistency(context);
|
|
@@ -3983,7 +3999,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
3983
3999
|
const { requestPatternScore } = getRequestPatternScore(context, deviceData, securityConfig.patterns);
|
|
3984
4000
|
|
|
3985
4001
|
const { tcpAnomalyScore } = getTcpAnomalyScore(context);
|
|
3986
|
-
const { protocolAnomalyScore } = getProtocolAnomalyScore(context);
|
|
4002
|
+
const { protocolAnomalyScore, http2AnomalyScore, quicAnomalyScore } = getProtocolAnomalyScore(context);
|
|
3987
4003
|
const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
|
|
3988
4004
|
const virtualizationScore = getVirtualizationAnomalyScore(context);
|
|
3989
4005
|
|
|
@@ -3996,8 +4012,8 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
3996
4012
|
if (Array.isArray(deviceData.ips)) {
|
|
3997
4013
|
deviceData.ips = new Set(deviceData.ips);
|
|
3998
4014
|
}
|
|
3999
|
-
|
|
4000
|
-
|
|
4015
|
+
// Le vecteur de suspicion est maintenant complet.
|
|
4016
|
+
return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, protocolAnomalyScore, http2AnomalyScore, quicAnomalyScore, renderingAnomalyScore, threatIntelScore, virtualizationScore };
|
|
4001
4017
|
};
|
|
4002
4018
|
|
|
4003
4019
|
// A residential user can change networks (home, 4G, public wifi).
|
|
@@ -4020,7 +4036,31 @@ export const identifyRequest = (securityConfig) => async (req, res) => {
|
|
|
4020
4036
|
// This function now acts as a lightweight wrapper around the engine's identifyRequest method.
|
|
4021
4037
|
// It requires a default configuration to work.
|
|
4022
4038
|
const config = securityConfig || {
|
|
4023
|
-
weights: {
|
|
4039
|
+
weights: {
|
|
4040
|
+
historyScore: 0.3,
|
|
4041
|
+
rotationScore: 0.5,
|
|
4042
|
+
headerAnomalyScore: 0.2,
|
|
4043
|
+
requestPatternScore: 0.6,
|
|
4044
|
+
inconsistencyScore: 0.8,
|
|
4045
|
+
behaviorScore: 0.7,
|
|
4046
|
+
honeypotScore: 1.0,
|
|
4047
|
+
botScore: 1.0,
|
|
4048
|
+
cookieDroppingScore: 0.9,
|
|
4049
|
+
crossLayerInconsistencyScore: 0.4,
|
|
4050
|
+
timeInconsistencyScore: 0.9,
|
|
4051
|
+
tlsSpoofingScore: 0.8,
|
|
4052
|
+
clientHintsInconsistencyScore: 0.7,
|
|
4053
|
+
clickVarianceScore: 0.6,
|
|
4054
|
+
subnetScore: 0.4,
|
|
4055
|
+
ipReputationScore: 0.5,
|
|
4056
|
+
botnetClusterScore: 0.7,
|
|
4057
|
+
tcpAnomalyScore: 0.8,
|
|
4058
|
+
quicAnomalyScore: 0.8,
|
|
4059
|
+
renderingAnomalyScore: 0.8,
|
|
4060
|
+
http2AnomalyScore: 0.8,
|
|
4061
|
+
threatIntelScore: 1.0,
|
|
4062
|
+
virtualizationScore: 0.8
|
|
4063
|
+
},
|
|
4024
4064
|
thresholds: { low: 20, medium: 40, high: 75 },
|
|
4025
4065
|
honeypot: { fields: [] } // Ensure honeypot config exists to prevent errors
|
|
4026
4066
|
};
|
|
@@ -4383,40 +4423,68 @@ function parseGraphQLQuery(body) {
|
|
|
4383
4423
|
* @param {string} zkpY - The 'y' component of the ZKP proof (public key).
|
|
4384
4424
|
* @returns {Promise<{threatIntelScore: number}>}
|
|
4385
4425
|
*/
|
|
4386
|
-
async function getThreatIntelScore(context, zkpY) {
|
|
4387
|
-
let score = 0;
|
|
4426
|
+
async function getThreatIntelScore(context, zkpY, threatIntelConfig = {}) {
|
|
4427
|
+
let score = 0.0;
|
|
4428
|
+
const signals = [];
|
|
4429
|
+
|
|
4430
|
+
// 1. Détection déterministe : Clé publique ZKP bannie par consensus fédéré
|
|
4388
4431
|
if (zkpY) {
|
|
4389
4432
|
const isBanned = await store.has(`banned-zkp-y:${zkpY}`);
|
|
4390
4433
|
if (isBanned) {
|
|
4391
|
-
score = 100;
|
|
4434
|
+
score = 100.0;
|
|
4435
|
+
signals.push({
|
|
4436
|
+
ruleId: 'FEDERATED_ZKP_BANNED',
|
|
4437
|
+
confidence: 1.0,
|
|
4438
|
+
score: 100.0,
|
|
4439
|
+
rationale: 'Cryptographic identity matched banned list consensus'
|
|
4440
|
+
});
|
|
4392
4441
|
}
|
|
4393
4442
|
}
|
|
4394
4443
|
|
|
4444
|
+
// 2. Détection physique : Modélisation continue du différentiel de transport (RTT TCP Edge vs Transit Applicatif)
|
|
4445
|
+
// Rationale : Un RTT TCP de socket très faible (datacenter/edge CDN) couplé à un délai applicatif WAN élevé
|
|
4446
|
+
// prouve l'interposition d'un relais/tunnel/proxy résidentiel entre le client réel et le point d'entrée.
|
|
4395
4447
|
const tcpRttHeader = context.headers?.['x-tcp-rtt'] || context.headers?.['x-real-rtt'];
|
|
4396
4448
|
const tcpRtt = tcpRttHeader ? parseInt(tcpRttHeader, 10) : null;
|
|
4397
4449
|
|
|
4398
4450
|
const behaviorHeader = context.headers?.['x-behavior-metrics'];
|
|
4399
|
-
if (behaviorHeader) {
|
|
4451
|
+
if (behaviorHeader && typeof behaviorHeader === 'string' && behaviorHeader.startsWith('{')) {
|
|
4400
4452
|
try {
|
|
4401
4453
|
const metrics = JSON.parse(behaviorHeader);
|
|
4402
|
-
if (metrics && metrics.clientTimestamp && context.requestTimestamp) {
|
|
4454
|
+
if (metrics && typeof metrics.clientTimestamp === 'number' && typeof context.requestTimestamp === 'number') {
|
|
4403
4455
|
const appLatency = context.requestTimestamp - metrics.clientTimestamp;
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4456
|
+
|
|
4457
|
+
// Évaluation uniquement si la latence est mesurable et positive avec une mesure socket RTT valide
|
|
4458
|
+
if (tcpRtt !== null && !isNaN(tcpRtt) && tcpRtt > 0 && appLatency > 0) {
|
|
4459
|
+
// Marge de tolérance contre le jitter réseau et le scheduling JS (Garbage Collector, event-loop)
|
|
4460
|
+
const jitterAllowance = 60.0;
|
|
4461
|
+
const effectiveRtt = Math.max(tcpRtt, 5.0);
|
|
4462
|
+
const tunnelDelta = appLatency - (tcpRtt + jitterAllowance);
|
|
4463
|
+
const divergenceRatio = appLatency / effectiveRtt;
|
|
4464
|
+
|
|
4465
|
+
// Condition de disjonction : liaison edge ultra-proche (< 40ms) + transit applicatif au moins 3x supérieur
|
|
4466
|
+
if (tcpRtt <= 40 && divergenceRatio >= 3.0 && tunnelDelta > 0) {
|
|
4467
|
+
// Progression sigmoïdale continue calibrée sur la propagation optique intercontinentale (120ms)
|
|
4468
|
+
const scaling = 120.0;
|
|
4469
|
+
const ratioWeight = Math.min(1.0, (divergenceRatio - 3.0) / 5.0);
|
|
4470
|
+
const proxyScore = Math.min(95.0, 40.0 + 55.0 * Math.tanh(tunnelDelta / scaling) * ratioWeight);
|
|
4471
|
+
const finalProxyScore = Math.round(proxyScore * 10) / 10;
|
|
4472
|
+
|
|
4473
|
+
score = Math.max(score, finalProxyScore);
|
|
4474
|
+
signals.push({
|
|
4475
|
+
ruleId: 'RESIDENTIAL_PROXY_RTT_DISCREPANCY',
|
|
4476
|
+
confidence: Math.round(ratioWeight * 100) / 100,
|
|
4477
|
+
score: finalProxyScore,
|
|
4478
|
+
rationale: `TCP RTT (${tcpRtt}ms) diverges from application transit (${appLatency}ms) with ratio ${divergenceRatio.toFixed(1)}:1`
|
|
4479
|
+
});
|
|
4412
4480
|
}
|
|
4413
4481
|
}
|
|
4414
4482
|
}
|
|
4415
4483
|
} catch (e) {
|
|
4416
|
-
// Ignorer silencieusement
|
|
4484
|
+
// Ignorer silencieusement les erreurs de parsing des métriques
|
|
4417
4485
|
}
|
|
4418
4486
|
}
|
|
4419
|
-
return { threatIntelScore: score };
|
|
4487
|
+
return { threatIntelScore: score, threatIntelSignals: signals };
|
|
4420
4488
|
}
|
|
4421
4489
|
|
|
4422
4490
|
export class FingerprintEngine {
|
|
@@ -4542,7 +4610,8 @@ export class FingerprintEngine {
|
|
|
4542
4610
|
'wasm',
|
|
4543
4611
|
'similarityThreshold', 'reset',
|
|
4544
4612
|
'ed25519_private_key', 'ed25519_public_key', 'upowModel',
|
|
4545
|
-
'federatedPeers', 'federationSecret', 'filterWhitelist'
|
|
4613
|
+
'federatedPeers', 'federationSecret', 'filterWhitelist',
|
|
4614
|
+
'challengeRateLimit'
|
|
4546
4615
|
]);
|
|
4547
4616
|
|
|
4548
4617
|
// 1. Check for essential keys
|
|
@@ -4602,7 +4671,9 @@ export class FingerprintEngine {
|
|
|
4602
4671
|
(suspicionVector.ipReputationScore || 0) * (weights.ipReputationScore || 0) +
|
|
4603
4672
|
(suspicionVector.tcpAnomalyScore || 0) * (weights.tcpAnomalyScore || 0) +
|
|
4604
4673
|
(suspicionVector.quicAnomalyScore || 0) * (weights.quicAnomalyScore || 0) + // NOUVEAU: QUIC Anomaly
|
|
4605
|
-
(suspicionVector.http2AnomalyScore || 0) * (weights.http2AnomalyScore || 0) +
|
|
4674
|
+
(suspicionVector.http2AnomalyScore || 0) * (weights.http2AnomalyScore || 0) +
|
|
4675
|
+
(suspicionVector.cookieDroppingScore || 0) * (weights.cookieDroppingScore || 0) +
|
|
4676
|
+
(suspicionVector.virtualizationScore || 0) * (weights.virtualizationScore || 0) +
|
|
4606
4677
|
(suspicionVector.threatIntelScore || 0) * (weights.threatIntelScore || 0) + // NOUVEAU: QUIC Anomaly
|
|
4607
4678
|
(suspicionVector.renderingAnomalyScore || 0) * (weights.renderingAnomalyScore || 0); // NOUVEAU: Rendering Anomaly
|
|
4608
4679
|
|
|
@@ -4802,7 +4873,7 @@ export class FingerprintEngine {
|
|
|
4802
4873
|
const { whitelist = [] } = this.securityConfig;
|
|
4803
4874
|
const botRules = whitelist.filter(rule => rule.hostnameSuffix);
|
|
4804
4875
|
if (botRules.length === 0) {
|
|
4805
|
-
return
|
|
4876
|
+
return null;
|
|
4806
4877
|
}
|
|
4807
4878
|
|
|
4808
4879
|
const { clientIp, headers } = requestContext;
|
|
@@ -4818,7 +4889,7 @@ export class FingerprintEngine {
|
|
|
4818
4889
|
}
|
|
4819
4890
|
});
|
|
4820
4891
|
if (!matchedRule) {
|
|
4821
|
-
return
|
|
4892
|
+
return null;
|
|
4822
4893
|
}
|
|
4823
4894
|
|
|
4824
4895
|
const botName = matchedRule.userAgent; // e.g., 'Googlebot'
|
|
@@ -4885,6 +4956,13 @@ export class FingerprintEngine {
|
|
|
4885
4956
|
}
|
|
4886
4957
|
|
|
4887
4958
|
async processRequest(requestContext) {
|
|
4959
|
+
if (this.securityConfig?.reset) {
|
|
4960
|
+
const subnet = getIpSubnet(requestContext.clientIp);
|
|
4961
|
+
if (subnet) {
|
|
4962
|
+
await store.delete(`subnet:${subnet}`);
|
|
4963
|
+
}
|
|
4964
|
+
await store.delete(`ip-reputation:${requestContext.clientIp}`);
|
|
4965
|
+
}
|
|
4888
4966
|
sanitizeProxyHeaders(requestContext, this.securityConfig);
|
|
4889
4967
|
|
|
4890
4968
|
const { clientIp = "unknown", path, cookies = {}, query = {}, isStatic, graphqlOperationType, graphqlOperationName } = requestContext;
|
|
@@ -5544,10 +5622,11 @@ export class FingerprintEngine {
|
|
|
5544
5622
|
this._log('High suspicion score detected - overriding valid ticket to re-issue challenge', { finalScore, deviceId });
|
|
5545
5623
|
}
|
|
5546
5624
|
|
|
5547
|
-
// ---
|
|
5548
|
-
const
|
|
5625
|
+
// --- Limiteur de débit par domaine et sous-réseau (Token Bucket) ---
|
|
5626
|
+
const domain = requestContext.headers?.host || 'default';
|
|
5627
|
+
const rateLimitPassed = await checkChallengeRateLimit(clientIp, domain, this.securityConfig?.challengeRateLimit);
|
|
5549
5628
|
if (!rateLimitPassed) {
|
|
5550
|
-
this._log('Challenge rate limit exceeded - blocking with 429', { clientIp });
|
|
5629
|
+
this._log('Challenge rate limit exceeded - blocking with 429', { clientIp, domain });
|
|
5551
5630
|
const decision = {
|
|
5552
5631
|
action: 'block',
|
|
5553
5632
|
status: 429,
|
|
@@ -6917,6 +6996,7 @@ export const powMiddleware = (securityConfig) => {
|
|
|
6917
6996
|
*/
|
|
6918
6997
|
export const __internal = {
|
|
6919
6998
|
get store() { return store; }, // Export the store for testing
|
|
6999
|
+
checkChallengeRateLimit,
|
|
6920
7000
|
getDeviceHash,
|
|
6921
7001
|
getCompositeDeviceHash,
|
|
6922
7002
|
getSuspicionVector,
|