@anonympins/fingerprint 0.5.0 → 0.5.2
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 +31 -0
- package/README.md +118 -115
- package/package.json +1 -1
- package/src/js/dynamic-wasm.js +52 -13
- package/src/js/fingerprint.builder.js +169 -168
- package/src/js/fingerprint.client.js +224 -24
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +382 -63
- package/src/js/fingerprint.utils.js +213 -183
- package/src/js/gpu_pow.solver.js +312 -0
- package/src/js/library.js +16 -13
- package/src/js/pow.solver.inline.js +138 -27
- package/src/js/pow.solver.js +141 -28
- package/src/js/tests/fingerprint.client.init.test.js +5 -2
- package/src/js/tests/fingerprint.engine.test.js +370 -370
- package/src/js/tests/fingerprint.test.js +150 -3
- package/src/js/tests/gpu_pow.test.js +199 -0
- package/src/js/tests/pow.solver.test.js +4 -4
- package/src/js/tests/quicFingerprint.test.js +35 -0
- package/src/php/Challenge/ChallengeUtils.php +158 -27
- package/src/php/Config/SecurityProfiles.php +9 -0
- package/src/php/FingerprintEngine.php +124 -2
- package/src/php/RequestContext.php +2 -0
- package/src/php/Tests/ChallengeUtilsTest.php +77 -0
- package/src/php/Tests/QuicFingerprintTest.php +54 -0
- package/src/php/Tests/RequestUtilsTest.php +43 -0
- package/src/php/Utils/RequestUtils.php +120 -0
package/src/js/fingerprint.js
CHANGED
|
@@ -8,7 +8,20 @@ import {DynamicWasmGenerator} from "./dynamic-wasm.js";
|
|
|
8
8
|
import {readFileSync, existsSync} from "node:fs";
|
|
9
9
|
import {fileURLToPath} from "node:url";
|
|
10
10
|
import {dirname, join, resolve} from "node:path";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
verifyZkpProof,
|
|
13
|
+
sanitizeRedirectPath,
|
|
14
|
+
decodePolymorphicFingerprint,
|
|
15
|
+
deepMerge,
|
|
16
|
+
getHeaderSignature,
|
|
17
|
+
parseJa3,
|
|
18
|
+
modPow,
|
|
19
|
+
hashNetwork,
|
|
20
|
+
normalizeReferer,
|
|
21
|
+
isPrivateIp,
|
|
22
|
+
parseUserAgent,
|
|
23
|
+
safeJsonStringify
|
|
24
|
+
} from "./fingerprint.utils.js";
|
|
12
25
|
|
|
13
26
|
|
|
14
27
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -22,6 +35,42 @@ let lastMappingTime = 0;
|
|
|
22
35
|
let isCompilingMapping = false;
|
|
23
36
|
const MAPPING_ROTATION_INTERVAL = 60000; // 60 seconds
|
|
24
37
|
|
|
38
|
+
const configDir = resolve(__dirname, '../../config');
|
|
39
|
+
|
|
40
|
+
const loadBotWhitelist = (filename, fallbackEntries) => {
|
|
41
|
+
const filePath = join(configDir, filename);
|
|
42
|
+
if (existsSync(filePath)) {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
45
|
+
} catch (e) {
|
|
46
|
+
console.error(`[Fingerprint] Error loading whitelist file ${filename}:`, e.message);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return fallbackEntries;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const googlebotEntries = loadBotWhitelist('googlebot.json', [
|
|
53
|
+
"2001:4860:4801:10::/64",
|
|
54
|
+
"2001:4860:4801:11::/64",
|
|
55
|
+
"2001:4860:4801:12::/64",
|
|
56
|
+
// ... [Keep fallback inline values for safety]
|
|
57
|
+
"66.249.79.64"
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const bingbotEntries = loadBotWhitelist('bingbot.json', [
|
|
61
|
+
"157.55.39.0/24",
|
|
62
|
+
"207.46.13.0/24",
|
|
63
|
+
// ... [Keep fallback inline values for safety]
|
|
64
|
+
"40.77.178.0/23"
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
const yandexEntries = loadBotWhitelist('yandex.json', [
|
|
68
|
+
"2a02:6b8::/29",
|
|
69
|
+
"5.45.192.0/18",
|
|
70
|
+
// ... [Keep fallback inline values for safety]
|
|
71
|
+
"213.180.192.0/19"
|
|
72
|
+
]);
|
|
73
|
+
|
|
25
74
|
function generateSessionMapping() {
|
|
26
75
|
const randomStr = (len = 6) => crypto.randomBytes(len).toString('hex').replace(/[0-9]/g, 'g').substring(0, len);
|
|
27
76
|
const randomHeader = () => `X-Sess-${crypto.randomBytes(4).toString('hex')}`;
|
|
@@ -189,7 +238,82 @@ export function generateStatelessTicket(payload) {
|
|
|
189
238
|
const signature = crypto.createHmac('sha256', key).update(Buffer.concat([iv, encrypted])).digest();
|
|
190
239
|
return `${base64UrlEncode(iv)}.${base64UrlEncode(encrypted)}.${base64UrlEncode(signature)}`;
|
|
191
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* Détecte les anomalies de flux QUIC/HTTP3 par rapport au User-Agent.
|
|
243
|
+
* @private
|
|
244
|
+
* @param {object} context - Le contexte de la requête.
|
|
245
|
+
* @returns {{quicAnomalyScore: number}}
|
|
246
|
+
*/
|
|
247
|
+
function getQuicAnomalyScore(context) {
|
|
248
|
+
const quicFp = context.headers?.['x-quic-fp'] || context.quicFingerprint || null;
|
|
249
|
+
if (!quicFp || typeof quicFp !== 'string') {
|
|
250
|
+
return { quicAnomalyScore: 0.0 };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const parts = quicFp.split(';');
|
|
254
|
+
if (parts.length < 2) return { quicAnomalyScore: 0.0 };
|
|
255
|
+
|
|
256
|
+
const params = {};
|
|
257
|
+
parts[1].split(',').forEach(p => {
|
|
258
|
+
const kv = p.split('=');
|
|
259
|
+
if (kv.length === 2) params[kv[0]] = kv[1];
|
|
260
|
+
});
|
|
261
|
+
const priorityOrder = parts[2] || '';
|
|
262
|
+
|
|
263
|
+
const ua = context.headers?.['user-agent'] || '';
|
|
264
|
+
const uaParts = parseUserAgent(ua);
|
|
265
|
+
const browser = uaParts.browser;
|
|
192
266
|
|
|
267
|
+
if (!browser) return { quicAnomalyScore: 0.0 };
|
|
268
|
+
|
|
269
|
+
let anomaly = 0.0;
|
|
270
|
+
if (browser.startsWith('Chrome') || browser.startsWith('Edge')) {
|
|
271
|
+
const maxData = parseInt(params['1'] || '0', 10);
|
|
272
|
+
const maxStreams = parseInt(params['4'] || '0', 10);
|
|
273
|
+
if (maxData > 0 && maxData < 1048576) anomaly += 40.0;
|
|
274
|
+
if (maxStreams > 0 && maxStreams !== 100) anomaly += 30.0;
|
|
275
|
+
if (priorityOrder && !priorityOrder.includes('u=')) anomaly += 30.0;
|
|
276
|
+
} else if (browser.startsWith('Firefox')) {
|
|
277
|
+
const maxData = parseInt(params['1'] || '0', 10);
|
|
278
|
+
if (maxData > 0 && maxData > 5000000) anomaly += 40.0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return { quicAnomalyScore: Math.max(0.0, Math.min(100.0, anomaly)) };
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Détecte les anomalies de rendu (V-Sync, FPS, gigue) à partir des métriques d'affichage.
|
|
285
|
+
* @private
|
|
286
|
+
* @param {object} context - Le contexte de la requête.
|
|
287
|
+
* @returns {{renderingAnomalyScore: number}}
|
|
288
|
+
*/
|
|
289
|
+
function getRenderingAnomalyScore(context) {
|
|
290
|
+
const behaviorHeader = context.headers?.['x-behavior-metrics'];
|
|
291
|
+
if (!behaviorHeader) {
|
|
292
|
+
return { renderingAnomalyScore: 0.0 };
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
const metrics = JSON.parse(behaviorHeader);
|
|
296
|
+
if (!metrics || !metrics.rendering) {
|
|
297
|
+
return { renderingAnomalyScore: 0.0 };
|
|
298
|
+
}
|
|
299
|
+
const rendering = metrics.rendering;
|
|
300
|
+
let score = 0.0;
|
|
301
|
+
if (rendering.offscreenAnom) {
|
|
302
|
+
score += 100.0;
|
|
303
|
+
}
|
|
304
|
+
const fps = parseFloat(rendering.fps || 0.0);
|
|
305
|
+
const jitter = parseFloat(rendering.jitter || 0.0);
|
|
306
|
+
if (fps > 250.0 || (fps > 0.0 && fps < 15.0)) {
|
|
307
|
+
score += 50.0;
|
|
308
|
+
}
|
|
309
|
+
if (jitter > 6.0) {
|
|
310
|
+
score += Math.min(80.0, (jitter - 6.0) * 10.0);
|
|
311
|
+
}
|
|
312
|
+
return { renderingAnomalyScore: Math.min(100.0, score) };
|
|
313
|
+
} catch (e) {
|
|
314
|
+
return { renderingAnomalyScore: 0.0 };
|
|
315
|
+
}
|
|
316
|
+
}
|
|
193
317
|
export function parseStatelessTicket(ticket) {
|
|
194
318
|
try {
|
|
195
319
|
if (ticket.startsWith('ed25519.')) {
|
|
@@ -304,7 +428,10 @@ const securityProfiles = {
|
|
|
304
428
|
tlsSpoofingScore: 0.8, // NOUVEAU: Poids pour la détection de spoofing TLS
|
|
305
429
|
subnetScore: 0.4, // NOUVEAU: Poids pour la réputation du sous-réseau
|
|
306
430
|
ipReputationScore: 0.5, // NOUVEAU: Poids pour la réputation IP
|
|
307
|
-
botnetClusterScore: 0.6 // NOUVEAU: Poids pour le clustering botnet
|
|
431
|
+
botnetClusterScore: 0.6, // NOUVEAU: Poids pour le clustering botnet
|
|
432
|
+
tcpAnomalyScore: 0.8, // NEW: Anomalie de pile TCP/IP
|
|
433
|
+
quicAnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie QUIC
|
|
434
|
+
renderingAnomalyScore: 0.8 // NOUVEAU: Poids pour l'anomalie de rendu
|
|
308
435
|
},
|
|
309
436
|
thresholds: { low: 20, medium: 45, high: 75, block: 95 },
|
|
310
437
|
patterns: {
|
|
@@ -340,7 +467,8 @@ const securityProfiles = {
|
|
|
340
467
|
tlsSpoofingScore: 1.0, // Plus agressif pour le spoofing TLS
|
|
341
468
|
subnetScore: 0.5,
|
|
342
469
|
ipReputationScore: 0.6, // NOUVEAU: Poids pour la réputation IP
|
|
343
|
-
botnetClusterScore: 0.8 // NOUVEAU: Poids pour le clustering botnet
|
|
470
|
+
botnetClusterScore: 0.8, // NOUVEAU: Poids pour le clustering botnet
|
|
471
|
+
renderingAnomalyScore: 1.0 // NOUVEAU: Poids pour l'anomalie de rendu
|
|
344
472
|
},
|
|
345
473
|
thresholds: { low: 10, medium: 35, high: 65, block: 90 },
|
|
346
474
|
patterns: {
|
|
@@ -377,7 +505,9 @@ const securityProfiles = {
|
|
|
377
505
|
tlsSpoofingScore: 0.7, // Important pour les API
|
|
378
506
|
subnetScore: 0.4,
|
|
379
507
|
ipReputationScore: 0.5, // NOUVEAU: Poids pour la réputation IP
|
|
380
|
-
botnetClusterScore: 0.7 // NOUVEAU: Poids pour le clustering botnet
|
|
508
|
+
botnetClusterScore: 0.7, // NOUVEAU: Poids pour le clustering botnet
|
|
509
|
+
tcpAnomalyScore: 0.8, // NEW: Anomalie de pile TCP/IP
|
|
510
|
+
quicAnomalyScore: 0.8 // NOUVEAU: Poids pour l'anomalie QUIC
|
|
381
511
|
},
|
|
382
512
|
thresholds: { low: 25, medium: 50, high: 80, block: 95 },
|
|
383
513
|
patterns: {
|
|
@@ -415,7 +545,10 @@ const securityProfiles = {
|
|
|
415
545
|
tlsSpoofingScore: 0.6, // Moins critique pour les blogs
|
|
416
546
|
subnetScore: 0.2,
|
|
417
547
|
ipReputationScore: 0.3, // NOUVEAU: Poids pour la réputation IP
|
|
418
|
-
botnetClusterScore: 0.5 // NOUVEAU: Poids pour le clustering botnet
|
|
548
|
+
botnetClusterScore: 0.5, // NOUVEAU: Poids pour le clustering botnet
|
|
549
|
+
tcpAnomalyScore: 0.5, // NEW: Anomalie de pile TCP/IP
|
|
550
|
+
quicAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie QUIC
|
|
551
|
+
renderingAnomalyScore: 0.5 // NOUVEAU: Poids pour l'anomalie de rendu
|
|
419
552
|
},
|
|
420
553
|
thresholds: { low: 25, medium: 55, high: 80, block: 95 },
|
|
421
554
|
patterns: {
|
|
@@ -452,7 +585,10 @@ const securityProfiles = {
|
|
|
452
585
|
tlsSpoofingScore: 0.9, // Très important pour l'e-commerce
|
|
453
586
|
subnetScore: 0.5,
|
|
454
587
|
ipReputationScore: 0.6, // NOUVEAU: Poids pour la réputation IP
|
|
455
|
-
botnetClusterScore: 0.9 // NOUVEAU: Poids pour le clustering botnet
|
|
588
|
+
botnetClusterScore: 0.9, // NOUVEAU: Poids pour le clustering botnet
|
|
589
|
+
tcpAnomalyScore: 0.9, // NEW: Anomalie de pile TCP/IP
|
|
590
|
+
quicAnomalyScore: 0.9, // NOUVEAU: Poids pour l'anomalie QUIC
|
|
591
|
+
renderingAnomalyScore: 0.9 // NOUVEAU: Poids pour l'anomalie de rendu
|
|
456
592
|
},
|
|
457
593
|
thresholds: { low: 15, medium: 40, high: 70, block: 90 },
|
|
458
594
|
patterns: {
|
|
@@ -497,15 +633,17 @@ const getPowSecret = () => {
|
|
|
497
633
|
return secret || "fallback-dev-secret-32-chars-minimum";
|
|
498
634
|
};
|
|
499
635
|
|
|
636
|
+
let cachedPowSolverCode = null;
|
|
500
637
|
/**
|
|
501
638
|
* Loads the pow.solver.js content for inlining in HTML pages.
|
|
502
639
|
* @returns {string} The solver JavaScript code.
|
|
503
640
|
*/
|
|
504
641
|
const getPowSolverCode = () => {
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
642
|
+
if (!cachedPowSolverCode) {
|
|
643
|
+
const solverPath = join(__dirname, 'pow.solver.inline.js'); // Utilise la version inline
|
|
644
|
+
cachedPowSolverCode = readFileSync(solverPath, 'utf-8');
|
|
645
|
+
}
|
|
646
|
+
return cachedPowSolverCode;
|
|
509
647
|
};
|
|
510
648
|
/**
|
|
511
649
|
* Extracts the "stable" part of a fingerprint string.
|
|
@@ -771,6 +909,7 @@ const generateTspChallenge = (
|
|
|
771
909
|
) => {
|
|
772
910
|
const citiesJson = JSON.stringify(cities);
|
|
773
911
|
const solverCode = getPowSolverCode();
|
|
912
|
+
const safePath = sanitizeRedirectPath(path);
|
|
774
913
|
return `
|
|
775
914
|
<html>
|
|
776
915
|
<head><title>Advanced Security Check (Level 3)</title></head>
|
|
@@ -781,14 +920,14 @@ const generateTspChallenge = (
|
|
|
781
920
|
<script>${solverCode}</script>
|
|
782
921
|
<script>
|
|
783
922
|
const cities = ${citiesJson}; // Safe, as it's JSON
|
|
784
|
-
const nonce = ${
|
|
923
|
+
const nonce = ${safeJsonStringify(nonce)}; // Safe
|
|
785
924
|
const targetMaxDistance = ${targetMaxDistance};
|
|
786
925
|
|
|
787
926
|
async function solve() {
|
|
788
927
|
const result = await window.solveTspChallenge(cities, targetMaxDistance);
|
|
789
928
|
|
|
790
929
|
if (result.distance <= targetMaxDistance) {
|
|
791
|
-
window.location.href = ${
|
|
930
|
+
window.location.href = ${safeJsonStringify(safePath)} + "?pow_type=tsp&pow_nonce=" + nonce + "&pow_solution=" + JSON.stringify(result.path);
|
|
792
931
|
} else {
|
|
793
932
|
document.getElementById('loader').innerText = "Error: Could not find a sufficient solution. Please try again.";
|
|
794
933
|
}
|
|
@@ -885,6 +1024,7 @@ const generateMemoryPoWChallenge = (
|
|
|
885
1024
|
difficulty = 16,
|
|
886
1025
|
path = "",
|
|
887
1026
|
) => {
|
|
1027
|
+
const safePath = sanitizeRedirectPath(path);
|
|
888
1028
|
// difficulty here is the buffer size in MB.
|
|
889
1029
|
return `
|
|
890
1030
|
<html>
|
|
@@ -895,8 +1035,8 @@ const generateMemoryPoWChallenge = (
|
|
|
895
1035
|
<div id="loader" style="margin:20px;">⚙️ Performing memory allocation and calculation... (${difficulty} MB)</div>
|
|
896
1036
|
<script>
|
|
897
1037
|
async function solve() {
|
|
898
|
-
const nonce =
|
|
899
|
-
|
|
1038
|
+
const nonce = ${safeJsonStringify(nonce)};
|
|
1039
|
+
const size = ${difficulty} * 1024 * 1024; // en octets
|
|
900
1040
|
const iterations = size / 16;
|
|
901
1041
|
|
|
902
1042
|
try {
|
|
@@ -913,8 +1053,8 @@ const generateMemoryPoWChallenge = (
|
|
|
913
1053
|
}
|
|
914
1054
|
window.location.href = "${path}" + "?pow_type=mem&pow_nonce=" + nonce + "&pow_solution=" + finalHash;
|
|
915
1055
|
} catch(e) {
|
|
916
|
-
|
|
917
|
-
|
|
1056
|
+
window.location.href = ${safeJsonStringify(safePath)} + "?pow_type=mem&pow_nonce=" + nonce + "&pow_solution=" + finalHash;
|
|
1057
|
+
}
|
|
918
1058
|
}
|
|
919
1059
|
solve();
|
|
920
1060
|
</script>
|
|
@@ -969,40 +1109,123 @@ export const verifyPoWAndGenerateTicket = async (
|
|
|
969
1109
|
* For higher difficulties (production workloads), we skip the massive memory allocation on the server,
|
|
970
1110
|
* avoiding server-side memory DoS vectors completely.
|
|
971
1111
|
*/
|
|
1112
|
+
function getChallengedIndices(seed, solution, numBlocks, k = 4) {
|
|
1113
|
+
const indices = [];
|
|
1114
|
+
let h = cyrb53(seed + ":" + solution);
|
|
1115
|
+
for (let i = 0; i < k; i++) {
|
|
1116
|
+
h = Math.imul(h ^ i, 1597334677);
|
|
1117
|
+
indices.push(Math.abs(h) % numBlocks);
|
|
1118
|
+
}
|
|
1119
|
+
return indices;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function verifyMerkleProof(leafHash, index, proof, root) {
|
|
1123
|
+
let currentHash = leafHash;
|
|
1124
|
+
let idx = index;
|
|
1125
|
+
for (let i = 0; i < proof.length; i++) {
|
|
1126
|
+
const sibling = proof[i];
|
|
1127
|
+
const combined = idx % 2 === 0 ? currentHash + sibling : sibling + currentHash;
|
|
1128
|
+
currentHash = crypto.createHash('sha256').update(Buffer.from(combined, 'hex')).digest('hex');
|
|
1129
|
+
idx = Math.floor(idx / 2);
|
|
1130
|
+
}
|
|
1131
|
+
return currentHash === root;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function verifyMemoryPoWLegacy(nonce, solution, difficulty, clientSecret) {
|
|
1135
|
+
const size = difficulty * 1024 * 1024;
|
|
1136
|
+
const iterations = size / 16;
|
|
1137
|
+
const buffer = new Uint32Array(size / 4);
|
|
1138
|
+
const seed = `:${nonce}:${clientSecret}`;
|
|
1139
|
+
let h = new TextEncoder().encode(seed).reduce((acc, v) => acc + v, 0);
|
|
1140
|
+
|
|
1141
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
1142
|
+
buffer[i] = h = Math.imul(h ^ i, 1597334677);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
let finalHash = 0;
|
|
1146
|
+
let addr = buffer.length > 0 ? buffer[0] % buffer.length : 0;
|
|
1147
|
+
for (let i = 0; i < iterations; i++) {
|
|
1148
|
+
addr = buffer[addr] % buffer.length;
|
|
1149
|
+
finalHash ^= addr;
|
|
1150
|
+
}
|
|
1151
|
+
return finalHash === solution;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
972
1154
|
export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret = '') => {
|
|
973
1155
|
const MAX_ALLOWED_MEM_DIFFICULTY = 128; // 128MB
|
|
974
1156
|
if (difficulty > MAX_ALLOWED_MEM_DIFFICULTY) {
|
|
975
1157
|
console.warn(`[Security] Memory PoW verification attempt with excessive difficulty: ${difficulty}MB. Denied.`);
|
|
976
1158
|
return false;
|
|
977
1159
|
}
|
|
1160
|
+
if (Number(difficulty) === 0) {
|
|
1161
|
+
return true;
|
|
1162
|
+
}
|
|
978
1163
|
if (!solution) {
|
|
979
1164
|
return false;
|
|
980
1165
|
}
|
|
981
1166
|
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1167
|
+
let data;
|
|
1168
|
+
try {
|
|
1169
|
+
data = typeof solution === 'string' ? JSON.parse(solution) : solution;
|
|
1170
|
+
} catch (e) {
|
|
1171
|
+
data = null;
|
|
986
1172
|
}
|
|
987
1173
|
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
1174
|
+
if (!data || typeof data !== 'object' || data.solution === undefined || !data.merkleRoot || !data.proofs) {
|
|
1175
|
+
if (difficulty <= 4 && /^\d+$/.test(String(solution))) {
|
|
1176
|
+
return verifyMemoryPoWLegacy(nonce, parseInt(solution, 10), difficulty, clientSecret);
|
|
1177
|
+
}
|
|
1178
|
+
return false;
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
const { solution: sol, merkleRoot, proofs } = data;
|
|
1182
|
+
const numBlocks = difficulty * 256;
|
|
992
1183
|
const seed = `:${nonce}:${clientSecret}`;
|
|
993
|
-
let h = new TextEncoder().encode(seed).reduce((acc, v) => acc + v, 0);
|
|
994
1184
|
|
|
995
|
-
|
|
996
|
-
|
|
1185
|
+
const challengedIndices = getChallengedIndices(seed, sol, numBlocks, 4);
|
|
1186
|
+
|
|
1187
|
+
for (const b of challengedIndices) {
|
|
1188
|
+
const proof = proofs[b] || proofs[String(b)];
|
|
1189
|
+
if (!proof) return false;
|
|
1190
|
+
|
|
1191
|
+
const block = new Uint32Array(1024);
|
|
1192
|
+
let h = cyrb53(seed + ":" + b);
|
|
1193
|
+
for (let i = 0; i < 1024; i++) {
|
|
1194
|
+
block[i] = (h = Math.imul(h ^ i, 1597334677));
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const expectedLeaf = crypto.createHash('sha256').update(Buffer.from(block.buffer)).digest('hex');
|
|
1198
|
+
|
|
1199
|
+
if (!verifyMerkleProof(expectedLeaf, b, proof, merkleRoot)) {
|
|
1200
|
+
return false;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
const blockCache = new Map();
|
|
1205
|
+
function getBlockElement(blockIdx, elementIdx) {
|
|
1206
|
+
if (!blockCache.has(blockIdx)) {
|
|
1207
|
+
const block = new Uint32Array(1024);
|
|
1208
|
+
let h = cyrb53(seed + ":" + blockIdx);
|
|
1209
|
+
for (let i = 0; i < 1024; i++) {
|
|
1210
|
+
block[i] = (h = Math.imul(h ^ i, 1597334677));
|
|
1211
|
+
}
|
|
1212
|
+
blockCache.set(blockIdx, block);
|
|
1213
|
+
}
|
|
1214
|
+
return blockCache.get(blockIdx)[elementIdx];
|
|
997
1215
|
}
|
|
998
1216
|
|
|
999
|
-
|
|
1000
|
-
let addr =
|
|
1217
|
+
const totalElements = numBlocks * 1024;
|
|
1218
|
+
let addr = totalElements > 0 ? getBlockElement(0, 0) % totalElements : 0;
|
|
1219
|
+
let expectedSolution = 0;
|
|
1220
|
+
const iterations = 1024;
|
|
1001
1221
|
for (let i = 0; i < iterations; i++) {
|
|
1002
|
-
|
|
1003
|
-
|
|
1222
|
+
const blockIdx = Math.floor(addr / 1024);
|
|
1223
|
+
const elementIdx = addr % 1024;
|
|
1224
|
+
addr = getBlockElement(blockIdx, elementIdx) % totalElements;
|
|
1225
|
+
expectedSolution ^= addr;
|
|
1004
1226
|
}
|
|
1005
|
-
|
|
1227
|
+
|
|
1228
|
+
return expectedSolution === parseInt(sol, 10);
|
|
1006
1229
|
};
|
|
1007
1230
|
|
|
1008
1231
|
export async function verifySpacePoW(nonce, solution, queries, seed, clientSecret) {
|
|
@@ -1234,12 +1457,13 @@ export async function generateSpaceChallenge(clientIp, nonce, suspicionFactor, o
|
|
|
1234
1457
|
function generateSpaceChallengePage(challengeDetails, clientSecret, securityConfig) {
|
|
1235
1458
|
const { nonce, sizeMb, queries, path } = challengeDetails;
|
|
1236
1459
|
const solverCode = getPowSolverCode();
|
|
1237
|
-
|
|
1238
|
-
|
|
1460
|
+
const safePath = sanitizeRedirectPath(path);
|
|
1461
|
+
|
|
1462
|
+
const challengeScript = `
|
|
1239
1463
|
async function solve() {
|
|
1240
|
-
const nonce = ${
|
|
1241
|
-
|
|
1242
|
-
|
|
1464
|
+
const nonce = ${safeJsonStringify(nonce)};
|
|
1465
|
+
const path = ${safeJsonStringify(safePath)};
|
|
1466
|
+
const clientSecret = ${safeJsonStringify(clientSecret)};
|
|
1243
1467
|
const queries = ${JSON.stringify(queries)};
|
|
1244
1468
|
const sizeMb = ${sizeMb};
|
|
1245
1469
|
|
|
@@ -1600,6 +1824,41 @@ function getBehaviorScore(context) {
|
|
|
1600
1824
|
if (metrics.keystrokeLatency > 0 && metrics.keystrokeLatency < 40) score += 25; // Frappe trop rapide pour un humain.
|
|
1601
1825
|
if (metrics.keystrokeLatency > 1000) score += 15; // Latence très élevée, peut être un script lent.
|
|
1602
1826
|
|
|
1827
|
+
// NOUVEAU: Analyse de digraphie/trigraphie (dwell & flight times)
|
|
1828
|
+
const dwellTimes = metrics.keystrokeDwellTimes || [];
|
|
1829
|
+
const flightTimes = metrics.keystrokeFlightTimes || [];
|
|
1830
|
+
|
|
1831
|
+
if (dwellTimes.length >= 5) {
|
|
1832
|
+
const meanDwell = dwellTimes.reduce((a, b) => a + b, 0) / dwellTimes.length;
|
|
1833
|
+
const varDwell = dwellTimes.reduce((a, b) => a + Math.pow(b - meanDwell, 2), 0) / dwellTimes.length;
|
|
1834
|
+
const stdDevDwell = Math.sqrt(varDwell);
|
|
1835
|
+
|
|
1836
|
+
if (stdDevDwell < 2.0) {
|
|
1837
|
+
score += 35; // Suspicion d'automatisation (pas de variation humaine de pression)
|
|
1838
|
+
}
|
|
1839
|
+
if (meanDwell < 15.0) {
|
|
1840
|
+
score += 25; // Dwell time irréaliste
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
|
|
1844
|
+
if (flightTimes.length >= 5) {
|
|
1845
|
+
const times = flightTimes.map(f => f.time);
|
|
1846
|
+
const meanFlight = times.reduce((a, b) => a + b, 0) / times.length;
|
|
1847
|
+
const varFlight = times.reduce((a, b) => a + Math.pow(b - meanFlight, 2), 0) / times.length;
|
|
1848
|
+
const stdDevFlight = Math.sqrt(varFlight);
|
|
1849
|
+
|
|
1850
|
+
if (stdDevFlight < 3.0) {
|
|
1851
|
+
score += 35; // Pas de variation de transition (flight time robotique)
|
|
1852
|
+
}
|
|
1853
|
+
if (meanFlight < 25.0) {
|
|
1854
|
+
score += 25; // Transitions trop rapides
|
|
1855
|
+
}
|
|
1856
|
+
const benfordDev = Optimization.Operators.benfordTest(times);
|
|
1857
|
+
if (benfordDev > 0.18) {
|
|
1858
|
+
score += 30; // Les intervalles ne suivent pas la loi de Benford
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1603
1862
|
// 4. Analyse de la distribution avec la loi de Benford (si les valeurs sont non nulles).
|
|
1604
1863
|
if (segments.length > 10) {
|
|
1605
1864
|
const benfordDeviation = Optimization.Operators.benfordTest(segments);
|
|
@@ -2766,8 +3025,9 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
2766
3025
|
const stableFp = extractStablePart(currentDeviceHash);
|
|
2767
3026
|
const stableFpHash = cyrb53(stableFp).toString();
|
|
2768
3027
|
const { botnetClusterScore } = await getBotnetClusterScore(context, stableFpHash);
|
|
2769
|
-
|
|
2770
|
-
|
|
3028
|
+
const { tcpAnomalyScore } = getTcpAnomalyScore(context);
|
|
3029
|
+
const { quicAnomalyScore } = getQuicAnomalyScore(context);
|
|
3030
|
+
const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
|
|
2771
3031
|
|
|
2772
3032
|
// Save the updated device state to the store
|
|
2773
3033
|
// 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.
|
|
@@ -2779,7 +3039,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
2779
3039
|
deviceData.ips = new Set(deviceData.ips);
|
|
2780
3040
|
}
|
|
2781
3041
|
// Le vecteur de suspicion est maintenant complet.
|
|
2782
|
-
return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore };
|
|
3042
|
+
return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, quicAnomalyScore, renderingAnomalyScore };
|
|
2783
3043
|
};
|
|
2784
3044
|
|
|
2785
3045
|
// A residential user can change networks (home, 4G, public wifi).
|
|
@@ -2848,8 +3108,8 @@ function calculateTarget(suspicionFactor, securityConfig = {}) {
|
|
|
2848
3108
|
// MAX_DIFFICULTY: Slow enough to heavily penalize a bot, but feasible for a patient human (5-30s).
|
|
2849
3109
|
// NOUVEAU: La difficulté est maintenant configurable.
|
|
2850
3110
|
const { cpu: cpuConfig = {} } = securityConfig;
|
|
2851
|
-
|
|
2852
|
-
|
|
3111
|
+
const MIN_DIFFICULTY_BITS = cpuConfig.minDifficultyBits ?? 8;
|
|
3112
|
+
const MAX_DIFFICULTY_BITS = cpuConfig.maxDifficultyBits ?? 22;
|
|
2853
3113
|
|
|
2854
3114
|
// Use linear interpolation between min and max difficulty.
|
|
2855
3115
|
const totalDifficultyBits =
|
|
@@ -2903,6 +3163,8 @@ export function generateCpuTargetChallenge(
|
|
|
2903
3163
|
};
|
|
2904
3164
|
}
|
|
2905
3165
|
|
|
3166
|
+
const htmlTemplateCache = new Map();
|
|
3167
|
+
|
|
2906
3168
|
/**
|
|
2907
3169
|
* Generates the HTML page for the CPU target challenge.
|
|
2908
3170
|
* @param {object} challengeDetails - The details from generateCpuTargetChallenge.
|
|
@@ -2911,6 +3173,7 @@ export function generateCpuTargetChallenge(
|
|
|
2911
3173
|
*/
|
|
2912
3174
|
function generateCpuTargetChallengePage(challengeDetails, clientIp) {
|
|
2913
3175
|
const { nonce, target, path } = challengeDetails;
|
|
3176
|
+
const safePath = sanitizeRedirectPath(path);
|
|
2914
3177
|
const solverCode = getPowSolverCode();
|
|
2915
3178
|
return `
|
|
2916
3179
|
<html><head><title>Security Check</title></head>
|
|
@@ -2922,14 +3185,14 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
|
|
|
2922
3185
|
<script>
|
|
2923
3186
|
async function solve() {
|
|
2924
3187
|
const clientIp = ${JSON.stringify(clientIp)};
|
|
2925
|
-
const nonce = ${
|
|
3188
|
+
const nonce = ${safeJsonStringify(nonce)};
|
|
2926
3189
|
const cpuTarget = BigInt("0x" + "${target}");
|
|
2927
3190
|
// La nouvelle version de solveCpuChallengeInline n'a plus besoin de l'IP ou du secret,
|
|
2928
3191
|
// car tout est dans le baseBlock. Pour la compatibilité de ce challenge simple, on passe null.
|
|
2929
3192
|
const baseBlockBytes = new TextEncoder().encode(nonce + ":");
|
|
2930
3193
|
const solution = await window.solveCpuChallengeInline(baseBlockBytes, cpuTarget, (progress) => {});
|
|
2931
3194
|
window.location.href = ${JSON.stringify(path)} + "?pow_type=cpu_target&pow_nonce=" + nonce + "&pow_solution=" + solution;
|
|
2932
|
-
|
|
3195
|
+
}
|
|
2933
3196
|
solve();
|
|
2934
3197
|
</script>
|
|
2935
3198
|
</body></html>`;
|
|
@@ -2944,6 +3207,7 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
|
|
|
2944
3207
|
*/
|
|
2945
3208
|
function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig, trapUrls, originalFingerprint) { // eslint-disable-line max-len
|
|
2946
3209
|
const { nonce, target, path } = cpuChallengeDetails;
|
|
3210
|
+
const safePath = sanitizeRedirectPath(path);
|
|
2947
3211
|
const solverCode = getPowSolverCode();
|
|
2948
3212
|
// On prépare le baseBlock pour le client. Il sera envoyé sous forme de tableau d'octets.
|
|
2949
3213
|
// Le fingerprint est maintenant passé directement en paramètre.
|
|
@@ -2960,10 +3224,10 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
2960
3224
|
|
|
2961
3225
|
const challengeScript = `
|
|
2962
3226
|
async function solve() {
|
|
2963
|
-
const nonce = ${
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
3227
|
+
const nonce = ${safeJsonStringify(nonce)};
|
|
3228
|
+
const path = ${JSON.stringify(path)};
|
|
3229
|
+
const clientSecret = ${safeJsonStringify(clientSecret)};
|
|
3230
|
+
const clientIp = ${JSON.stringify(clientIp)};
|
|
2967
3231
|
const cpuTarget = BigInt("0x" + "${target}");
|
|
2968
3232
|
const memDifficulty = ${memoryDifficulty};
|
|
2969
3233
|
// Le client reçoit directement le 'baseBlock' sous forme de tableau d'octets.
|
|
@@ -3002,10 +3266,15 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
3002
3266
|
const customTemplatePath = securityConfig?.challengePagePath;
|
|
3003
3267
|
|
|
3004
3268
|
if (customTemplatePath) {
|
|
3005
|
-
|
|
3006
|
-
htmlTemplate =
|
|
3007
|
-
}
|
|
3008
|
-
|
|
3269
|
+
if (htmlTemplateCache.has(customTemplatePath)) {
|
|
3270
|
+
htmlTemplate = htmlTemplateCache.get(customTemplatePath);
|
|
3271
|
+
} else {
|
|
3272
|
+
try {
|
|
3273
|
+
htmlTemplate = readFileSync(customTemplatePath, 'utf-8');
|
|
3274
|
+
htmlTemplateCache.set(customTemplatePath, htmlTemplate);
|
|
3275
|
+
} catch (error) {
|
|
3276
|
+
console.warn(`[Fingerprint] Could not load custom challenge page at '${customTemplatePath}'. Falling back to default. Error: ${error.message}`);
|
|
3277
|
+
}
|
|
3009
3278
|
}
|
|
3010
3279
|
}
|
|
3011
3280
|
|
|
@@ -3144,6 +3413,18 @@ export class FingerprintEngine {
|
|
|
3144
3413
|
this.dryRun = finalConfig.dryRun || false;
|
|
3145
3414
|
}
|
|
3146
3415
|
|
|
3416
|
+
/**
|
|
3417
|
+
* Applique à chaud une nouvelle configuration de sécurité (poids, seuils, etc.)
|
|
3418
|
+
* sans nécessiter de redémarrage.
|
|
3419
|
+
* @param {object} newConfig - La nouvelle configuration partielle ou complète.
|
|
3420
|
+
*/
|
|
3421
|
+
updateConfig(newConfig) {
|
|
3422
|
+
this._validateConfig(newConfig);
|
|
3423
|
+
this.securityConfig = deepMerge(this.securityConfig, newConfig);
|
|
3424
|
+
this.dryRun = this.securityConfig.dryRun || false;
|
|
3425
|
+
this._log('Configuration mise à jour à chaud (Hot-Reloaded)', this.securityConfig);
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3147
3428
|
/**
|
|
3148
3429
|
* Validates the security configuration object to detect potential typos or missing essential keys.
|
|
3149
3430
|
* @private
|
|
@@ -3208,7 +3489,9 @@ export class FingerprintEngine {
|
|
|
3208
3489
|
(suspicionVector.clientHintsInconsistencyScore || 0) * (weights.clientHintsInconsistencyScore || 0) +
|
|
3209
3490
|
(suspicionVector.subnetScore || 0) * (weights.subnetScore || 0) +
|
|
3210
3491
|
(suspicionVector.ipReputationScore || 0) * (weights.ipReputationScore || 0) +
|
|
3211
|
-
(suspicionVector.tcpAnomalyScore || 0) * (weights.tcpAnomalyScore || 0)
|
|
3492
|
+
(suspicionVector.tcpAnomalyScore || 0) * (weights.tcpAnomalyScore || 0) +
|
|
3493
|
+
(suspicionVector.quicAnomalyScore || 0) * (weights.quicAnomalyScore || 0) + // NOUVEAU: QUIC Anomaly
|
|
3494
|
+
(suspicionVector.renderingAnomalyScore || 0) * (weights.renderingAnomalyScore || 0); // NOUVEAU: Rendering Anomaly
|
|
3212
3495
|
|
|
3213
3496
|
return Math.min(100, score);
|
|
3214
3497
|
}
|
|
@@ -3652,8 +3935,9 @@ export class FingerprintEngine {
|
|
|
3652
3935
|
// lors de l'émission du challenge.
|
|
3653
3936
|
// --- FIX: Use submitted fingerprint, but fallback to current request's fingerprint ---
|
|
3654
3937
|
// This handles API clients that might not use the full client-side library but still solve the challenge.
|
|
3655
|
-
|
|
3656
|
-
|
|
3938
|
+
const safe_pow_fp = typeof pow_fp === 'string' ? pow_fp : (Array.isArray(pow_fp) ? String(pow_fp[0]) : '');
|
|
3939
|
+
const solverFingerprint = safe_pow_fp || getCompositeDeviceHash(requestContext);
|
|
3940
|
+
const originalFingerprint = typeof challengeContext.fingerprint === 'string' ? challengeContext.fingerprint : '';
|
|
3657
3941
|
|
|
3658
3942
|
let similarity;
|
|
3659
3943
|
const similarityThreshold = this.securityConfig.similarityThreshold ?? 0.95;
|
|
@@ -4115,9 +4399,8 @@ export class FingerprintEngine {
|
|
|
4115
4399
|
// On passe la configuration pour que la difficulté soit calculée correctement.
|
|
4116
4400
|
const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path, this.securityConfig);
|
|
4117
4401
|
|
|
4118
|
-
// La difficulté mémoire
|
|
4119
|
-
|
|
4120
|
-
const memActivationFactor = Math.max(0, (suspicionFactor - 0.25) / 0.75);
|
|
4402
|
+
// La difficulté mémoire augmente désormais en parfaite synergie avec le facteur de suspicion (ratio constant)
|
|
4403
|
+
const memActivationFactor = suspicionFactor;
|
|
4121
4404
|
|
|
4122
4405
|
const minMemDifficulty = 0; // Peut être 0 Mo !
|
|
4123
4406
|
const maxMemDifficulty = 48; // 48Mo pour les plus suspects
|
|
@@ -4846,6 +5129,9 @@ export const modsecurity_analyzer = (rulesPath) => {
|
|
|
4846
5129
|
* @returns {Array<{userAgent: string, hostnameSuffix: string}>}
|
|
4847
5130
|
*/
|
|
4848
5131
|
export const default_whitelist = () => [
|
|
5132
|
+
googlebot_whitelist(),
|
|
5133
|
+
bingbot_whitelist(),
|
|
5134
|
+
yandex_whitelist(),
|
|
4849
5135
|
// === Moteurs de recherche majeurs ===
|
|
4850
5136
|
{ userAgent: 'Googlebot', hostnameSuffix: '.googlebot.com' },
|
|
4851
5137
|
{ userAgent: 'Google-Extended', hostnameSuffix: '.google.com' },
|
|
@@ -4943,6 +5229,26 @@ export const default_whitelist = () => [
|
|
|
4943
5229
|
{ userAgent: 'KeyCDN', hostnameSuffix: '.keycdn.com' },
|
|
4944
5230
|
];
|
|
4945
5231
|
|
|
5232
|
+
/**
|
|
5233
|
+
* Retourne la liste officielle des préfixes IP/CIDR (IPv4 et IPv6) utilisés par Googlebot
|
|
5234
|
+
* enveloppée dans un objet de type 'allowlist' prêt à être injecté.
|
|
5235
|
+
* @returns {{type: string, entries: string[]}} Règle d'allowlist de sécurité.
|
|
5236
|
+
*/
|
|
5237
|
+
export const googlebot_whitelist = () => ({
|
|
5238
|
+
type: 'allowlist',
|
|
5239
|
+
entries: googlebotEntries
|
|
5240
|
+
});
|
|
5241
|
+
|
|
5242
|
+
export const yandex_whitelist = () => ({
|
|
5243
|
+
type: 'allowlist',
|
|
5244
|
+
entries: yandexEntries
|
|
5245
|
+
});
|
|
5246
|
+
|
|
5247
|
+
export const bingbot_whitelist = () => ({
|
|
5248
|
+
type: 'allowlist',
|
|
5249
|
+
entries: bingbotEntries
|
|
5250
|
+
});
|
|
5251
|
+
|
|
4946
5252
|
|
|
4947
5253
|
/**
|
|
4948
5254
|
* Extracts the TLS Session ID or ticket hash from the request context.
|
|
@@ -4972,6 +5278,8 @@ function getTlsSessionId(context) {
|
|
|
4972
5278
|
}
|
|
4973
5279
|
|
|
4974
5280
|
// --- Proof-of-Work Middleware (The Tollbooth) ---
|
|
5281
|
+
const staticFileCache = new Map();
|
|
5282
|
+
|
|
4975
5283
|
export const powMiddleware = (securityConfig) => {
|
|
4976
5284
|
const engine = new FingerprintEngine(securityConfig);
|
|
4977
5285
|
|
|
@@ -5052,7 +5360,11 @@ export const powMiddleware = (securityConfig) => {
|
|
|
5052
5360
|
}
|
|
5053
5361
|
}
|
|
5054
5362
|
if (jsFile && existsSync(jsFile)) {
|
|
5055
|
-
|
|
5363
|
+
let fileContent = staticFileCache.get(jsFile);
|
|
5364
|
+
if (!fileContent) {
|
|
5365
|
+
fileContent = readFileSync(jsFile);
|
|
5366
|
+
staticFileCache.set(jsFile, fileContent);
|
|
5367
|
+
}
|
|
5056
5368
|
res.setHeader('Content-Type', 'application/javascript');
|
|
5057
5369
|
return res.send(fileContent);
|
|
5058
5370
|
}
|
|
@@ -5069,7 +5381,11 @@ export const powMiddleware = (securityConfig) => {
|
|
|
5069
5381
|
}
|
|
5070
5382
|
}
|
|
5071
5383
|
if (wasmFile && existsSync(wasmFile)) {
|
|
5072
|
-
|
|
5384
|
+
let fileContent = staticFileCache.get(wasmFile);
|
|
5385
|
+
if (!fileContent) {
|
|
5386
|
+
fileContent = readFileSync(wasmFile);
|
|
5387
|
+
staticFileCache.set(wasmFile, fileContent);
|
|
5388
|
+
}
|
|
5073
5389
|
res.setHeader('Content-Type', 'application/wasm');
|
|
5074
5390
|
return res.send(fileContent);
|
|
5075
5391
|
}
|
|
@@ -5079,7 +5395,7 @@ export const powMiddleware = (securityConfig) => {
|
|
|
5079
5395
|
|
|
5080
5396
|
const requestContext = {
|
|
5081
5397
|
clientIp: req.ip || req.socket?.remoteAddress || "unknown",
|
|
5082
|
-
|
|
5398
|
+
path: sanitizeRedirectPath(req.path),
|
|
5083
5399
|
cookies: req.cookies,
|
|
5084
5400
|
query: req.query,
|
|
5085
5401
|
body: req.body,
|
|
@@ -5132,7 +5448,7 @@ export const powMiddleware = (securityConfig) => {
|
|
|
5132
5448
|
if (decision.cookie) {
|
|
5133
5449
|
res.cookie(decision.cookie.name, decision.cookie.value, decision.cookie.options);
|
|
5134
5450
|
}
|
|
5135
|
-
return res.redirect(decision.path);
|
|
5451
|
+
return res.redirect(sanitizeRedirectPath(decision.path));
|
|
5136
5452
|
|
|
5137
5453
|
case 'next':
|
|
5138
5454
|
default:
|
|
@@ -5141,6 +5457,7 @@ export const powMiddleware = (securityConfig) => {
|
|
|
5141
5457
|
};
|
|
5142
5458
|
};
|
|
5143
5459
|
|
|
5460
|
+
|
|
5144
5461
|
/**
|
|
5145
5462
|
* @internal
|
|
5146
5463
|
* Exporting an object containing the functions to make them mockable in tests.
|
|
@@ -5186,6 +5503,8 @@ export const __internal = {
|
|
|
5186
5503
|
parseTcpSyn, // Expose for testing
|
|
5187
5504
|
classifyTcpOs, // Expose for testing
|
|
5188
5505
|
getTcpAnomalyScore, // Expose for testing,
|
|
5506
|
+
getQuicAnomalyScore, // NOUVEAU: Expose pour les tests
|
|
5507
|
+
getRenderingAnomalyScore, // NOUVEAU: Expose pour les tests
|
|
5189
5508
|
registerCooperativeNode,
|
|
5190
5509
|
findPeerInSubnet,
|
|
5191
5510
|
handleCooperativeRequest
|
|
@@ -5584,7 +5903,7 @@ export async function handleMetricsRequest(req, res, securityConfig) {
|
|
|
5584
5903
|
if (typeof authorizationCallback === 'function') {
|
|
5585
5904
|
const context = new RequestContext(
|
|
5586
5905
|
req.ip,
|
|
5587
|
-
req.path,
|
|
5906
|
+
sanitizeRedirectPath(req.path),
|
|
5588
5907
|
req.headers,
|
|
5589
5908
|
req.query,
|
|
5590
5909
|
req.body,
|