@anonympins/fingerprint 0.7.3 → 0.7.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +1 -1
- package/package.json +109 -107
- package/public/fingerprint-anti-bot.zip +0 -0
- package/src/js/dynamic-wasm.js +218 -218
- package/src/js/fingerprint.client.js +329 -88
- package/src/js/fingerprint.client.obfuscated.js +1 -0
- package/src/js/fingerprint.js +486 -231
- 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/dynamic-wasm.test.js +77 -77
- package/src/js/tests/fingerprint.client.init.test.js +108 -0
- package/src/js/tests/fingerprint.test.js +153 -3
- package/src/js/tests/threatIntel.test.js +40 -1
- package/src/php/Challenge/ChallengeUtils.php +25 -16
- package/src/php/Config/SecurityProfiles.php +5 -0
- package/src/php/FingerprintEngine.php +186 -37
- package/src/php/Tests/ChallengeUtilsTest.php +469 -453
- package/src/php/Tests/RequestUtilsTest.php +1 -1
- package/src/php/Tests/ThreatIntelTest.php +34 -0
- package/src/php/Utils/RequestUtils.php +257 -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
|
@@ -68,7 +68,8 @@ function canAttemptDns() {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
* Diffuse un ZKP banni aux pairs fédérés de manière asynchrone (non-bloquante)
|
|
71
|
+
* Diffuse un ZKP banni aux pairs fédérés de manière asynchrone (non-bloquante)
|
|
72
|
+
* avec protection par Anonymat Différentiel (Differential Privacy / Bruit de Laplace & Leurres).
|
|
72
73
|
* @private
|
|
73
74
|
* @param {string} zkpY - La clé publique ZKP du terminal banni.
|
|
74
75
|
* @param {object} config - La configuration de sécurité.
|
|
@@ -77,46 +78,87 @@ async function broadcastBannedZkp(zkpY, config) {
|
|
|
77
78
|
const peers = config.federatedPeers || [];
|
|
78
79
|
if (peers.length === 0) return;
|
|
79
80
|
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
let signature = '';
|
|
84
|
-
let isAsymmetric = false;
|
|
81
|
+
const dpConfig = config.differentialPrivacy || {};
|
|
82
|
+
const dpEnabled = dpConfig.enabled !== false;
|
|
83
|
+
const epsilon = typeof dpConfig.epsilon === 'number' ? dpConfig.epsilon : (config.dpEpsilon || 1.0);
|
|
85
84
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
85
|
+
const sendReport = (targetZkpY, ts) => {
|
|
86
|
+
const msg = `${ts}:${targetZkpY}`;
|
|
87
|
+
let signature = '';
|
|
88
|
+
let isAsymmetric = false;
|
|
89
|
+
|
|
90
|
+
if (process.env.ED25519_PRIVATE_KEY) {
|
|
91
|
+
try {
|
|
92
|
+
const cleanKey = process.env.ED25519_PRIVATE_KEY.replace(/\\n/g, '\n');
|
|
93
|
+
const signBuffer = crypto.sign(null, Buffer.from(msg), {
|
|
94
|
+
key: cleanKey,
|
|
95
|
+
format: 'pem',
|
|
96
|
+
type: 'pkcs8'
|
|
97
|
+
});
|
|
98
|
+
signature = signBuffer.toString('hex');
|
|
99
|
+
isAsymmetric = true;
|
|
100
|
+
} catch (e) {
|
|
101
|
+
console.error('[Fingerprint] Asymmetric broadcast signing failed, falling back to HMAC:', e.message);
|
|
102
|
+
}
|
|
98
103
|
}
|
|
99
|
-
}
|
|
100
104
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
if (!isAsymmetric) {
|
|
106
|
+
const secret = config.federationSecret || getPowSecret();
|
|
107
|
+
signature = crypto.createHmac('sha256', secret).update(msg).digest('hex');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
peers.forEach(peerUrl => {
|
|
111
|
+
fetch(peerUrl + '?coop_op=share_threat_intel', {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: {
|
|
114
|
+
'Content-Type': 'application/json',
|
|
115
|
+
'X-Federation-Signature': isAsymmetric ? '' : signature,
|
|
116
|
+
'X-Federation-Signature-Ed25519': isAsymmetric ? signature : '',
|
|
117
|
+
'X-Federation-Timestamp': String(ts)
|
|
118
|
+
},
|
|
119
|
+
body: JSON.stringify({ zkpY: targetZkpY })
|
|
120
|
+
}).catch(() => {
|
|
121
|
+
// Échec de propagation silencieux pour ne pas perturber le thread principal
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
let reportTimestamp = Date.now();
|
|
127
|
+
|
|
128
|
+
// 1. Differential Privacy : Perturbation laplacienne de l'horodatage
|
|
129
|
+
if (dpEnabled) {
|
|
130
|
+
const deltaT = 5000; // Sensibilité de 5s
|
|
131
|
+
const b = deltaT / Math.max(0.1, epsilon);
|
|
132
|
+
const u = secureRandomFloat() - 0.5;
|
|
133
|
+
const safeU = Math.abs(u) < 1e-7 ? (u >= 0 ? 1e-7 : -1e-7) : u;
|
|
134
|
+
const laplaceNoise = -b * Math.sign(safeU) * Math.log(1 - 2 * Math.abs(safeU));
|
|
135
|
+
const clampedNoise = Math.max(-60000, Math.min(60000, Math.round(laplaceNoise)));
|
|
136
|
+
reportTimestamp = Date.now() + clampedNoise;
|
|
104
137
|
}
|
|
105
138
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
139
|
+
sendReport(zkpY, reportTimestamp);
|
|
140
|
+
|
|
141
|
+
// 2. Differential Privacy : Réponse randomisée par injection de leurres (Decoy ZKP)
|
|
142
|
+
if (dpEnabled) {
|
|
143
|
+
const decoyProbability = dpConfig.dummyRate !== undefined
|
|
144
|
+
? dpConfig.dummyRate
|
|
145
|
+
: (1 / (1 + Math.exp(epsilon)));
|
|
146
|
+
|
|
147
|
+
if (secureRandomFloat() < decoyProbability) {
|
|
148
|
+
const ZKP_P = 115792089237316195423570985008687907853269984665640564039457584007908834671663n;
|
|
149
|
+
const decoyBytes = crypto.randomBytes(32);
|
|
150
|
+
const decoyInt = (BigInt('0x' + decoyBytes.toString('hex')) % (ZKP_P - 2n)) + 1n;
|
|
151
|
+
const decoyZkpY = decoyInt.toString(16);
|
|
152
|
+
|
|
153
|
+
const uDecoy = secureRandomFloat() - 0.5;
|
|
154
|
+
const safeUDecoy = Math.abs(uDecoy) < 1e-7 ? (uDecoy >= 0 ? 1e-7 : -1e-7) : uDecoy;
|
|
155
|
+
const b = 5000 / Math.max(0.1, epsilon);
|
|
156
|
+
const decoyNoise = Math.max(-60000, Math.min(60000, Math.round(-b * Math.sign(safeUDecoy) * Math.log(1 - 2 * Math.abs(safeUDecoy)))));
|
|
157
|
+
const decoyTimestamp = Date.now() + decoyNoise;
|
|
158
|
+
|
|
159
|
+
sendReport(decoyZkpY, decoyTimestamp);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
120
162
|
}
|
|
121
163
|
|
|
122
164
|
const withTimeout = (promise, ms) => {
|
|
@@ -158,25 +200,15 @@ const loadBotWhitelist = (filename, fallbackEntries) => {
|
|
|
158
200
|
};
|
|
159
201
|
|
|
160
202
|
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
203
|
]);
|
|
167
204
|
|
|
168
205
|
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
206
|
]);
|
|
174
207
|
|
|
175
208
|
const yandexEntries = loadBotWhitelist('yandex.json', [
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
"213.180.192.0/19"
|
|
209
|
+
]);
|
|
210
|
+
|
|
211
|
+
const facebookEntries = loadBotWhitelist('facebook.json', [
|
|
180
212
|
]);
|
|
181
213
|
|
|
182
214
|
function generateSessionMapping() {
|
|
@@ -361,7 +393,7 @@ export function generateStatelessTicket(payload) {
|
|
|
361
393
|
* @param {object} context - Le contexte de la requête.
|
|
362
394
|
* @returns {{protocolAnomalyScore: number}}
|
|
363
395
|
*/
|
|
364
|
-
function getProtocolAnomalyScore(context) {
|
|
396
|
+
export function getProtocolAnomalyScore(context) {
|
|
365
397
|
let http2Anomaly = 0.0;
|
|
366
398
|
let quicAnomaly = 0.0;
|
|
367
399
|
|
|
@@ -374,9 +406,10 @@ function getProtocolAnomalyScore(context) {
|
|
|
374
406
|
const h2Fp = context.headers?.['x-http2-fingerprint'] || context.http2Fingerprint || null;
|
|
375
407
|
if (h2Fp && typeof h2Fp === 'string') {
|
|
376
408
|
const parts = h2Fp.split('|');
|
|
377
|
-
if (parts.length >=
|
|
409
|
+
if (parts.length >= 3) {
|
|
378
410
|
const connWindow = parseInt(parts[1], 10);
|
|
379
|
-
const
|
|
411
|
+
const streamPriority = parts[2] || '';
|
|
412
|
+
const headerOrder = parts.length > 3 ? parts[3] : '';
|
|
380
413
|
const isChromium = browser.startsWith('Chrome') || browser.startsWith('Edge');
|
|
381
414
|
const isFirefox = browser.startsWith('Firefox');
|
|
382
415
|
const isSafari = browser.startsWith('Safari');
|
|
@@ -384,11 +417,43 @@ function getProtocolAnomalyScore(context) {
|
|
|
384
417
|
if (isChromium) {
|
|
385
418
|
if (headerOrder && headerOrder !== 'm,a,s,p') http2Anomaly += 60.0;
|
|
386
419
|
if (connWindow === 65535 || connWindow === 65536) http2Anomaly += 40.0;
|
|
420
|
+
if (streamPriority === '0' || streamPriority === '') http2Anomaly += 50.0;
|
|
387
421
|
} else if (isFirefox) {
|
|
388
422
|
if (headerOrder && headerOrder !== 'm,s,p,a') http2Anomaly += 60.0;
|
|
389
423
|
} else if (isSafari) {
|
|
390
424
|
if (headerOrder && headerOrder !== 'm,s,p,a') http2Anomaly += 60.0;
|
|
391
425
|
}
|
|
426
|
+
|
|
427
|
+
// Analyse fine des trames (PRIORITY, WINDOW_UPDATE, CONTINUATION)
|
|
428
|
+
if (parts.length >= 5) {
|
|
429
|
+
const frameCountsStr = parts[4];
|
|
430
|
+
const frameCounts = {};
|
|
431
|
+
frameCountsStr.split(',').forEach(item => {
|
|
432
|
+
const kv = item.split(':');
|
|
433
|
+
if (kv.length === 2) {
|
|
434
|
+
frameCounts[kv[0]] = parseInt(kv[1], 10);
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
const priorityCount = frameCounts.p || 0;
|
|
439
|
+
const windowUpdateCount = frameCounts.w || 0;
|
|
440
|
+
const continuationCount = frameCounts.c || 0;
|
|
441
|
+
|
|
442
|
+
if (isChromium) {
|
|
443
|
+
// Chrome envoie des trames PRIORITY pour l'arbre de dépendances
|
|
444
|
+
if (priorityCount === 0) http2Anomaly += 25.0;
|
|
445
|
+
// Chrome est agressif avec les WINDOW_UPDATE
|
|
446
|
+
if (windowUpdateCount < 2) http2Anomaly += 20.0;
|
|
447
|
+
} else if (isFirefox) {
|
|
448
|
+
// Firefox utilise un schéma de priorité différent, souvent avec moins de trames PRIORITY
|
|
449
|
+
if (priorityCount > 1) http2Anomaly += 20.0;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Les bots génériques n'envoient souvent pas de trames CONTINUATION pour les en-têtes longs
|
|
453
|
+
if (continuationCount === 0 && (headerOrder.match(/,/g) || []).length > 3) {
|
|
454
|
+
http2Anomaly += 30.0;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
392
457
|
}
|
|
393
458
|
}
|
|
394
459
|
|
|
@@ -490,7 +555,7 @@ function getProtocolAnomalyScore(context) {
|
|
|
490
555
|
*/
|
|
491
556
|
function getRenderingAnomalyScore(context) {
|
|
492
557
|
const behaviorHeader = context.headers?.['x-behavior-metrics'];
|
|
493
|
-
if (!behaviorHeader) {
|
|
558
|
+
if (!behaviorHeader || !isNaN(Number(behaviorHeader))) {
|
|
494
559
|
return { renderingAnomalyScore: 0.0 };
|
|
495
560
|
}
|
|
496
561
|
try {
|
|
@@ -580,33 +645,45 @@ export function parseStatelessTicket(ticket) {
|
|
|
580
645
|
}
|
|
581
646
|
|
|
582
647
|
/**
|
|
583
|
-
* Vérifie le limiteur de débit Token Bucket pour les demandes de challenge d'un sous-réseau.
|
|
648
|
+
* Vérifie le limiteur de débit Token Bucket pour les demandes de challenge d'un sous-réseau par domaine.
|
|
584
649
|
* @param {string} clientIp - L'adresse IP du client.
|
|
650
|
+
* @param {string} [domain='default'] - Le domaine/hôte ciblé (en-tête Host).
|
|
651
|
+
* @param {object} [rateLimitConfig] - Configuration optionnelle du limiteur.
|
|
585
652
|
* @returns {Promise<boolean>} True si la requête est autorisée, false si elle est limitée.
|
|
586
653
|
*/
|
|
587
|
-
async function checkChallengeRateLimit(clientIp) {
|
|
588
|
-
|
|
654
|
+
async function checkChallengeRateLimit(clientIp, domain = 'default', rateLimitConfig = {}) {
|
|
655
|
+
if (rateLimitConfig && rateLimitConfig.enabled === false) {
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
658
|
+
if (!clientIp || isLoopbackIp(clientIp)) {
|
|
659
|
+
return true; // Bypass local/dev
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const subnet = getIpSubnet(clientIp) || clientIp;
|
|
589
663
|
if (!subnet) return false;
|
|
590
664
|
|
|
591
|
-
const
|
|
592
|
-
const
|
|
593
|
-
tokens: 5.0,
|
|
594
|
-
lastRefill: Date.now() / 1000
|
|
595
|
-
};
|
|
665
|
+
const host = (domain || 'default').toLowerCase().split(':')[0];
|
|
666
|
+
const key = `rate-limit:${host}:${subnet}`;
|
|
596
667
|
|
|
597
|
-
const capacity =
|
|
598
|
-
const refillRate = 0
|
|
668
|
+
const capacity = Number(rateLimitConfig?.capacity ?? 30.0);
|
|
669
|
+
const refillRate = Number(rateLimitConfig?.refillRate ?? 1.0); // 1 token par seconde (au lieu de 0.1)
|
|
599
670
|
const now = Date.now() / 1000;
|
|
600
671
|
|
|
601
|
-
const
|
|
672
|
+
const rateLimitData = (await store.get(key)) || {
|
|
673
|
+
tokens: capacity,
|
|
674
|
+
lastRefill: now
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
const elapsed = Math.max(0, now - rateLimitData.lastRefill);
|
|
602
678
|
const tokens = Math.min(capacity, rateLimitData.tokens + elapsed * refillRate);
|
|
679
|
+
const ttl = Math.max(60, Math.ceil(capacity / Math.max(0.1, refillRate)));
|
|
603
680
|
|
|
604
681
|
if (tokens < 1.0) {
|
|
605
|
-
await store.set(key, { tokens, lastRefill: now },
|
|
682
|
+
await store.set(key, { tokens, lastRefill: now }, ttl);
|
|
606
683
|
return false;
|
|
607
684
|
}
|
|
608
685
|
|
|
609
|
-
await store.set(key, { tokens: tokens - 1.0, lastRefill: now },
|
|
686
|
+
await store.set(key, { tokens: tokens - 1.0, lastRefill: now }, ttl);
|
|
610
687
|
return true;
|
|
611
688
|
}
|
|
612
689
|
|
|
@@ -656,20 +733,25 @@ const securityProfiles = {
|
|
|
656
733
|
headerAnomalyScore: 0.1,
|
|
657
734
|
requestPatternScore: 0.6,
|
|
658
735
|
inconsistencyScore: 0.8,
|
|
659
|
-
behaviorScore: 0.7,
|
|
736
|
+
behaviorScore: 0.7,
|
|
660
737
|
honeypotScore: 1.0,
|
|
738
|
+
botScore: 1.0,
|
|
739
|
+
cookieDroppingScore: 0.9,
|
|
661
740
|
crossLayerInconsistencyScore: 0.4,
|
|
662
741
|
timeInconsistencyScore: 0.9,
|
|
663
|
-
tlsSpoofingScore: 0.8,
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
742
|
+
tlsSpoofingScore: 0.8,
|
|
743
|
+
clientHintsInconsistencyScore: 0.7,
|
|
744
|
+
clickVarianceScore: 0.6,
|
|
745
|
+
subnetScore: 0.4,
|
|
746
|
+
ipReputationScore: 0.5,
|
|
747
|
+
botnetClusterScore: 0.6,
|
|
748
|
+
tcpAnomalyScore: 0.8,
|
|
668
749
|
quicAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie QUIC
|
|
669
|
-
protocolAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie HTTP/2
|
|
670
750
|
renderingAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie de rendu
|
|
751
|
+
http2AnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie HTTP/2
|
|
671
752
|
threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
|
|
672
753
|
virtualizationScore: 0.8,
|
|
754
|
+
mtuAnomalyScore: 0.9, // NOUVEAU: Poids pour l'anomalie MTU/fragmentation
|
|
673
755
|
},
|
|
674
756
|
thresholds: { low: 20, medium: 45, high: 75, block: 95 },
|
|
675
757
|
patterns: {
|
|
@@ -703,15 +785,23 @@ const securityProfiles = {
|
|
|
703
785
|
inconsistencyScore: 1.0,
|
|
704
786
|
behaviorScore: 0.8,
|
|
705
787
|
honeypotScore: 1.0,
|
|
788
|
+
botScore: 1.0,
|
|
789
|
+
cookieDroppingScore: 1.0,
|
|
706
790
|
crossLayerInconsistencyScore: 0.6,
|
|
707
791
|
timeInconsistencyScore: 1.0,
|
|
708
|
-
tlsSpoofingScore: 1.0,
|
|
792
|
+
tlsSpoofingScore: 1.0,
|
|
793
|
+
clientHintsInconsistencyScore: 0.9,
|
|
794
|
+
clickVarianceScore: 0.7,
|
|
709
795
|
subnetScore: 0.5,
|
|
710
|
-
ipReputationScore: 0.6,
|
|
711
|
-
botnetClusterScore: 0.8,
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
796
|
+
ipReputationScore: 0.6,
|
|
797
|
+
botnetClusterScore: 0.8,
|
|
798
|
+
tcpAnomalyScore: 1.0,
|
|
799
|
+
quicAnomalyScore: 1.0,
|
|
800
|
+
renderingAnomalyScore: 1.0,
|
|
801
|
+
http2AnomalyScore: 1.0,
|
|
802
|
+
threatIntelScore: 1.0,
|
|
803
|
+
mtuAnomalyScore: 0.9,
|
|
804
|
+
virtualizationScore: 1.0,
|
|
715
805
|
},
|
|
716
806
|
thresholds: { low: 10, medium: 35, high: 65, block: 90 },
|
|
717
807
|
patterns: {
|
|
@@ -746,15 +836,23 @@ const securityProfiles = {
|
|
|
746
836
|
inconsistencyScore: 0.7,
|
|
747
837
|
behaviorScore: 0.2, // Lower weight, as browser behavior is not applicable
|
|
748
838
|
honeypotScore: 1.0,
|
|
839
|
+
botScore: 0.8,
|
|
840
|
+
cookieDroppingScore: 0.8,
|
|
749
841
|
crossLayerInconsistencyScore: 0.5,
|
|
750
842
|
timeInconsistencyScore: 0.8,
|
|
751
|
-
tlsSpoofingScore: 0.7,
|
|
843
|
+
tlsSpoofingScore: 0.7,
|
|
844
|
+
clientHintsInconsistencyScore: 0.6,
|
|
845
|
+
clickVarianceScore: 0.3,
|
|
752
846
|
subnetScore: 0.4,
|
|
753
|
-
ipReputationScore: 0.5,
|
|
754
|
-
botnetClusterScore: 0.7,
|
|
755
|
-
tcpAnomalyScore: 0.8,
|
|
756
|
-
|
|
757
|
-
|
|
847
|
+
ipReputationScore: 0.5,
|
|
848
|
+
botnetClusterScore: 0.7,
|
|
849
|
+
tcpAnomalyScore: 0.8,
|
|
850
|
+
quicAnomalyScore: 0.8,
|
|
851
|
+
http2AnomalyScore: 0.8,
|
|
852
|
+
renderingAnomalyScore: 0.2,
|
|
853
|
+
threatIntelScore: 0.6,
|
|
854
|
+
virtualizationScore: 0.8,
|
|
855
|
+
mtuAnomalyScore: 0.9,
|
|
758
856
|
},
|
|
759
857
|
thresholds: { low: 25, medium: 50, high: 80, block: 95 },
|
|
760
858
|
patterns: {
|
|
@@ -801,9 +899,11 @@ const securityProfiles = {
|
|
|
801
899
|
ipReputationScore: 0.3, // NOUVEAU: Poids pour la réputation IP
|
|
802
900
|
botnetClusterScore: 0.5, // NOUVEAU: Poids pour le clustering botnet
|
|
803
901
|
tcpAnomalyScore: 0.5, // NEW: Anomalie de pile TCP/IP
|
|
804
|
-
protocolAnomalyScore: 0.5,
|
|
805
902
|
quicAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie QUIC
|
|
903
|
+
http2AnomalyScore: 0.5,
|
|
806
904
|
renderingAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie de rendu
|
|
905
|
+
mtuAnomalyScore: 0.9,
|
|
906
|
+
virtualizationScore: 0.8,
|
|
807
907
|
},
|
|
808
908
|
thresholds: { low: 25, medium: 55, high: 80, block: 95 },
|
|
809
909
|
patterns: {
|
|
@@ -832,7 +932,6 @@ const securityProfiles = {
|
|
|
832
932
|
historyScore: 0.4,
|
|
833
933
|
rotationScore: 0.6,
|
|
834
934
|
headerAnomalyScore: 0.2,
|
|
835
|
-
// Utilisation d'un score de pattern unifié avec un poids très élevé
|
|
836
935
|
requestPatternScore: 0.9,
|
|
837
936
|
inconsistencyScore: 1.0, // Crucial for preventing account takeover
|
|
838
937
|
behaviorScore: 0.8, // Important for checkout/login forms
|
|
@@ -849,9 +948,11 @@ const securityProfiles = {
|
|
|
849
948
|
ipReputationScore: 0.6, // NOUVEAU: Poids pour la réputation IP
|
|
850
949
|
botnetClusterScore: 0.9, // NOUVEAU: Poids pour le clustering botnet
|
|
851
950
|
tcpAnomalyScore: 0.9, // NEW: Anomalie de pile TCP/IP
|
|
852
|
-
protocolAnomalyScore: 0.9,
|
|
853
951
|
quicAnomalyScore: 0.9, // NOUVEAU: Poids pour l'anomalie QUIC
|
|
854
|
-
|
|
952
|
+
http2AnomalyScore: 0.9,
|
|
953
|
+
renderingAnomalyScore: 0.9,
|
|
954
|
+
mtuAnomalyScore: 0.9,
|
|
955
|
+
virtualizationScore: 0.8,
|
|
855
956
|
},
|
|
856
957
|
thresholds: { low: 15, medium: 40, high: 70, block: 90 },
|
|
857
958
|
patterns: {
|
|
@@ -1466,88 +1567,6 @@ export const verifyTspChallenge = (
|
|
|
1466
1567
|
}
|
|
1467
1568
|
};
|
|
1468
1569
|
|
|
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
1570
|
/**
|
|
1552
1571
|
* Verifies a memory PoW solution.
|
|
1553
1572
|
*
|
|
@@ -1725,6 +1744,11 @@ export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '',
|
|
|
1725
1744
|
if (!expiry || Date.now() > expiry) {
|
|
1726
1745
|
return false;
|
|
1727
1746
|
}
|
|
1747
|
+
if (statelessData.greenlist || (storedDeviceHash && storedDeviceHash.startsWith('webauthn:greenlist:'))) {
|
|
1748
|
+
if (!deviceId || deviceId === storedDeviceId) {
|
|
1749
|
+
return true;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1728
1752
|
if (storedDeviceHash && storedDeviceHash.startsWith('zkp:')) {
|
|
1729
1753
|
const expectedY = storedDeviceHash.split(':')[1];
|
|
1730
1754
|
if (zkpProof) {
|
|
@@ -1752,6 +1776,11 @@ export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '',
|
|
|
1752
1776
|
await store.delete(`ticket:${ticket}`);
|
|
1753
1777
|
return false;
|
|
1754
1778
|
}
|
|
1779
|
+
if (ticketData.greenlist || (storedDeviceHash && storedDeviceHash.startsWith('webauthn:greenlist:'))) {
|
|
1780
|
+
if (!deviceId || deviceId === storedDeviceId) {
|
|
1781
|
+
return true;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1755
1784
|
if (storedDeviceHash && storedDeviceHash.startsWith('zkp:')) {
|
|
1756
1785
|
const expectedY = storedDeviceHash.split(':')[1];
|
|
1757
1786
|
if (zkpProof) {
|
|
@@ -2335,13 +2364,37 @@ function analyzeTouchMovements(history) {
|
|
|
2335
2364
|
* @returns {{behaviorScore: number}}
|
|
2336
2365
|
*/
|
|
2337
2366
|
function getBehaviorScore(context) {
|
|
2338
|
-
const behaviorHeader = context.headers["x-behavior-metrics"];
|
|
2339
|
-
if (
|
|
2367
|
+
const behaviorHeader = context.headers ? (context.headers["x-behavior-metrics"] ?? context.headers["X-Behavior-Metrics"]) : undefined;
|
|
2368
|
+
if (behaviorHeader === undefined || behaviorHeader === null || behaviorHeader === '') {
|
|
2340
2369
|
return { behaviorScore: 0 }; // Pas de données, pas de pénalité.
|
|
2341
2370
|
}
|
|
2342
2371
|
|
|
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
|
+
// Traitement direct si un score numérique est envoyé côté client
|
|
2381
|
+
if (typeof behaviorHeader === 'number') {
|
|
2382
|
+
return { behaviorScore: Math.max(0, Math.min(100, behaviorHeader)) };
|
|
2383
|
+
}
|
|
2384
|
+
if (typeof behaviorHeader === 'string' && !isNaN(Number(behaviorHeader)) && behaviorHeader.trim() !== '') {
|
|
2385
|
+
const trimmed = behaviorHeader.trim();
|
|
2386
|
+
if (trimmed === '1') return { behaviorScore: 0 };
|
|
2387
|
+
if (trimmed === '0') return { behaviorScore: 100 };
|
|
2388
|
+
return { behaviorScore: Math.max(0, Math.min(100, parseFloat(trimmed))) };
|
|
2389
|
+
}
|
|
2390
|
+
|
|
2343
2391
|
try {
|
|
2344
2392
|
const metrics = JSON.parse(behaviorHeader);
|
|
2393
|
+
if (metrics === 1) return { behaviorScore: 0 };
|
|
2394
|
+
if (metrics === 0) return { behaviorScore: 100 };
|
|
2395
|
+
if (typeof metrics === 'number') {
|
|
2396
|
+
return { behaviorScore: Math.max(0, Math.min(100, metrics)) };
|
|
2397
|
+
}
|
|
2345
2398
|
let score = 0;
|
|
2346
2399
|
|
|
2347
2400
|
// 1. Pénalité maximale si un honeypot client a été déclenché.
|
|
@@ -2355,9 +2408,18 @@ function getBehaviorScore(context) {
|
|
|
2355
2408
|
const { avgSpeed, avgAcceleration, straightness, pauses, segments } = analyzeMouseMovements(metrics.mouseMovementsHistory);
|
|
2356
2409
|
const touchAnalysis = analyzeTouchMovements(metrics.touchMovementsHistory);
|
|
2357
2410
|
|
|
2358
|
-
// Pénalité pour absence totale d'interaction
|
|
2411
|
+
// Pénalité pour absence totale d'interaction. Un utilisateur légitime peut simplement lire la page.
|
|
2412
|
+
// On applique donc une pénalité de base faible, qui est amplifiée uniquement si d'autres
|
|
2413
|
+
// signaux passifs de bot (ex: rendu offscreen) sont présents.
|
|
2359
2414
|
if (avgSpeed === 0 && touchAnalysis.avgSpeed === 0 && metrics.keystrokeLatency === 0) {
|
|
2360
|
-
|
|
2415
|
+
let noInteractionPenalty = 5; // Pénalité de base très faible.
|
|
2416
|
+
|
|
2417
|
+
// Amplification si d'autres signaux passifs de bot sont présents.
|
|
2418
|
+
if (metrics.rendering?.offscreenAnom) {
|
|
2419
|
+
noInteractionPenalty += 40;
|
|
2420
|
+
}
|
|
2421
|
+
|
|
2422
|
+
score += noInteractionPenalty;
|
|
2361
2423
|
}
|
|
2362
2424
|
|
|
2363
2425
|
// 3. (NOUVEAU) Analyse de la longueur de l'historique de navigation.
|
|
@@ -2397,21 +2459,19 @@ function getBehaviorScore(context) {
|
|
|
2397
2459
|
if (touch.avgRadius > 0 && touch.radiusVariance === 0) {
|
|
2398
2460
|
score += 30; // Spoofed pointer area size
|
|
2399
2461
|
}
|
|
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)
|
|
2462
|
+
if (touch.segments.length > 10) {
|
|
2463
|
+
const benfordDev = Optimization.Operators.benfordTest(touch.segments);
|
|
2464
|
+
if (benfordDev > 0.18) score += 35;
|
|
2411
2465
|
}
|
|
2412
2466
|
}
|
|
2413
2467
|
}
|
|
2414
2468
|
|
|
2469
|
+
// Détection de ferme mobile : un appareil mobile parfaitement immobile est suspect, indépendamment des interactions tactiles.
|
|
2470
|
+
const isMobileDevice = (context.headers['user-agent'] || '').includes('Mobile');
|
|
2471
|
+
if (isMobileDevice && typeof metrics.motionVariance === 'number' && metrics.motionVariance === 0) {
|
|
2472
|
+
score += 50; // Terminal fixé sur un châssis mécanique (rack ADB)
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2415
2475
|
// Plausibilité de la latence de frappe
|
|
2416
2476
|
if (metrics.keystrokeLatency > 0 && metrics.keystrokeLatency < 40) score += 25; // Frappe trop rapide pour un humain.
|
|
2417
2477
|
if (metrics.keystrokeLatency > 1000) score += 15; // Latence très élevée, peut être un script lent.
|
|
@@ -2981,6 +3041,10 @@ function analyzeClickPositions(history) {
|
|
|
2981
3041
|
* @returns {{clickVarianceScore: number}}
|
|
2982
3042
|
*/
|
|
2983
3043
|
function getClickVarianceScore(context) {
|
|
3044
|
+
const rawMetrics = context.headers?.['x-behavior-metrics'];
|
|
3045
|
+
if (!rawMetrics || !isNaN(Number(rawMetrics))) {
|
|
3046
|
+
return { clickVarianceScore: 0 };
|
|
3047
|
+
}
|
|
2984
3048
|
const metrics = JSON.parse(context.headers['x-behavior-metrics'] || '{}');
|
|
2985
3049
|
const score = analyzeClickPositions(metrics.clicksHistory);
|
|
2986
3050
|
return { clickVarianceScore: score };
|
|
@@ -3832,7 +3896,7 @@ async function getBehavioralIndicators(context, deviceData) {
|
|
|
3832
3896
|
// --- VALIDATION DE L'ANCRAGE MATÉRIEL WEBAUTHN ---
|
|
3833
3897
|
const behaviorHeader = context.headers?.['x-behavior-metrics'];
|
|
3834
3898
|
let webauthnVerified = false;
|
|
3835
|
-
if (behaviorHeader) {
|
|
3899
|
+
if (behaviorHeader && typeof behaviorHeader === 'string' && behaviorHeader.startsWith('{')) {
|
|
3836
3900
|
try {
|
|
3837
3901
|
const metrics = JSON.parse(behaviorHeader);
|
|
3838
3902
|
if (metrics && metrics.webauthnAnchor) {
|
|
@@ -3908,6 +3972,52 @@ function getVirtualizationAnomalyScore(context) {
|
|
|
3908
3972
|
return Math.min(100.0, score);
|
|
3909
3973
|
}
|
|
3910
3974
|
|
|
3975
|
+
/**
|
|
3976
|
+
* @private
|
|
3977
|
+
* Analyzes TCP MTU and fragmentation flags to detect network tunnels (VPN/Proxy).
|
|
3978
|
+
* @param {object} context - The request context.
|
|
3979
|
+
* @returns {{mtuAnomalyScore: number}}
|
|
3980
|
+
*/
|
|
3981
|
+
function getMtuAnomalyScore(context) {
|
|
3982
|
+
const mtuHeader = context.headers?.['x-tcp-mtu-info'];
|
|
3983
|
+
if (!mtuHeader || typeof mtuHeader !== 'string') {
|
|
3984
|
+
return { mtuAnomalyScore: 0.0 };
|
|
3985
|
+
}
|
|
3986
|
+
|
|
3987
|
+
const parts = mtuHeader.split(':');
|
|
3988
|
+
if (parts.length < 2) {
|
|
3989
|
+
return { mtuAnomalyScore: 0.0 };
|
|
3990
|
+
}
|
|
3991
|
+
|
|
3992
|
+
const mtu = parseInt(parts[0], 10);
|
|
3993
|
+
const df = parseInt(parts[1], 10); // Don't Fragment bit (1 or 0)
|
|
3994
|
+
|
|
3995
|
+
if (isNaN(mtu) || isNaN(df)) {
|
|
3996
|
+
return { mtuAnomalyScore: 0.0 };
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3999
|
+
let score = 0.0;
|
|
4000
|
+
|
|
4001
|
+
// Pénalité modérée pour les MTU typiques des VPNs/tunnels.
|
|
4002
|
+
if (mtu > 1200 && mtu <= 1420) {
|
|
4003
|
+
score += 35.0;
|
|
4004
|
+
} else if (mtu > 1420 && mtu < 1492) {
|
|
4005
|
+
score += 20.0;
|
|
4006
|
+
}
|
|
4007
|
+
|
|
4008
|
+
const ua = context.headers?.['user-agent'] || '';
|
|
4009
|
+
const uaParts = parseUserAgent(ua);
|
|
4010
|
+
const os = uaParts.os;
|
|
4011
|
+
|
|
4012
|
+
// Pénalités additionnelles en cas d'incohérence OS vs signature réseau.
|
|
4013
|
+
if (os) {
|
|
4014
|
+
if (os.startsWith('Windows') && mtu < 1492) score += 20.0;
|
|
4015
|
+
if ((os.startsWith('Android') || os.startsWith('iOS')) && mtu < 1480) score += 15.0;
|
|
4016
|
+
if (df === 0 && (os.startsWith('Windows') || os.startsWith('Mac') || os.startsWith('Linux'))) score += 40.0;
|
|
4017
|
+
}
|
|
4018
|
+
|
|
4019
|
+
return { mtuAnomalyScore: Math.min(100.0, score) };
|
|
4020
|
+
}
|
|
3911
4021
|
|
|
3912
4022
|
/**
|
|
3913
4023
|
* Returns a vector of raw (unweighted) suspicion scores.
|
|
@@ -3976,7 +4086,12 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
3976
4086
|
const { botScore } = getBotScore(context);
|
|
3977
4087
|
|
|
3978
4088
|
// NOUVEAU: On calcule le score d'incohérence temporelle.
|
|
3979
|
-
|
|
4089
|
+
let parsedBehaviorMetrics = {};
|
|
4090
|
+
const rawBehaviorHeader = context.headers?.['x-behavior-metrics'];
|
|
4091
|
+
if (rawBehaviorHeader && typeof rawBehaviorHeader === 'string' && rawBehaviorHeader.startsWith('{')) {
|
|
4092
|
+
try { parsedBehaviorMetrics = JSON.parse(rawBehaviorHeader); } catch (e) {}
|
|
4093
|
+
}
|
|
4094
|
+
const { timeInconsistencyScore } = getTimeInconsistencyScore(context, parsedBehaviorMetrics, deviceData);
|
|
3980
4095
|
|
|
3981
4096
|
// NOUVEAU: On calcule le score d'incohérence entre les couches.
|
|
3982
4097
|
const { crossLayerInconsistencyScore } = getCrossLayerInconsistency(context);
|
|
@@ -3990,9 +4105,10 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
3990
4105
|
const { requestPatternScore } = getRequestPatternScore(context, deviceData, securityConfig.patterns);
|
|
3991
4106
|
|
|
3992
4107
|
const { tcpAnomalyScore } = getTcpAnomalyScore(context);
|
|
3993
|
-
const { protocolAnomalyScore } = getProtocolAnomalyScore(context);
|
|
4108
|
+
const { protocolAnomalyScore, http2AnomalyScore, quicAnomalyScore } = getProtocolAnomalyScore(context);
|
|
3994
4109
|
const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
|
|
3995
4110
|
const virtualizationScore = getVirtualizationAnomalyScore(context);
|
|
4111
|
+
const { mtuAnomalyScore } = getMtuAnomalyScore(context);
|
|
3996
4112
|
|
|
3997
4113
|
// Save the updated device state to the store
|
|
3998
4114
|
// Note: deviceData.ips is a Set, which may not serialize correctly in all stores (e.g., JSON). A Redis store should handle this via custom serialization or by converting to an array.
|
|
@@ -4003,8 +4119,8 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
4003
4119
|
if (Array.isArray(deviceData.ips)) {
|
|
4004
4120
|
deviceData.ips = new Set(deviceData.ips);
|
|
4005
4121
|
}
|
|
4006
|
-
|
|
4007
|
-
|
|
4122
|
+
// Le vecteur de suspicion est maintenant complet.
|
|
4123
|
+
return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, protocolAnomalyScore, http2AnomalyScore, quicAnomalyScore, renderingAnomalyScore, threatIntelScore, virtualizationScore, mtuAnomalyScore };
|
|
4008
4124
|
};
|
|
4009
4125
|
|
|
4010
4126
|
// A residential user can change networks (home, 4G, public wifi).
|
|
@@ -4027,7 +4143,31 @@ export const identifyRequest = (securityConfig) => async (req, res) => {
|
|
|
4027
4143
|
// This function now acts as a lightweight wrapper around the engine's identifyRequest method.
|
|
4028
4144
|
// It requires a default configuration to work.
|
|
4029
4145
|
const config = securityConfig || {
|
|
4030
|
-
weights: {
|
|
4146
|
+
weights: {
|
|
4147
|
+
historyScore: 0.3,
|
|
4148
|
+
rotationScore: 0.5,
|
|
4149
|
+
headerAnomalyScore: 0.2,
|
|
4150
|
+
requestPatternScore: 0.6,
|
|
4151
|
+
inconsistencyScore: 0.8,
|
|
4152
|
+
behaviorScore: 0.7,
|
|
4153
|
+
honeypotScore: 1.0,
|
|
4154
|
+
botScore: 1.0,
|
|
4155
|
+
cookieDroppingScore: 0.9,
|
|
4156
|
+
crossLayerInconsistencyScore: 0.4,
|
|
4157
|
+
timeInconsistencyScore: 0.9,
|
|
4158
|
+
tlsSpoofingScore: 0.8,
|
|
4159
|
+
clientHintsInconsistencyScore: 0.7,
|
|
4160
|
+
clickVarianceScore: 0.6,
|
|
4161
|
+
subnetScore: 0.4,
|
|
4162
|
+
ipReputationScore: 0.5,
|
|
4163
|
+
botnetClusterScore: 0.7,
|
|
4164
|
+
tcpAnomalyScore: 0.8,
|
|
4165
|
+
quicAnomalyScore: 0.8,
|
|
4166
|
+
renderingAnomalyScore: 0.8,
|
|
4167
|
+
http2AnomalyScore: 0.8,
|
|
4168
|
+
threatIntelScore: 1.0,
|
|
4169
|
+
virtualizationScore: 0.8
|
|
4170
|
+
},
|
|
4031
4171
|
thresholds: { low: 20, medium: 40, high: 75 },
|
|
4032
4172
|
honeypot: { fields: [] } // Ensure honeypot config exists to prevent errors
|
|
4033
4173
|
};
|
|
@@ -4199,6 +4339,24 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
|
|
|
4199
4339
|
</body></html>`;
|
|
4200
4340
|
}
|
|
4201
4341
|
|
|
4342
|
+
/**
|
|
4343
|
+
* Generates closed-shadow DOM honeypot markup for server-rendered challenge pages.
|
|
4344
|
+
* Utilizes Declarative Shadow DOM with mode="closed" to prevent screen reader false positives
|
|
4345
|
+
* while capturing bot web scrapers and crawlers.
|
|
4346
|
+
* @param {string[]} trapUrls
|
|
4347
|
+
* @returns {string}
|
|
4348
|
+
*/
|
|
4349
|
+
function generateClosedShadowTraps(trapUrls = []) {
|
|
4350
|
+
const list = Array.isArray(trapUrls) ? trapUrls : [];
|
|
4351
|
+
if (list.length === 0) return '';
|
|
4352
|
+
|
|
4353
|
+
const trapLinksHtml = list.map((url, idx) => {
|
|
4354
|
+
return `<a href="${url}" rel="nofollow" tabindex="-1"><span>> ${idx + 1}</span></a>`;
|
|
4355
|
+
}).join(' ');
|
|
4356
|
+
|
|
4357
|
+
return `<div style="position:absolute;left:-9999px;top:-9999px;width:0;height:0;overflow:hidden;visibility:hidden;pointer-events:none;" aria-hidden="true"><template shadowrootmode="closed" shadowroot="closed"><style>:host { position: absolute; left: -9999px; top: -9999px; width: 0; height: 0; overflow: hidden; visibility: hidden; pointer-events: none; } a { color: transparent; text-decoration: none; }</style>${trapLinksHtml}</template></div>`;
|
|
4358
|
+
}
|
|
4359
|
+
|
|
4202
4360
|
/**
|
|
4203
4361
|
* Generates the HTML content for a combined CPU + Memory PoW challenge.
|
|
4204
4362
|
* @param {object} cpuChallengeDetails - Details from generateCpuTargetChallenge.
|
|
@@ -4215,6 +4373,7 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
4215
4373
|
const fingerprint = originalFingerprint;
|
|
4216
4374
|
const baseBlock = createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint, clientIp, tlsSessionId);
|
|
4217
4375
|
const baseBlockBytes = `[${baseBlock.toString('utf8').split('').map(c => c.charCodeAt(0)).join(',')}]`;
|
|
4376
|
+
const trapContainerHtml = generateClosedShadowTraps(trapUrls);
|
|
4218
4377
|
|
|
4219
4378
|
// Prépare la configuration pour l'initialisation du client, y compris les URL pièges.
|
|
4220
4379
|
const clientInitConfig = {
|
|
@@ -4282,12 +4441,24 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
4282
4441
|
}
|
|
4283
4442
|
|
|
4284
4443
|
if (!htmlTemplate) {
|
|
4285
|
-
htmlTemplate = `<html><head><title>Advanced Security Check</title></head><body style="font-family:sans-serif; text-align:center; padding-top:50px;"><h1>Enhanced Verification... (Level 2)</h1><p>Your activity requires an additional security check. This may take a few moments.</p><div id="loader" style="margin:20px;">⚙️ Initializing combined verification...</div><script><!-- FINGERPRINT_SOLVER_SCRIPT --></script><script><!-- FINGERPRINT_CHALLENGE_SCRIPT --></script
|
|
4444
|
+
htmlTemplate = `<html><head><title>Advanced Security Check</title></head><body style="font-family:sans-serif; text-align:center; padding-top:50px;"><h1>Enhanced Verification... (Level 2)</h1><p>Your activity requires an additional security check. This may take a few moments.</p><div id="loader" style="margin:20px;">⚙️ Initializing combined verification...</div><script><!-- FINGERPRINT_SOLVER_SCRIPT --></script><script><!-- FINGERPRINT_CHALLENGE_SCRIPT --></script><!-- FINGERPRINT_TRAPS --></body></html>`; // eslint-disable-line max-len
|
|
4286
4445
|
}
|
|
4287
4446
|
|
|
4288
|
-
|
|
4447
|
+
let renderedHtml = htmlTemplate
|
|
4289
4448
|
.replace('<!-- FINGERPRINT_SOLVER_SCRIPT -->', solverCode)
|
|
4290
4449
|
.replace('<!-- FINGERPRINT_CHALLENGE_SCRIPT -->', challengeScript);
|
|
4450
|
+
|
|
4451
|
+
if (renderedHtml.includes('<!-- FINGERPRINT_TRAPS -->')) {
|
|
4452
|
+
renderedHtml = renderedHtml.replace('<!-- FINGERPRINT_TRAPS -->', trapContainerHtml);
|
|
4453
|
+
} else if (trapContainerHtml) {
|
|
4454
|
+
if (renderedHtml.includes('</body>')) {
|
|
4455
|
+
renderedHtml = renderedHtml.replace('</body>', `${trapContainerHtml}</body>`);
|
|
4456
|
+
} else {
|
|
4457
|
+
renderedHtml += trapContainerHtml;
|
|
4458
|
+
}
|
|
4459
|
+
}
|
|
4460
|
+
|
|
4461
|
+
return renderedHtml;
|
|
4291
4462
|
}
|
|
4292
4463
|
|
|
4293
4464
|
/**
|
|
@@ -4415,7 +4586,7 @@ async function getThreatIntelScore(context, zkpY, threatIntelConfig = {}) {
|
|
|
4415
4586
|
const tcpRtt = tcpRttHeader ? parseInt(tcpRttHeader, 10) : null;
|
|
4416
4587
|
|
|
4417
4588
|
const behaviorHeader = context.headers?.['x-behavior-metrics'];
|
|
4418
|
-
if (behaviorHeader) {
|
|
4589
|
+
if (behaviorHeader && typeof behaviorHeader === 'string' && behaviorHeader.startsWith('{')) {
|
|
4419
4590
|
try {
|
|
4420
4591
|
const metrics = JSON.parse(behaviorHeader);
|
|
4421
4592
|
if (metrics && typeof metrics.clientTimestamp === 'number' && typeof context.requestTimestamp === 'number') {
|
|
@@ -4577,7 +4748,9 @@ export class FingerprintEngine {
|
|
|
4577
4748
|
'wasm',
|
|
4578
4749
|
'similarityThreshold', 'reset',
|
|
4579
4750
|
'ed25519_private_key', 'ed25519_public_key', 'upowModel',
|
|
4580
|
-
'federatedPeers', 'federationSecret', 'filterWhitelist'
|
|
4751
|
+
'federatedPeers', 'federationSecret', 'filterWhitelist',
|
|
4752
|
+
'challengeRateLimit',
|
|
4753
|
+
'differentialPrivacy', 'dpEpsilon'
|
|
4581
4754
|
]);
|
|
4582
4755
|
|
|
4583
4756
|
// 1. Check for essential keys
|
|
@@ -4615,36 +4788,31 @@ export class FingerprintEngine {
|
|
|
4615
4788
|
}
|
|
4616
4789
|
}
|
|
4617
4790
|
calculateFinalScore(suspicionVector) {
|
|
4618
|
-
const
|
|
4619
|
-
if (!
|
|
4620
|
-
|
|
4621
|
-
const
|
|
4622
|
-
|
|
4623
|
-
(
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
(
|
|
4627
|
-
(
|
|
4628
|
-
(
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
(
|
|
4632
|
-
|
|
4633
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
(suspicionVector.virtualizationScore || 0) * (weights.virtualizationScore || 0) +
|
|
4644
|
-
(suspicionVector.threatIntelScore || 0) * (weights.threatIntelScore || 0) + // NOUVEAU: QUIC Anomaly
|
|
4645
|
-
(suspicionVector.renderingAnomalyScore || 0) * (weights.renderingAnomalyScore || 0); // NOUVEAU: Rendering Anomaly
|
|
4646
|
-
|
|
4647
|
-
return Math.min(100, score);
|
|
4791
|
+
const baseWeights = this.securityConfig.weights;
|
|
4792
|
+
if (!baseWeights) return 0;
|
|
4793
|
+
|
|
4794
|
+
const dynamicWeights = { ...baseWeights };
|
|
4795
|
+
if ((suspicionVector.mtuAnomalyScore || 0) > 50.0) {
|
|
4796
|
+
this._log('Tunnel detected, amplifying suspicion weights.', { mtuScore: suspicionVector.mtuAnomalyScore });
|
|
4797
|
+
|
|
4798
|
+
// Augmente le poids des incohérences de bas niveau (difficiles à falsifier)
|
|
4799
|
+
dynamicWeights.tlsSpoofingScore = (baseWeights.tlsSpoofingScore || 0.8) * 1.25;
|
|
4800
|
+
dynamicWeights.crossLayerInconsistencyScore = (baseWeights.crossLayerInconsistencyScore || 0.4) * 1.4;
|
|
4801
|
+
dynamicWeights.clientHintsInconsistencyScore = (baseWeights.clientHintsInconsistencyScore || 0.7) * 1.2;
|
|
4802
|
+
|
|
4803
|
+
// Augmente le poids des anomalies comportementales (un bot derrière un VPN est plus suspect)
|
|
4804
|
+
dynamicWeights.behaviorScore = (baseWeights.behaviorScore || 0.7) * 1.15;
|
|
4805
|
+
dynamicWeights.requestPatternScore = (baseWeights.requestPatternScore || 0.6) * 1.2;
|
|
4806
|
+
}
|
|
4807
|
+
|
|
4808
|
+
let score = 0;
|
|
4809
|
+
for (const key in dynamicWeights) {
|
|
4810
|
+
const metricScore = suspicionVector[key] || 0.0;
|
|
4811
|
+
if (metricScore > 0) {
|
|
4812
|
+
score += metricScore * dynamicWeights[key];
|
|
4813
|
+
}
|
|
4814
|
+
}
|
|
4815
|
+
return Math.min(100.0, score);
|
|
4648
4816
|
}
|
|
4649
4817
|
|
|
4650
4818
|
|
|
@@ -4965,9 +5133,84 @@ export class FingerprintEngine {
|
|
|
4965
5133
|
|
|
4966
5134
|
// Bypass instantané si l'appareil a prouvé cryptographiquement son identité matérielle (Secure Enclave / TPM)
|
|
4967
5135
|
const { deviceId, deviceData, newCookie } = await resolveRequestIdentity(requestContext, this.securityConfig);
|
|
5136
|
+
|
|
5137
|
+
// Règle Anti-Ferme : Même avec du matériel sécurisé, un appareil condamné ou menant une attaque certaine est révoqué et bloqué
|
|
5138
|
+
if (deviceData?.condemned || this._hasCertainAttack(requestContext)) {
|
|
5139
|
+
if (deviceData) {
|
|
5140
|
+
deviceData.condemned = true;
|
|
5141
|
+
deviceData.webauthnVerified = false;
|
|
5142
|
+
await store.set(`device:${deviceId}`, deviceData);
|
|
5143
|
+
}
|
|
5144
|
+
this._log('Condemned device or attack detected - revoking hardware trust and blocking', { deviceId });
|
|
5145
|
+
const decision = { action: 'block', status: 404, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
|
|
5146
|
+
if (this.dryRun) {
|
|
5147
|
+
this._log(`[Dry Run] Intended action: ${decision.action}`, { score: decision.score });
|
|
5148
|
+
decision.intendedAction = decision.action;
|
|
5149
|
+
decision.action = 'next';
|
|
5150
|
+
delete decision.status;
|
|
5151
|
+
delete decision.body;
|
|
5152
|
+
if (requestContext._newCookies) {
|
|
5153
|
+
const deviceCookie = requestContext._newCookies.find(c => c.name === 'device_id');
|
|
5154
|
+
if (deviceCookie) decision.newCookieForResponse = deviceCookie;
|
|
5155
|
+
}
|
|
5156
|
+
}
|
|
5157
|
+
return decision;
|
|
5158
|
+
}
|
|
5159
|
+
|
|
5160
|
+
// Validation immédiate de l'attestation matérielle WebAuthn si présente dans la requête
|
|
5161
|
+
const behaviorHeader = requestContext.headers?.['x-behavior-metrics'];
|
|
5162
|
+
if (behaviorHeader && typeof behaviorHeader === 'string' && behaviorHeader.startsWith('{')) {
|
|
5163
|
+
try {
|
|
5164
|
+
const parsedMetrics = JSON.parse(behaviorHeader);
|
|
5165
|
+
if (parsedMetrics && parsedMetrics.webauthnAnchor && deviceData) {
|
|
5166
|
+
if (verifyWebAuthnHardwareAnchor(parsedMetrics.webauthnAnchor, deviceData)) {
|
|
5167
|
+
deviceData.webauthnVerified = true;
|
|
5168
|
+
await store.set(`device:${deviceId}`, deviceData);
|
|
5169
|
+
}
|
|
5170
|
+
}
|
|
5171
|
+
} catch (e) {}
|
|
5172
|
+
}
|
|
5173
|
+
|
|
4968
5174
|
if (deviceData && deviceData.webauthnVerified) {
|
|
4969
5175
|
this._log('Hardware-anchored device verified (WebAuthn) - full bypass granted', { deviceId });
|
|
4970
|
-
|
|
5176
|
+
const greenlistTtl = 365 * 86400 * 1000; // 1 an (permanent)
|
|
5177
|
+
const greenlistTicket = generateStatelessTicket({
|
|
5178
|
+
expiry: Date.now() + greenlistTtl,
|
|
5179
|
+
originalIp: clientIp,
|
|
5180
|
+
deviceId,
|
|
5181
|
+
deviceHash: 'webauthn:greenlist:' + (deviceData.webauthnCredentialId || deviceId),
|
|
5182
|
+
greenlist: true
|
|
5183
|
+
});
|
|
5184
|
+
const isHttps = requestContext.headers?.['x-forwarded-proto'] === 'https' ||
|
|
5185
|
+
requestContext.rawReq?.secure ||
|
|
5186
|
+
requestContext.rawReq?.protocol === 'https' ||
|
|
5187
|
+
requestContext.rawReq?.connection?.encrypted;
|
|
5188
|
+
const secureOption = isHttps || this.isProduction;
|
|
5189
|
+
|
|
5190
|
+
const decision = {
|
|
5191
|
+
action: 'next',
|
|
5192
|
+
score: 0,
|
|
5193
|
+
vector: { webauthn_verified: 100 },
|
|
5194
|
+
cookie: {
|
|
5195
|
+
name: 'pow_clearance',
|
|
5196
|
+
value: greenlistTicket,
|
|
5197
|
+
options: {
|
|
5198
|
+
httpOnly: true,
|
|
5199
|
+
secure: secureOption,
|
|
5200
|
+
sameSite: 'strict',
|
|
5201
|
+
...(secureOption && { partitioned: true }),
|
|
5202
|
+
maxAge: greenlistTtl,
|
|
5203
|
+
path: '/'
|
|
5204
|
+
}
|
|
5205
|
+
}
|
|
5206
|
+
};
|
|
5207
|
+
if (requestContext._newCookies) {
|
|
5208
|
+
const deviceCookie = requestContext._newCookies.find(c => c.name === 'device_id');
|
|
5209
|
+
if (deviceCookie) {
|
|
5210
|
+
decision.newCookieForResponse = deviceCookie;
|
|
5211
|
+
}
|
|
5212
|
+
}
|
|
5213
|
+
return decision;
|
|
4971
5214
|
}
|
|
4972
5215
|
|
|
4973
5216
|
if (isStatic) {
|
|
@@ -5010,7 +5253,7 @@ export class FingerprintEngine {
|
|
|
5010
5253
|
if (filterWhitelist === true) {
|
|
5011
5254
|
bypassWhitelist = this._hasCertainAttack(requestContext);
|
|
5012
5255
|
} else if (typeof filterWhitelist === 'number') {
|
|
5013
|
-
|
|
5256
|
+
const res = await getScoreAndVector();
|
|
5014
5257
|
if (res.score > filterWhitelist) {
|
|
5015
5258
|
bypassWhitelist = true;
|
|
5016
5259
|
}
|
|
@@ -5589,10 +5832,11 @@ export class FingerprintEngine {
|
|
|
5589
5832
|
this._log('High suspicion score detected - overriding valid ticket to re-issue challenge', { finalScore, deviceId });
|
|
5590
5833
|
}
|
|
5591
5834
|
|
|
5592
|
-
// ---
|
|
5593
|
-
const
|
|
5835
|
+
// --- Limiteur de débit par domaine et sous-réseau (Token Bucket) ---
|
|
5836
|
+
const domain = requestContext.headers?.host || 'default';
|
|
5837
|
+
const rateLimitPassed = await checkChallengeRateLimit(clientIp, domain, this.securityConfig?.challengeRateLimit);
|
|
5594
5838
|
if (!rateLimitPassed) {
|
|
5595
|
-
this._log('Challenge rate limit exceeded - blocking with 429', { clientIp });
|
|
5839
|
+
this._log('Challenge rate limit exceeded - blocking with 429', { clientIp, domain });
|
|
5596
5840
|
const decision = {
|
|
5597
5841
|
action: 'block',
|
|
5598
5842
|
status: 429,
|
|
@@ -6599,6 +6843,7 @@ export const default_whitelist = () => [
|
|
|
6599
6843
|
googlebot_whitelist(),
|
|
6600
6844
|
bingbot_whitelist(),
|
|
6601
6845
|
yandex_whitelist(),
|
|
6846
|
+
facebook_whitelist(),
|
|
6602
6847
|
// === Moteurs de recherche majeurs ===
|
|
6603
6848
|
{ userAgent: 'Googlebot', hostnameSuffix: '.googlebot.com' },
|
|
6604
6849
|
{ userAgent: 'Google-Extended', hostnameSuffix: '.google.com' },
|
|
@@ -6716,6 +6961,11 @@ export const bingbot_whitelist = () => ({
|
|
|
6716
6961
|
entries: bingbotEntries
|
|
6717
6962
|
});
|
|
6718
6963
|
|
|
6964
|
+
export const facebook_whitelist = () => ({
|
|
6965
|
+
type: 'allowlist',
|
|
6966
|
+
entries: facebookEntries
|
|
6967
|
+
});
|
|
6968
|
+
|
|
6719
6969
|
|
|
6720
6970
|
/**
|
|
6721
6971
|
* Extracts the TLS Session ID or ticket hash from the request context.
|
|
@@ -6949,6 +7199,9 @@ export const powMiddleware = (securityConfig) => {
|
|
|
6949
7199
|
|
|
6950
7200
|
case 'next':
|
|
6951
7201
|
default:
|
|
7202
|
+
if (decision.cookie && res && typeof res.cookie === 'function') {
|
|
7203
|
+
res.cookie(decision.cookie.name, decision.cookie.value, decision.cookie.options);
|
|
7204
|
+
}
|
|
6952
7205
|
return next();
|
|
6953
7206
|
}
|
|
6954
7207
|
};
|
|
@@ -6962,8 +7215,10 @@ export const powMiddleware = (securityConfig) => {
|
|
|
6962
7215
|
*/
|
|
6963
7216
|
export const __internal = {
|
|
6964
7217
|
get store() { return store; }, // Export the store for testing
|
|
7218
|
+
checkChallengeRateLimit,
|
|
6965
7219
|
getDeviceHash,
|
|
6966
7220
|
getCompositeDeviceHash,
|
|
7221
|
+
generateClosedShadowTraps,
|
|
6967
7222
|
getSuspicionVector,
|
|
6968
7223
|
getTlsSessionId,
|
|
6969
7224
|
pruneTrafficData,
|