@anonympins/fingerprint 0.5.1 → 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.
@@ -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 { verifyZkpProof, decodePolymorphicFingerprint, deepMerge, getHeaderSignature, parseJa3, modPow, hashNetwork, normalizeReferer, isPrivateIp, parseUserAgent } from "./fingerprint.utils.js";
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')}`;
@@ -584,15 +633,17 @@ const getPowSecret = () => {
584
633
  return secret || "fallback-dev-secret-32-chars-minimum";
585
634
  };
586
635
 
636
+ let cachedPowSolverCode = null;
587
637
  /**
588
638
  * Loads the pow.solver.js content for inlining in HTML pages.
589
639
  * @returns {string} The solver JavaScript code.
590
640
  */
591
641
  const getPowSolverCode = () => {
592
- // On supprime le try/catch. Si le fichier n'est pas trouvé, le processus plantera,
593
- // ce qui est préférable à servir un code de secours potentiellement désynchronisé.
594
- const solverPath = join(__dirname, 'pow.solver.inline.js'); // Utilise la version inline
595
- return readFileSync(solverPath, 'utf-8');
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;
596
647
  };
597
648
  /**
598
649
  * Extracts the "stable" part of a fingerprint string.
@@ -858,6 +909,7 @@ const generateTspChallenge = (
858
909
  ) => {
859
910
  const citiesJson = JSON.stringify(cities);
860
911
  const solverCode = getPowSolverCode();
912
+ const safePath = sanitizeRedirectPath(path);
861
913
  return `
862
914
  <html>
863
915
  <head><title>Advanced Security Check (Level 3)</title></head>
@@ -868,14 +920,14 @@ const generateTspChallenge = (
868
920
  <script>${solverCode}</script>
869
921
  <script>
870
922
  const cities = ${citiesJson}; // Safe, as it's JSON
871
- const nonce = ${JSON.stringify(nonce)}; // Safe
923
+ const nonce = ${safeJsonStringify(nonce)}; // Safe
872
924
  const targetMaxDistance = ${targetMaxDistance};
873
925
 
874
926
  async function solve() {
875
927
  const result = await window.solveTspChallenge(cities, targetMaxDistance);
876
928
 
877
929
  if (result.distance <= targetMaxDistance) {
878
- window.location.href = ${JSON.stringify(path)} + "?pow_type=tsp&pow_nonce=" + nonce + "&pow_solution=" + JSON.stringify(result.path);
930
+ window.location.href = ${safeJsonStringify(safePath)} + "?pow_type=tsp&pow_nonce=" + nonce + "&pow_solution=" + JSON.stringify(result.path);
879
931
  } else {
880
932
  document.getElementById('loader').innerText = "Error: Could not find a sufficient solution. Please try again.";
881
933
  }
@@ -972,6 +1024,7 @@ const generateMemoryPoWChallenge = (
972
1024
  difficulty = 16,
973
1025
  path = "",
974
1026
  ) => {
1027
+ const safePath = sanitizeRedirectPath(path);
975
1028
  // difficulty here is the buffer size in MB.
976
1029
  return `
977
1030
  <html>
@@ -982,8 +1035,8 @@ const generateMemoryPoWChallenge = (
982
1035
  <div id="loader" style="margin:20px;">⚙️ Performing memory allocation and calculation... (${difficulty} MB)</div>
983
1036
  <script>
984
1037
  async function solve() {
985
- const nonce = "${nonce}";
986
- const size = ${difficulty} * 1024 * 1024; // en octets
1038
+ const nonce = ${safeJsonStringify(nonce)};
1039
+ const size = ${difficulty} * 1024 * 1024; // en octets
987
1040
  const iterations = size / 16;
988
1041
 
989
1042
  try {
@@ -1000,8 +1053,8 @@ const generateMemoryPoWChallenge = (
1000
1053
  }
1001
1054
  window.location.href = "${path}" + "?pow_type=mem&pow_nonce=" + nonce + "&pow_solution=" + finalHash;
1002
1055
  } catch(e) {
1003
- document.getElementById('loader').innerText = "Error: Insufficient memory. Please refresh.";
1004
- }
1056
+ window.location.href = ${safeJsonStringify(safePath)} + "?pow_type=mem&pow_nonce=" + nonce + "&pow_solution=" + finalHash;
1057
+ }
1005
1058
  }
1006
1059
  solve();
1007
1060
  </script>
@@ -1056,40 +1109,123 @@ export const verifyPoWAndGenerateTicket = async (
1056
1109
  * For higher difficulties (production workloads), we skip the massive memory allocation on the server,
1057
1110
  * avoiding server-side memory DoS vectors completely.
1058
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
+
1059
1154
  export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret = '') => {
1060
1155
  const MAX_ALLOWED_MEM_DIFFICULTY = 128; // 128MB
1061
1156
  if (difficulty > MAX_ALLOWED_MEM_DIFFICULTY) {
1062
1157
  console.warn(`[Security] Memory PoW verification attempt with excessive difficulty: ${difficulty}MB. Denied.`);
1063
1158
  return false;
1064
1159
  }
1160
+ if (Number(difficulty) === 0) {
1161
+ return true;
1162
+ }
1065
1163
  if (!solution) {
1066
1164
  return false;
1067
1165
  }
1068
1166
 
1069
- // If difficulty is high (production workloads), we treat memory PoW purely as a client-side cost.
1070
- // Cryptographic integrity is already fully enforced by the chained CPU PoW verification.
1071
- if (difficulty > 4) {
1072
- return true;
1167
+ let data;
1168
+ try {
1169
+ data = typeof solution === 'string' ? JSON.parse(solution) : solution;
1170
+ } catch (e) {
1171
+ data = null;
1172
+ }
1173
+
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;
1073
1179
  }
1074
1180
 
1075
- // Fallback: Cryptographic verification path for low-difficulty challenges / unit tests
1076
- const size = difficulty * 1024 * 1024;
1077
- const iterations = size / 16;
1078
- const buffer = new Uint32Array(size / 4);
1181
+ const { solution: sol, merkleRoot, proofs } = data;
1182
+ const numBlocks = difficulty * 256;
1079
1183
  const seed = `:${nonce}:${clientSecret}`;
1080
- let h = new TextEncoder().encode(seed).reduce((acc, v) => acc + v, 0);
1081
1184
 
1082
- for (let i = 0; i < buffer.length; i++) {
1083
- buffer[i] = h = Math.imul(h ^ i, 1597334677);
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
+ }
1084
1202
  }
1085
1203
 
1086
- let finalHash = 0;
1087
- let addr = buffer.length > 0 ? buffer[0] % buffer.length : 0;
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];
1215
+ }
1216
+
1217
+ const totalElements = numBlocks * 1024;
1218
+ let addr = totalElements > 0 ? getBlockElement(0, 0) % totalElements : 0;
1219
+ let expectedSolution = 0;
1220
+ const iterations = 1024;
1088
1221
  for (let i = 0; i < iterations; i++) {
1089
- addr = buffer[addr] % buffer.length;
1090
- finalHash ^= addr;
1222
+ const blockIdx = Math.floor(addr / 1024);
1223
+ const elementIdx = addr % 1024;
1224
+ addr = getBlockElement(blockIdx, elementIdx) % totalElements;
1225
+ expectedSolution ^= addr;
1091
1226
  }
1092
- return finalHash === parseInt(solution, 10);
1227
+
1228
+ return expectedSolution === parseInt(sol, 10);
1093
1229
  };
1094
1230
 
1095
1231
  export async function verifySpacePoW(nonce, solution, queries, seed, clientSecret) {
@@ -1321,12 +1457,13 @@ export async function generateSpaceChallenge(clientIp, nonce, suspicionFactor, o
1321
1457
  function generateSpaceChallengePage(challengeDetails, clientSecret, securityConfig) {
1322
1458
  const { nonce, sizeMb, queries, path } = challengeDetails;
1323
1459
  const solverCode = getPowSolverCode();
1324
-
1325
- const challengeScript = `
1460
+ const safePath = sanitizeRedirectPath(path);
1461
+
1462
+ const challengeScript = `
1326
1463
  async function solve() {
1327
- const nonce = ${JSON.stringify(nonce)};
1328
- const path = ${JSON.stringify(path)};
1329
- const clientSecret = ${JSON.stringify(clientSecret)};
1464
+ const nonce = ${safeJsonStringify(nonce)};
1465
+ const path = ${safeJsonStringify(safePath)};
1466
+ const clientSecret = ${safeJsonStringify(clientSecret)};
1330
1467
  const queries = ${JSON.stringify(queries)};
1331
1468
  const sizeMb = ${sizeMb};
1332
1469
 
@@ -2971,8 +3108,8 @@ function calculateTarget(suspicionFactor, securityConfig = {}) {
2971
3108
  // MAX_DIFFICULTY: Slow enough to heavily penalize a bot, but feasible for a patient human (5-30s).
2972
3109
  // NOUVEAU: La difficulté est maintenant configurable.
2973
3110
  const { cpu: cpuConfig = {} } = securityConfig;
2974
- const MIN_DIFFICULTY_BITS = cpuConfig.minDifficultyBits ?? 8;
2975
- const MAX_DIFFICULTY_BITS = cpuConfig.maxDifficultyBits ?? 16;
3111
+ const MIN_DIFFICULTY_BITS = cpuConfig.minDifficultyBits ?? 8;
3112
+ const MAX_DIFFICULTY_BITS = cpuConfig.maxDifficultyBits ?? 22;
2976
3113
 
2977
3114
  // Use linear interpolation between min and max difficulty.
2978
3115
  const totalDifficultyBits =
@@ -3026,6 +3163,8 @@ export function generateCpuTargetChallenge(
3026
3163
  };
3027
3164
  }
3028
3165
 
3166
+ const htmlTemplateCache = new Map();
3167
+
3029
3168
  /**
3030
3169
  * Generates the HTML page for the CPU target challenge.
3031
3170
  * @param {object} challengeDetails - The details from generateCpuTargetChallenge.
@@ -3034,6 +3173,7 @@ export function generateCpuTargetChallenge(
3034
3173
  */
