@anonympins/fingerprint 0.7.3 → 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 +14 -0
- 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 -0
- package/src/js/fingerprint.js +198 -163
- 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 +135 -3
- package/src/php/Challenge/ChallengeUtils.php +15 -16
- package/src/php/Tests/ChallengeUtilsTest.php +469 -453
- package/src/php/Tests/RequestUtilsTest.php +1 -1
- package/src/php/Utils/RequestUtils.php +209 -127
- package/src/php/WordPress/fingerprint-anti-bot.php +1 -1
- 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/readme.txt +1 -1
package/src/js/fingerprint.js
CHANGED
|
@@ -158,25 +158,12 @@ const loadBotWhitelist = (filename, fallbackEntries) => {
|
|
|
158
158
|
};
|
|
159
159
|
|
|
160
160
|
const googlebotEntries = loadBotWhitelist('googlebot.json', [
|
|
161
|
-
"2001:4860:4801:10::/64",
|
|
162
|
-
"2001:4860:4801:11::/64",
|
|
163
|
-
"2001:4860:4801:12::/64",
|
|
164
|
-
// ... [Keep fallback inline values for safety]
|
|
165
|
-
"66.249.79.64"
|
|
166
161
|
]);
|
|
167
162
|
|
|
168
163
|
const bingbotEntries = loadBotWhitelist('bingbot.json', [
|
|
169
|
-
"157.55.39.0/24",
|
|
170
|
-
"207.46.13.0/24",
|
|
171
|
-
// ... [Keep fallback inline values for safety]
|
|
172
|
-
"40.77.178.0/23"
|
|
173
164
|
]);
|
|
174
165
|
|
|
175
166
|
const yandexEntries = loadBotWhitelist('yandex.json', [
|
|
176
|
-
"2a02:6b8::/29",
|
|
177
|
-
"5.45.192.0/18",
|
|
178
|
-
// ... [Keep fallback inline values for safety]
|
|
179
|
-
"213.180.192.0/19"
|
|
180
167
|
]);
|
|
181
168
|
|
|
182
169
|
function generateSessionMapping() {
|
|
@@ -361,7 +348,7 @@ export function generateStatelessTicket(payload) {
|
|
|
361
348
|
* @param {object} context - Le contexte de la requête.
|
|
362
349
|
* @returns {{protocolAnomalyScore: number}}
|
|
363
350
|
*/
|
|
364
|
-
function getProtocolAnomalyScore(context) {
|
|
351
|
+
export function getProtocolAnomalyScore(context) {
|
|
365
352
|
let http2Anomaly = 0.0;
|
|
366
353
|
let quicAnomaly = 0.0;
|
|
367
354
|
|
|
@@ -374,9 +361,10 @@ function getProtocolAnomalyScore(context) {
|
|
|
374
361
|
const h2Fp = context.headers?.['x-http2-fingerprint'] || context.http2Fingerprint || null;
|
|
375
362
|
if (h2Fp && typeof h2Fp === 'string') {
|
|
376
363
|
const parts = h2Fp.split('|');
|
|
377
|
-
if (parts.length >=
|
|
364
|
+
if (parts.length >= 3) {
|
|
378
365
|
const connWindow = parseInt(parts[1], 10);
|
|
379
|
-
const
|
|
366
|
+
const streamPriority = parts[2] || '';
|
|
367
|
+
const headerOrder = parts.length > 3 ? parts[3] : '';
|
|
380
368
|
const isChromium = browser.startsWith('Chrome') || browser.startsWith('Edge');
|
|
381
369
|
const isFirefox = browser.startsWith('Firefox');
|
|
382
370
|
const isSafari = browser.startsWith('Safari');
|
|
@@ -384,11 +372,43 @@ function getProtocolAnomalyScore(context) {
|
|
|
384
372
|
if (isChromium) {
|
|
385
373
|
if (headerOrder && headerOrder !== 'm,a,s,p') http2Anomaly += 60.0;
|
|
386
374
|
if (connWindow === 65535 || connWindow === 65536) http2Anomaly += 40.0;
|
|
375
|
+
if (streamPriority === '0' || streamPriority === '') http2Anomaly += 50.0;
|
|
387
376
|
} else if (isFirefox) {
|
|
388
377
|
if (headerOrder && headerOrder !== 'm,s,p,a') http2Anomaly += 60.0;
|
|
389
378
|
} else if (isSafari) {
|
|
390
379
|
if (headerOrder && headerOrder !== 'm,s,p,a') http2Anomaly += 60.0;
|
|
391
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
|
+
}
|
|
392
412
|
}
|
|
393
413
|
}
|
|
394
414
|
|
|
@@ -490,7 +510,7 @@ function getProtocolAnomalyScore(context) {
|
|
|
490
510
|
*/
|
|
491
511
|
function getRenderingAnomalyScore(context) {
|
|
492
512
|
const behaviorHeader = context.headers?.['x-behavior-metrics'];
|
|
493
|
-
if (!behaviorHeader) {
|
|
513
|
+
if (!behaviorHeader || !isNaN(Number(behaviorHeader))) {
|
|
494
514
|
return { renderingAnomalyScore: 0.0 };
|
|
495
515
|
}
|
|
496
516
|
try {
|
|
@@ -580,33 +600,45 @@ export function parseStatelessTicket(ticket) {
|
|
|
580
600
|
}
|
|
581
601
|
|
|
582
602
|
/**
|
|
583
|
-
* 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.
|
|
584
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.
|
|
585
607
|
* @returns {Promise<boolean>} True si la requête est autorisée, false si elle est limitée.
|
|
586
608
|
*/
|
|
587
|
-
async function checkChallengeRateLimit(clientIp) {
|
|
588
|
-
|
|
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;
|
|
589
618
|
if (!subnet) return false;
|
|
590
619
|
|
|
591
|
-
const
|
|
592
|
-
const
|
|
593
|
-
tokens: 5.0,
|
|
594
|
-
lastRefill: Date.now() / 1000
|
|
595
|
-
};
|
|
620
|
+
const host = (domain || 'default').toLowerCase().split(':')[0];
|
|
621
|
+
const key = `rate-limit:${host}:${subnet}`;
|
|
596
622
|
|
|
597
|
-
const capacity =
|
|
598
|
-
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)
|
|
599
625
|
const now = Date.now() / 1000;
|
|
600
626
|
|
|
601
|
-
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);
|
|
602
633
|
const tokens = Math.min(capacity, rateLimitData.tokens + elapsed * refillRate);
|
|
634
|
+
const ttl = Math.max(60, Math.ceil(capacity / Math.max(0.1, refillRate)));
|
|
603
635
|
|
|
604
636
|
if (tokens < 1.0) {
|
|
605
|
-
await store.set(key, { tokens, lastRefill: now },
|
|
637
|
+
await store.set(key, { tokens, lastRefill: now }, ttl);
|
|
606
638
|
return false;
|
|
607
639
|
}
|
|
608
640
|
|
|
609
|
-
await store.set(key, { tokens: tokens - 1.0, lastRefill: now },
|
|
641
|
+
await store.set(key, { tokens: tokens - 1.0, lastRefill: now }, ttl);
|
|
610
642
|
return true;
|
|
611
643
|
}
|
|
612
644
|
|
|
@@ -656,18 +688,22 @@ const securityProfiles = {
|
|
|
656
688
|
headerAnomalyScore: 0.1,
|
|
657
689
|
requestPatternScore: 0.6,
|
|
658
690
|
inconsistencyScore: 0.8,
|
|
659
|
-
behaviorScore: 0.7,
|
|
691
|
+
behaviorScore: 0.7,
|
|
660
692
|
honeypotScore: 1.0,
|
|
693
|
+
botScore: 1.0,
|
|
694
|
+
cookieDroppingScore: 0.9,
|
|
661
695
|
crossLayerInconsistencyScore: 0.4,
|
|
662
696
|
timeInconsistencyScore: 0.9,
|
|
663
|
-
tlsSpoofingScore: 0.8,
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
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,
|
|
668
704
|
quicAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie QUIC
|
|
669
|
-
protocolAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie HTTP/2
|
|
670
705
|
renderingAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie de rendu
|
|
706
|
+
http2AnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie HTTP/2
|
|
671
707
|
threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
|
|
672
708
|
virtualizationScore: 0.8,
|
|
673
709
|
},
|
|
@@ -703,15 +739,22 @@ const securityProfiles = {
|
|
|
703
739
|
inconsistencyScore: 1.0,
|
|
704
740
|
behaviorScore: 0.8,
|
|
705
741
|
honeypotScore: 1.0,
|
|
742
|
+
botScore: 1.0,
|
|
743
|
+
cookieDroppingScore: 1.0,
|
|
706
744
|
crossLayerInconsistencyScore: 0.6,
|
|
707
745
|
timeInconsistencyScore: 1.0,
|
|
708
|
-
tlsSpoofingScore: 1.0,
|
|
746
|
+
tlsSpoofingScore: 1.0,
|
|
747
|
+
clientHintsInconsistencyScore: 0.9,
|
|
748
|
+
clickVarianceScore: 0.7,
|
|
709
749
|
subnetScore: 0.5,
|
|
710
|
-
ipReputationScore: 0.6,
|
|
711
|
-
botnetClusterScore: 0.8,
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
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,
|
|
715
758
|
},
|
|
716
759
|
thresholds: { low: 10, medium: 35, high: 65, block: 90 },
|
|
717
760
|
patterns: {
|
|
@@ -746,15 +789,22 @@ const securityProfiles = {
|
|
|
746
789
|
inconsistencyScore: 0.7,
|
|
747
790
|
behaviorScore: 0.2, // Lower weight, as browser behavior is not applicable
|
|
748
791
|
honeypotScore: 1.0,
|
|
792
|
+
botScore: 0.8,
|
|
793
|
+
cookieDroppingScore: 0.8,
|
|
749
794
|
crossLayerInconsistencyScore: 0.5,
|
|
750
795
|
timeInconsistencyScore: 0.8,
|
|
751
|
-
tlsSpoofingScore: 0.7,
|
|
796
|
+
tlsSpoofingScore: 0.7,
|
|
797
|
+
clientHintsInconsistencyScore: 0.6,
|
|
798
|
+
clickVarianceScore: 0.3,
|
|
752
799
|
subnetScore: 0.4,
|
|
753
|
-
ipReputationScore: 0.5,
|
|
754
|
-
botnetClusterScore: 0.7,
|
|
755
|
-
tcpAnomalyScore: 0.8,
|
|
756
|
-
|
|
757
|
-
|
|
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,
|
|
758
808
|
},
|
|
759
809
|
thresholds: { low: 25, medium: 50, high: 80, block: 95 },
|
|
760
810
|
patterns: {
|
|
@@ -801,9 +851,10 @@ const securityProfiles = {
|
|
|
801
851
|
ipReputationScore: 0.3, // NOUVEAU: Poids pour la réputation IP
|
|
802
852
|
botnetClusterScore: 0.5, // NOUVEAU: Poids pour le clustering botnet
|
|
803
853
|
tcpAnomalyScore: 0.5, // NEW: Anomalie de pile TCP/IP
|
|
804
|
-
protocolAnomalyScore: 0.5,
|
|
805
854
|
quicAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie QUIC
|
|
855
|
+
http2AnomalyScore: 0.5,
|
|
806
856
|
renderingAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie de rendu
|
|
857
|
+
virtualizationScore: 0.8,
|
|
807
858
|
},
|
|
808
859
|
thresholds: { low: 25, medium: 55, high: 80, block: 95 },
|
|
809
860
|
patterns: {
|
|
@@ -832,7 +883,6 @@ const securityProfiles = {
|
|
|
832
883
|
historyScore: 0.4,
|
|
833
884
|
rotationScore: 0.6,
|
|
834
885
|
headerAnomalyScore: 0.2,
|
|
835
|
-
// Utilisation d'un score de pattern unifié avec un poids très élevé
|
|
836
886
|
requestPatternScore: 0.9,
|
|
837
887
|
inconsistencyScore: 1.0, // Crucial for preventing account takeover
|
|
838
888
|
behaviorScore: 0.8, // Important for checkout/login forms
|
|
@@ -849,9 +899,10 @@ const securityProfiles = {
|
|
|
849
899
|
ipReputationScore: 0.6, // NOUVEAU: Poids pour la réputation IP
|
|
850
900
|
botnetClusterScore: 0.9, // NOUVEAU: Poids pour le clustering botnet
|
|
851
901
|
tcpAnomalyScore: 0.9, // NEW: Anomalie de pile TCP/IP
|
|
852
|
-
protocolAnomalyScore: 0.9,
|
|
853
902
|
quicAnomalyScore: 0.9, // NOUVEAU: Poids pour l'anomalie QUIC
|
|
854
|
-
|
|
903
|
+
http2AnomalyScore: 0.9,
|
|
904
|
+
renderingAnomalyScore: 0.9,
|
|
905
|
+
virtualizationScore: 0.8,
|
|
855
906
|
},
|
|
856
907
|
thresholds: { low: 15, medium: 40, high: 70, block: 90 },
|
|
857
908
|
patterns: {
|
|
@@ -1466,88 +1517,6 @@ export const verifyTspChallenge = (
|
|
|
1466
1517
|
}
|
|
1467
1518
|
};
|
|
1468
1519
|
|
|
1469
|
-
/**
|
|
1470
|
-
* Generates the HTML content for a memory-intensive PoW challenge.
|
|
1471
|
-
*/
|
|
1472
|
-
const generateMemoryPoWChallenge = (
|
|
1473
|
-
clientIp,
|
|
1474
|
-
nonce,
|
|
1475
|
-
difficulty = 16,
|
|
1476
|
-
path = "",
|
|
1477
|
-
) => {
|
|
1478
|
-
const safePath = sanitizeRedirectPath(path);
|
|
1479
|
-
// difficulty here is the buffer size in MB.
|
|
1480
|
-
return `
|
|
1481
|
-
<html>
|
|
1482
|
-
<head><title>Advanced Security Check</title></head>
|
|
1483
|
-
<body style="font-family:sans-serif; text-align:center; padding-top:50px;">
|
|
1484
|
-
<h1>Enhanced Verification... (Level 2)</h1>
|
|
1485
|
-
<p>Your activity requires an additional security check.</p>
|
|
1486
|
-
<div id="loader" style="margin:20px;">⚙️ Performing memory allocation and calculation... (${difficulty} MB)</div>
|
|
1487
|
-
<script>
|
|
1488
|
-
async function solve() {
|
|
1489
|
-
const nonce = ${safeJsonStringify(nonce)};
|
|
1490
|
-
const size = ${difficulty} * 1024 * 1024; // en octets
|
|
1491
|
-
const iterations = size / 16;
|
|
1492
|
-
|
|
1493
|
-
try {
|
|
1494
|
-
const buffer = new Uint32Array(size / 4);
|
|
1495
|
-
let h = new TextEncoder().encode(nonce).reduce((acc, v) => acc + v, 0);
|
|
1496
|
-
for (let i = 0; i < buffer.length; i++) {
|
|
1497
|
-
buffer[i] = (h = Math.imul(h ^ i, 1597334677));
|
|
1498
|
-
}
|
|
1499
|
-
|
|
1500
|
-
let finalHash = 0;
|
|
1501
|
-
for(let i = 0; i < iterations; i++) {
|
|
1502
|
-
const addr = buffer[i % buffer.length] % buffer.length;
|
|
1503
|
-
finalHash ^= buffer[addr];
|
|
1504
|
-
}
|
|
1505
|
-
window.location.href = "${path}" + "?pow_type=mem&pow_nonce=" + nonce + "&pow_solution=" + finalHash;
|
|
1506
|
-
} catch(e) {
|
|
1507
|
-
window.location.href = ${safeJsonStringify(safePath)} + "?pow_type=mem&pow_nonce=" + nonce + "&pow_solution=" + finalHash;
|
|
1508
|
-
}
|
|
1509
|
-
}
|
|
1510
|
-
solve();
|
|
1511
|
-
</script>
|
|
1512
|
-
</body>
|
|
1513
|
-
</html>`;
|
|
1514
|
-
};
|
|
1515
|
-
|
|
1516
|
-
/**
|
|
1517
|
-
* Verifies if a PoW solution is valid and generates a clearance ticket.
|
|
1518
|
-
*/
|
|
1519
|
-
export const verifyPoWAndGenerateTicket = async (
|
|
1520
|
-
ip,
|
|
1521
|
-
nonce,
|
|
1522
|
-
solution,
|
|
1523
|
-
difficulty = 4,
|
|
1524
|
-
deviceId = '',
|
|
1525
|
-
deviceHash = ''
|
|
1526
|
-
) => {
|
|
1527
|
-
// 1. Verify the solution: hash(ip + nonce + solution) must start with N zeros
|
|
1528
|
-
const hash = crypto
|
|
1529
|
-
.createHash("sha256")
|
|
1530
|
-
.update(`${ip}:${nonce}:${solution}`)
|
|
1531
|
-
.digest("hex");
|
|
1532
|
-
|
|
1533
|
-
if (!hash.startsWith("0".repeat(difficulty))) {
|
|
1534
|
-
return null;
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
// 2. Generate a signed and encrypted stateless ticket
|
|
1538
|
-
const expiry = Date.now() + 3600000; // 1 hour
|
|
1539
|
-
const payload = {
|
|
1540
|
-
expiry,
|
|
1541
|
-
originalIp: ip,
|
|
1542
|
-
deviceId,
|
|
1543
|
-
deviceHash
|
|
1544
|
-
};
|
|
1545
|
-
|
|
1546
|
-
return generateStatelessTicket(payload);
|
|
1547
|
-
};
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
1520
|
/**
|
|
1552
1521
|
* Verifies a memory PoW solution.
|
|
1553
1522
|
*
|
|
@@ -2335,13 +2304,37 @@ function analyzeTouchMovements(history) {
|
|
|
2335
2304
|
* @returns {{behaviorScore: number}}
|
|
2336
2305
|
*/
|
|
2337
2306
|
function getBehaviorScore(context) {
|
|
2338
|
-
const behaviorHeader = context.headers["x-behavior-metrics"];
|
|
2339
|
-
if (
|
|
2307
|
+
const behaviorHeader = context.headers ? (context.headers["x-behavior-metrics"] ?? context.headers["X-Behavior-Metrics"]) : undefined;
|
|
2308
|
+
if (behaviorHeader === undefined || behaviorHeader === null || behaviorHeader === '') {
|
|
2340
2309
|
return { behaviorScore: 0 }; // Pas de données, pas de pénalité.
|
|
2341
2310
|
}
|
|
2342
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
|
+
|
|
2343
2331
|
try {
|
|
2344
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
|
+
}
|
|
2345
2338
|
let score = 0;
|
|
2346
2339
|
|
|
2347
2340
|
// 1. Pénalité maximale si un honeypot client a été déclenché.
|
|
@@ -2355,9 +2348,18 @@ function getBehaviorScore(context) {
|
|
|
2355
2348
|
const { avgSpeed, avgAcceleration, straightness, pauses, segments } = analyzeMouseMovements(metrics.mouseMovementsHistory);
|
|
2356
2349
|
const touchAnalysis = analyzeTouchMovements(metrics.touchMovementsHistory);
|
|
2357
2350
|
|
|
2358
|
-
// 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.
|
|
2359
2354
|
if (avgSpeed === 0 && touchAnalysis.avgSpeed === 0 && metrics.keystrokeLatency === 0) {
|
|
2360
|
-
|
|
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;
|
|
2361
2363
|
}
|
|
2362
2364
|
|
|
2363
2365
|
// 3. (NOUVEAU) Analyse de la longueur de l'historique de navigation.
|
|
@@ -2397,21 +2399,19 @@ function getBehaviorScore(context) {
|
|
|
2397
2399
|
if (touch.avgRadius > 0 && touch.radiusVariance === 0) {
|
|
2398
2400
|
score += 30; // Spoofed pointer area size
|
|
2399
2401
|
}
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
if (benfordDev > 0.18) score += 35;
|
|
2404
|
-
}
|
|
2405
|
-
|
|
2406
|
-
// Détection de ferme mobile : Touch actif sur mobile sans aucune vibration physique (châssis/rack ADB)
|
|
2407
|
-
const isMobileDevice = (context.headers['user-agent'] || '').includes('Mobile');
|
|
2408
|
-
if (isMobileDevice && touchHistory.length >= 5 && typeof metrics.motionVariance === 'number') {
|
|
2409
|
-
if (metrics.motionVariance === 0) {
|
|
2410
|
-
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;
|
|
2411
2405
|
}
|
|
2412
2406
|
}
|
|
2413
2407
|
}
|
|
2414
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
|
+
|
|
2415
2415
|
// Plausibilité de la latence de frappe
|
|
2416
2416
|
if (metrics.keystrokeLatency > 0 && metrics.keystrokeLatency < 40) score += 25; // Frappe trop rapide pour un humain.
|
|
2417
2417
|
if (metrics.keystrokeLatency > 1000) score += 15; // Latence très élevée, peut être un script lent.
|
|
@@ -2981,6 +2981,10 @@ function analyzeClickPositions(history) {
|
|
|
2981
2981
|
* @returns {{clickVarianceScore: number}}
|
|
2982
2982
|
*/
|
|
2983
2983
|
function getClickVarianceScore(context) {
|
|
2984
|
+
const rawMetrics = context.headers?.['x-behavior-metrics'];
|
|
2985
|
+
if (!rawMetrics || !isNaN(Number(rawMetrics))) {
|
|
2986
|
+
return { clickVarianceScore: 0 };
|
|
2987
|
+
}
|
|
2984
2988
|
const metrics = JSON.parse(context.headers['x-behavior-metrics'] || '{}');
|
|
2985
2989
|
const score = analyzeClickPositions(metrics.clicksHistory);
|
|
2986
2990
|
return { clickVarianceScore: score };
|
|
@@ -3832,7 +3836,7 @@ async function getBehavioralIndicators(context, deviceData) {
|
|
|
3832
3836
|
// --- VALIDATION DE L'ANCRAGE MATÉRIEL WEBAUTHN ---
|
|
3833
3837
|
const behaviorHeader = context.headers?.['x-behavior-metrics'];
|
|
3834
3838
|
let webauthnVerified = false;
|
|
3835
|
-
if (behaviorHeader) {
|
|
3839
|
+
if (behaviorHeader && typeof behaviorHeader === 'string' && behaviorHeader.startsWith('{')) {
|
|
3836
3840
|
try {
|
|
3837
3841
|
const metrics = JSON.parse(behaviorHeader);
|
|
3838
3842
|
if (metrics && metrics.webauthnAnchor) {
|
|
@@ -3976,7 +3980,12 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
3976
3980
|
const { botScore } = getBotScore(context);
|
|
3977
3981
|
|
|
3978
3982
|
// NOUVEAU: On calcule le score d'incohérence temporelle.
|
|
3979
|
-
|
|
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);
|
|
3980
3989
|
|
|
3981
3990
|
// NOUVEAU: On calcule le score d'incohérence entre les couches.
|
|
3982
3991
|
const { crossLayerInconsistencyScore } = getCrossLayerInconsistency(context);
|
|
@@ -3990,7 +3999,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
3990
3999
|
const { requestPatternScore } = getRequestPatternScore(context, deviceData, securityConfig.patterns);
|
|
3991
4000
|
|
|
3992
4001
|
const { tcpAnomalyScore } = getTcpAnomalyScore(context);
|
|
3993
|
-
const { protocolAnomalyScore } = getProtocolAnomalyScore(context);
|
|
4002
|
+
const { protocolAnomalyScore, http2AnomalyScore, quicAnomalyScore } = getProtocolAnomalyScore(context);
|
|
3994
4003
|
const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
|
|
3995
4004
|
const virtualizationScore = getVirtualizationAnomalyScore(context);
|
|
3996
4005
|
|
|
@@ -4003,8 +4012,8 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
4003
4012
|
if (Array.isArray(deviceData.ips)) {
|
|
4004
4013
|
deviceData.ips = new Set(deviceData.ips);
|
|
4005
4014
|
}
|
|
4006
|
-
|
|
4007
|
-
|
|
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 };
|
|
4008
4017
|
};
|
|
4009
4018
|
|
|
4010
4019
|
// A residential user can change networks (home, 4G, public wifi).
|
|
@@ -4027,7 +4036,31 @@ export const identifyRequest = (securityConfig) => async (req, res) => {
|
|
|
4027
4036
|
// This function now acts as a lightweight wrapper around the engine's identifyRequest method.
|
|
4028
4037
|
// It requires a default configuration to work.
|
|
4029
4038
|
const config = securityConfig || {
|
|
4030
|
-
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
|
+
},
|
|
4031
4064
|
thresholds: { low: 20, medium: 40, high: 75 },
|
|
4032
4065
|
honeypot: { fields: [] } // Ensure honeypot config exists to prevent errors
|
|
4033
4066
|
};
|
|
@@ -4415,7 +4448,7 @@ async function getThreatIntelScore(context, zkpY, threatIntelConfig = {}) {
|
|
|
4415
4448
|
const tcpRtt = tcpRttHeader ? parseInt(tcpRttHeader, 10) : null;
|
|
4416
4449
|
|
|
4417
4450
|
const behaviorHeader = context.headers?.['x-behavior-metrics'];
|
|
4418
|
-
if (behaviorHeader) {
|
|
4451
|
+
if (behaviorHeader && typeof behaviorHeader === 'string' && behaviorHeader.startsWith('{')) {
|
|
4419
4452
|
try {
|
|
4420
4453
|
const metrics = JSON.parse(behaviorHeader);
|
|
4421
4454
|
if (metrics && typeof metrics.clientTimestamp === 'number' && typeof context.requestTimestamp === 'number') {
|
|
@@ -4577,7 +4610,8 @@ export class FingerprintEngine {
|
|
|
4577
4610
|
'wasm',
|
|
4578
4611
|
'similarityThreshold', 'reset',
|
|
4579
4612
|
'ed25519_private_key', 'ed25519_public_key', 'upowModel',
|
|
4580
|
-
'federatedPeers', 'federationSecret', 'filterWhitelist'
|
|
4613
|
+
'federatedPeers', 'federationSecret', 'filterWhitelist',
|
|
4614
|
+
'challengeRateLimit'
|
|
4581
4615
|
]);
|
|
4582
4616
|
|
|
4583
4617
|
// 1. Check for essential keys
|
|
@@ -4637,8 +4671,7 @@ export class FingerprintEngine {
|
|
|
4637
4671
|
(suspicionVector.ipReputationScore || 0) * (weights.ipReputationScore || 0) +
|
|
4638
4672
|
(suspicionVector.tcpAnomalyScore || 0) * (weights.tcpAnomalyScore || 0) +
|
|
4639
4673
|
(suspicionVector.quicAnomalyScore || 0) * (weights.quicAnomalyScore || 0) + // NOUVEAU: QUIC Anomaly
|
|
4640
|
-
(suspicionVector.http2AnomalyScore || 0) * (weights.http2AnomalyScore || 0) +
|
|
4641
|
-
(suspicionVector.protocolAnomalyScore || 0) * (weights.protocolAnomalyScore || 0) +
|
|
4674
|
+
(suspicionVector.http2AnomalyScore || 0) * (weights.http2AnomalyScore || 0) +
|
|
4642
4675
|
(suspicionVector.cookieDroppingScore || 0) * (weights.cookieDroppingScore || 0) +
|
|
4643
4676
|
(suspicionVector.virtualizationScore || 0) * (weights.virtualizationScore || 0) +
|
|
4644
4677
|
(suspicionVector.threatIntelScore || 0) * (weights.threatIntelScore || 0) + // NOUVEAU: QUIC Anomaly
|
|
@@ -5589,10 +5622,11 @@ export class FingerprintEngine {
|
|
|
5589
5622
|
this._log('High suspicion score detected - overriding valid ticket to re-issue challenge', { finalScore, deviceId });
|
|
5590
5623
|
}
|
|
5591
5624
|
|
|
5592
|
-
// ---
|
|
5593
|
-
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);
|
|
5594
5628
|
if (!rateLimitPassed) {
|
|
5595
|
-
this._log('Challenge rate limit exceeded - blocking with 429', { clientIp });
|
|
5629
|
+
this._log('Challenge rate limit exceeded - blocking with 429', { clientIp, domain });
|
|
5596
5630
|
const decision = {
|
|
5597
5631
|
action: 'block',
|
|
5598
5632
|
status: 429,
|
|
@@ -6962,6 +6996,7 @@ export const powMiddleware = (securityConfig) => {
|
|
|
6962
6996
|
*/
|
|
6963
6997
|
export const __internal = {
|
|
6964
6998
|
get store() { return store; }, // Export the store for testing
|
|
6999
|
+
checkChallengeRateLimit,
|
|
6965
7000
|
getDeviceHash,
|
|
6966
7001
|
getCompositeDeviceHash,
|
|
6967
7002
|
getSuspicionVector,
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service Worker for transparent network interception and credential enrichment.
|
|
3
|
+
* Leaves global window.fetch native and untouched to prevent tampering detection.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
let clientCredentials = {
|
|
7
|
+
fp: '',
|
|
8
|
+
behavior: 1,
|
|
9
|
+
targetDomains: []
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
self.addEventListener('install', () => {
|
|
13
|
+
self.skipWaiting();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
self.addEventListener('activate', (event) => {
|
|
17
|
+
event.waitUntil(self.clients.claim());
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
self.addEventListener('message', (event) => {
|
|
21
|
+
if (event.data && event.data.type === 'UPDATE_CREDENTIALS') {
|
|
22
|
+
if (typeof event.data.fp !== 'undefined') {
|
|
23
|
+
clientCredentials.fp = event.data.fp;
|
|
24
|
+
}
|
|
25
|
+
if (typeof event.data.behavior !== 'undefined') {
|
|
26
|
+
clientCredentials.behavior = event.data.behavior;
|
|
27
|
+
}
|
|
28
|
+
if (Array.isArray(event.data.targetDomains)) {
|
|
29
|
+
clientCredentials.targetDomains = event.data.targetDomains;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
self.addEventListener('fetch', (event) => {
|
|
35
|
+
const url = new URL(event.request.url);
|
|
36
|
+
|
|
37
|
+
// Only process HTTP and HTTPS requests
|
|
38
|
+
if (!url.protocol.startsWith('http')) return;
|
|
39
|
+
|
|
40
|
+
// Scope filtering: same-origin or configured domains
|
|
41
|
+
const targets = clientCredentials.targetDomains;
|
|
42
|
+
const shouldProtect = (targets.length === 0 && url.origin === self.location.origin) ||
|
|
43
|
+
(targets.length > 0 && targets.includes(url.hostname));
|
|
44
|
+
|
|
45
|
+
if (!shouldProtect) return;
|
|
46
|
+
|
|
47
|
+
event.respondWith((async () => {
|
|
48
|
+
const newHeaders = new Headers(event.request.headers);
|
|
49
|
+
|
|
50
|
+
if (clientCredentials.fp) {
|
|
51
|
+
newHeaders.set('X-Device-Fingerprint', clientCredentials.fp);
|
|
52
|
+
}
|
|
53
|
+
if (typeof clientCredentials.behavior !== 'undefined') {
|
|
54
|
+
newHeaders.set('X-Behavior-Metrics', String(clientCredentials.behavior));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const requestInit = {
|
|
58
|
+
method: event.request.method,
|
|
59
|
+
headers: newHeaders,
|
|
60
|
+
credentials: event.request.credentials,
|
|
61
|
+
cache: event.request.cache,
|
|
62
|
+
redirect: event.request.redirect,
|
|
63
|
+
referrer: event.request.referrer,
|
|
64
|
+
integrity: event.request.integrity
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (event.request.mode !== 'navigate') {
|
|
68
|
+
requestInit.mode = event.request.mode;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (event.request.method !== 'GET' && event.request.method !== 'HEAD') {
|
|
72
|
+
try {
|
|
73
|
+
const bodyBlob = await event.request.clone().blob();
|
|
74
|
+
if (bodyBlob.size > 0) requestInit.body = bodyBlob;
|
|
75
|
+
} catch (e) {}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const modifiedRequest = new Request(event.request.url, requestInit);
|
|
79
|
+
return fetch(modifiedRequest);
|
|
80
|
+
})());
|
|
81
|
+
});
|