@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.
- package/CHANGELOG.md +9 -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 +3 -1
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +234 -54
- package/src/js/fingerprint.utils.js +213 -183
- package/src/js/gpu_pow.solver.js +96 -1
- 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.engine.test.js +370 -370
- package/src/js/tests/fingerprint.test.js +87 -3
- package/src/js/tests/gpu_pow.test.js +81 -0
- package/src/js/tests/pow.solver.test.js +4 -4
- package/src/php/Challenge/ChallengeUtils.php +116 -27
- package/src/php/FingerprintEngine.php +103 -2
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')}`;
|
|
@@ -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
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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 = ${
|
|
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 = ${
|
|
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 =
|
|
986
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
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
|
-
|
|
1076
|
-
const
|
|
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
|
-
|
|
1083
|
-
|
|
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
|
-
|
|
1087
|
-
|
|
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
|
-
|
|
1090
|
-
|
|
1222
|
+
const blockIdx = Math.floor(addr / 1024);
|
|
1223
|
+
const elementIdx = addr % 1024;
|
|
1224
|
+
addr = getBlockElement(blockIdx, elementIdx) % totalElements;
|
|
1225
|
+
expectedSolution ^= addr;
|
|
1091
1226
|
}
|
|
1092
|
-
|
|
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
|
-
|
|
1460
|
+
const safePath = sanitizeRedirectPath(path);
|
|
1461
|
+
|
|
1462
|
+
const challengeScript = `
|
|
1326
1463
|
async function solve() {
|
|
1327
|
-
const nonce = ${
|
|
1328
|
-
|
|
1329
|
-
|
|
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
|
-
|
|
2975
|
-
|
|
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 = ${
|
|
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 = ${
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
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
|
-
|
|
3129
|
-
htmlTemplate =
|
|
3130
|
-
}
|
|
3131
|
-
|
|
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
|
-
|
|
3793
|
-
|
|
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
|
|
4256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|