3035
3174
  function generateCpuTargetChallengePage(challengeDetails, clientIp) {
3036
3175
  const { nonce, target, path } = challengeDetails;
3176
+ const safePath = sanitizeRedirectPath(path);
3037
3177
  const solverCode = getPowSolverCode();
3038
3178
  return `
3039
3179
  <html><head><title>Security Check</title></head>
@@ -3045,14 +3185,14 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
3045
3185
  <script>
3046
3186
  async function solve() {
3047
3187
  const clientIp = ${JSON.stringify(clientIp)};
3048
- const nonce = ${JSON.stringify(nonce)};
3188
+ const nonce = ${safeJsonStringify(nonce)};
3049
3189
  const cpuTarget = BigInt("0x" + "${target}");
3050
3190
  // La nouvelle version de solveCpuChallengeInline n'a plus besoin de l'IP ou du secret,
3051
3191
  // car tout est dans le baseBlock. Pour la compatibilité de ce challenge simple, on passe null.
3052
3192
  const baseBlockBytes = new TextEncoder().encode(nonce + ":");
3053
3193
  const solution = await window.solveCpuChallengeInline(baseBlockBytes, cpuTarget, (progress) => {});
3054
3194
  window.location.href = ${JSON.stringify(path)} + "?pow_type=cpu_target&pow_nonce=" + nonce + "&pow_solution=" + solution;
3055
- }
3195
+ }
3056
3196
  solve();
3057
3197
  </script>
3058
3198
  </body></html>`;
@@ -3067,6 +3207,7 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
3067
3207
  */
3068
3208
  function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig, trapUrls, originalFingerprint) { // eslint-disable-line max-len
3069
3209
  const { nonce, target, path } = cpuChallengeDetails;
3210
+ const safePath = sanitizeRedirectPath(path);
3070
3211
  const solverCode = getPowSolverCode();
3071
3212
  // On prépare le baseBlock pour le client. Il sera envoyé sous forme de tableau d'octets.
3072
3213
  // Le fingerprint est maintenant passé directement en paramètre.
@@ -3083,10 +3224,10 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
3083
3224
 
3084
3225
  const challengeScript = `
3085
3226
  async function solve() {
3086
- const nonce = ${JSON.stringify(nonce)};
3087
- const path = ${JSON.stringify(path)};
3088
- const clientSecret = ${JSON.stringify(clientSecret)};
3089
- const clientIp = ${JSON.stringify(clientIp)};
3227
+ const nonce = ${safeJsonStringify(nonce)};
3228
+ const path = ${JSON.stringify(path)};
3229
+ const clientSecret = ${safeJsonStringify(clientSecret)};
3230
+ const clientIp = ${JSON.stringify(clientIp)};
3090
3231
  const cpuTarget = BigInt("0x" + "${target}");
3091
3232
  const memDifficulty = ${memoryDifficulty};
3092
3233
  // Le client reçoit directement le 'baseBlock' sous forme de tableau d'octets.
@@ -3125,10 +3266,15 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
3125
3266
  const customTemplatePath = securityConfig?.challengePagePath;
3126
3267
 
3127
3268
  if (customTemplatePath) {
3128
- try {
3129
- htmlTemplate = readFileSync(customTemplatePath, 'utf-8');
3130
- } catch (error) {
3131
- console.warn(`[Fingerprint] Could not load custom challenge page at '${customTemplatePath}'. Falling back to default. Error: ${error.message}`);
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
+ }
3132
3278
  }
3133
3279
  }
3134
3280
 
@@ -3789,8 +3935,9 @@ export class FingerprintEngine {
3789
3935
  // lors de l'émission du challenge.
3790
3936
  // --- FIX: Use submitted fingerprint, but fallback to current request's fingerprint ---
3791
3937
  // This handles API clients that might not use the full client-side library but still solve the challenge.
3792
- const solverFingerprint = pow_fp || getCompositeDeviceHash(requestContext);
3793
- const originalFingerprint = challengeContext.fingerprint; // This is the fingerprint of the request that *triggered* the challenge
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 : '';
3794
3941
 
3795
3942
  let similarity;
3796
3943
  const similarityThreshold = this.securityConfig.similarityThreshold ?? 0.95;
@@ -4252,9 +4399,8 @@ export class FingerprintEngine {
4252
4399
  // On passe la configuration pour que la difficulté soit calculée correctement.
4253
4400
  const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path, this.securityConfig);
4254
4401
 
4255
- // La difficulté mémoire démarre à 0 et augmente seulement après un certain seuil de suspicion.
4256
- // Par exemple, elle ne commence à augmenter qu'à partir de 25% du chemin entre 'low' et 'high'.
4257
- 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;
4258
4404
 
4259
4405
  const minMemDifficulty = 0; // Peut être 0 Mo !
4260
4406
  const maxMemDifficulty = 48; // 48Mo pour les plus suspects
@@ -4983,6 +5129,9 @@ export const modsecurity_analyzer = (rulesPath) => {
4983
5129
  * @returns {Array<{userAgent: string, hostnameSuffix: string}>}
4984
5130
  */
4985
5131
  export const default_whitelist = () => [
5132
+ googlebot_whitelist(),
5133
+ bingbot_whitelist(),
5134
+ yandex_whitelist(),
4986
5135
  // === Moteurs de recherche majeurs ===
4987
5136
  { userAgent: 'Googlebot', hostnameSuffix: '.googlebot.com' },
4988
5137
  { userAgent: 'Google-Extended', hostnameSuffix: '.google.com' },
@@ -5080,6 +5229,26 @@ export const default_whitelist = () => [
5080
5229
  { userAgent: 'KeyCDN', hostnameSuffix: '.keycdn.com' },
5081
5230
  ];
5082
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
+
5083
5252
 
5084
5253
  /**
5085
5254
  * Extracts the TLS Session ID or ticket hash from the request context.
@@ -5109,6 +5278,8 @@ function getTlsSessionId(context) {
5109
5278
  }
5110
5279
 
5111
5280
  // --- Proof-of-Work Middleware (The Tollbooth) ---
5281
+ const staticFileCache = new Map();
5282
+
5112
5283
  export const powMiddleware = (securityConfig) => {
5113
5284
  const engine = new FingerprintEngine(securityConfig);
5114
5285
 
@@ -5189,7 +5360,11 @@ export const powMiddleware = (securityConfig) => {
5189
5360
  }
5190
5361
  }
5191
5362
  if (jsFile && existsSync(jsFile)) {
5192
- const fileContent = readFileSync(jsFile);
5363
+ let fileContent = staticFileCache.get(jsFile);
5364
+ if (!fileContent) {
5365
+ fileContent = readFileSync(jsFile);
5366
+ staticFileCache.set(jsFile, fileContent);
5367
+ }
5193
5368
  res.setHeader('Content-Type', 'application/javascript');
5194
5369
  return res.send(fileContent);
5195
5370
  }
@@ -5206,7 +5381,11 @@ export const powMiddleware = (securityConfig) => {
5206
5381
  }
5207
5382
  }
5208
5383
  if (wasmFile && existsSync(wasmFile)) {
5209
- const fileContent = readFileSync(wasmFile);
5384
+ let fileContent = staticFileCache.get(wasmFile);
5385
+ if (!fileContent) {
5386
+ fileContent = readFileSync(wasmFile);
5387
+ staticFileCache.set(wasmFile, fileContent);
5388
+ }
5210
5389
  res.setHeader('Content-Type', 'application/wasm');
5211
5390
  return res.send(fileContent);
5212
5391
  }
@@ -5216,7 +5395,7 @@ export const powMiddleware = (securityConfig) => {
5216
5395
 
5217
5396
  const requestContext = {
5218
5397
  clientIp: req.ip || req.socket?.remoteAddress || "unknown",
5219
- path: req.path,
5398
+ path: sanitizeRedirectPath(req.path),
5220
5399
  cookies: req.cookies,
5221
5400
  query: req.query,
5222
5401
  body: req.body,
@@ -5269,7 +5448,7 @@ export const powMiddleware = (securityConfig) => {
5269
5448
  if (decision.cookie) {
5270
5449
  res.cookie(decision.cookie.name, decision.cookie.value, decision.cookie.options);
5271
5450
  }
5272
- return res.redirect(decision.path);
5451
+ return res.redirect(sanitizeRedirectPath(decision.path));
5273
5452
 
5274
5453
  case 'next':
5275
5454
  default:
@@ -5278,6 +5457,7 @@ export const powMiddleware = (securityConfig) => {
5278
5457
  };
5279
5458
  };
5280
5459
 
5460
+
5281
5461
  /**
5282
5462
  * @internal
5283
5463
  * Exporting an object containing the functions to make them mockable in tests.
@@ -5723,7 +5903,7 @@ export async function handleMetricsRequest(req, res, securityConfig) {
5723
5903
  if (typeof authorizationCallback === 'function') {
5724
5904
  const context = new RequestContext(
5725
5905
  req.ip,
5726
- req.path,
5906
+ sanitizeRedirectPath(req.path),
5727
5907
  req.headers,
5728
5908
  req.query,
5729
5909
  req.body,