@anonympins/fingerprint 0.1.4 → 0.2.1
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/README.md +100 -36
- package/fingerprint.builder.js +160 -103
- package/fingerprint.client.js +21 -4
- package/fingerprint.js +491 -295
- package/library.js +25 -19
- package/optimization.worker.js +28 -0
- package/package.json +4 -1
- package/pow.solver.js +205 -22
- package/pow.worker.js +27 -0
- package/problem-manager.js +175 -0
package/fingerprint.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import { BlockList } from "node:net";
|
|
3
3
|
import dns from "node:dns/promises";
|
|
4
|
+
import { problemManager } from "./problem-manager.js";
|
|
4
5
|
import { Optimization } from "./library.js";
|
|
5
6
|
import { cyrb53, FingerprintBuilder } from "./fingerprint.builder.js";
|
|
6
7
|
import { readFileSync } from "node:fs";
|
|
@@ -26,111 +27,12 @@ const getPowSecret = () => {
|
|
|
26
27
|
* @returns {string} The solver JavaScript code.
|
|
27
28
|
*/
|
|
28
29
|
const getPowSolverCode = () => {
|
|
29
|
-
try
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
console.warn('Could not load pow.solver.js for inlining, using fallback inline code');
|
|
36
|
-
// Fallback inline code if file cannot be loaded
|
|
37
|
-
return `(function(global){
|
|
38
|
-
async function solveCpuTargetInline(clientIp, nonce, target, clientSecret, progressCallback){
|
|
39
|
-
const cpuTarget = typeof target === 'bigint' ? target : BigInt('0x' + target);
|
|
40
|
-
let cpuSolution = 0;
|
|
41
|
-
const ipPart = clientIp || '';
|
|
42
|
-
while(true){
|
|
43
|
-
// When a clientSecret is used, the IP is omitted from the hash to make it independent of the network.
|
|
44
|
-
const msg = clientSecret ? \`\${nonce}:\${cpuSolution}:\${clientSecret}\` : \`\${ipPart}:\${nonce}:\${cpuSolution}\`;
|
|
45
|
-
const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg));
|
|
46
|
-
const hashHex = Array.from(new Uint8Array(buf)).map(b=>b.toString(16).padStart(2,'0')).join('');
|
|
47
|
-
if(BigInt('0x'+hashHex) < cpuTarget) break;
|
|
48
|
-
cpuSolution++;
|
|
49
|
-
if(cpuSolution % 100000 === 0) await new Promise(r=>setTimeout(r,0));
|
|
50
|
-
}
|
|
51
|
-
return cpuSolution;
|
|
52
|
-
}
|
|
53
|
-
async function solveMemory(seed, difficulty){
|
|
54
|
-
const size = difficulty * 1024 * 1024;
|
|
55
|
-
const buffer = new Uint32Array(size / 4);
|
|
56
|
-
let h = new TextEncoder().encode(seed).reduce((acc,v)=>acc+v,0);
|
|
57
|
-
for(let i=0;i<buffer.length;i++) buffer[i] = h = Math.imul(h^i,1597334677);
|
|
58
|
-
let solution = 0;
|
|
59
|
-
const iterations = size / 16;
|
|
60
|
-
let addr = buffer.length > 0 ? buffer[0] % buffer.length : 0;
|
|
61
|
-
for(let i=0;i<iterations;i++){
|
|
62
|
-
addr = buffer[addr] % buffer.length;
|
|
63
|
-
solution ^= addr;
|
|
64
|
-
}
|
|
65
|
-
return solution;
|
|
66
|
-
}
|
|
67
|
-
async function solveTsp(cities, targetMaxDistance){
|
|
68
|
-
function distance(c1,c2){return Math.sqrt(Math.pow(c1.x-c2.x,2)+Math.pow(c1.y-c2.y,2));}
|
|
69
|
-
function evaluatePathDistance(cities,path){
|
|
70
|
-
let total=0;
|
|
71
|
-
for(let i=0;i<path.length-1;i++) total+=distance(cities[path[i]],cities[path[i+1]]);
|
|
72
|
-
total+=distance(cities[path[path.length-1]],cities[path[0]]);
|
|
73
|
-
return total;
|
|
74
|
-
}
|
|
75
|
-
function solveTspNearestNeighbor(cities){
|
|
76
|
-
const n=cities.length;
|
|
77
|
-
if(n===0)return[];
|
|
78
|
-
let path=[0];
|
|
79
|
-
let visited=new Array(n).fill(false);
|
|
80
|
-
visited[0]=true;
|
|
81
|
-
for(let i=1;i<n;i++){
|
|
82
|
-
let nearest=-1, minDist=Infinity;
|
|
83
|
-
for(let j=0;j<n;j++){
|
|
84
|
-
if(!visited[j]){
|
|
85
|
-
const d=distance(cities[path[i-1]],cities[j]);
|
|
86
|
-
if(d<minDist){minDist=d;nearest=j;}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
path.push(nearest);
|
|
90
|
-
visited[nearest]=true;
|
|
91
|
-
}
|
|
92
|
-
return path;
|
|
93
|
-
}
|
|
94
|
-
await new Promise(r=>setTimeout(r,10));
|
|
95
|
-
const solutionPath=solveTspNearestNeighbor(cities);
|
|
96
|
-
const solutionDistance=evaluatePathDistance(cities,solutionPath);
|
|
97
|
-
return{path:solutionPath,distance:solutionDistance};
|
|
98
|
-
}
|
|
99
|
-
async function solveChallenge(challenge) {
|
|
100
|
-
const { type, nonce, clientSecret, cpuTarget, memDifficulty, cities, clientIp, targetMaxDistance } = challenge;
|
|
101
|
-
const solutions = {};
|
|
102
|
-
|
|
103
|
-
switch (type) {
|
|
104
|
-
case 'cpu_target':
|
|
105
|
-
solutions.cpu = await solveCpuTargetInline(clientIp, nonce, cpuTarget, clientSecret);
|
|
106
|
-
break;
|
|
107
|
-
case 'cpu_mem':
|
|
108
|
-
case 'cpu_mem_inline':
|
|
109
|
-
const memSeed = nonce + ":" + clientSecret;
|
|
110
|
-
const [cpuSol, memSol] = await Promise.all([
|
|
111
|
-
solveCpuTargetInline(clientIp, nonce, cpuTarget, clientSecret),
|
|
112
|
-
solveMemory(memSeed, memDifficulty)
|
|
113
|
-
]);
|
|
114
|
-
solutions.cpu = cpuSol;
|
|
115
|
-
solutions.mem = memSol;
|
|
116
|
-
break;
|
|
117
|
-
case 'tsp':
|
|
118
|
-
const tspResult = await solveTsp(cities, targetMaxDistance);
|
|
119
|
-
solutions.tsp = tspResult.path;
|
|
120
|
-
solutions.distance = tspResult.distance;
|
|
121
|
-
break;
|
|
122
|
-
default:
|
|
123
|
-
throw new Error(\`Unknown challenge type: \${type}\`);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return solutions;
|
|
127
|
-
}
|
|
128
|
-
global.solveCpuChallengeInline=solveCpuTargetInline;
|
|
129
|
-
global.solveMemoryChallenge=solveMemory;
|
|
130
|
-
global.solveTspChallenge=solveTsp;
|
|
131
|
-
global.solveChallenge=solveChallenge;
|
|
132
|
-
})(typeof window!=='undefined'?window:global);`;
|
|
133
|
-
}
|
|
30
|
+
// On supprime le try/catch. Si le fichier n'est pas trouvé, le processus plantera,
|
|
31
|
+
// ce qui est préférable à servir un code de secours potentiellement désynchronisé.
|
|
32
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
33
|
+
const __dirname = dirname(__filename);
|
|
34
|
+
const solverPath = join(__dirname, 'pow.solver.inline.js'); // Utilise la version inline
|
|
35
|
+
return readFileSync(solverPath, 'utf-8');
|
|
134
36
|
};
|
|
135
37
|
|
|
136
38
|
/**
|
|
@@ -189,7 +91,7 @@ function getHeaderSignature(context) {
|
|
|
189
91
|
for (let i = 0; i < context.rawHeaders.length; i += 2) {
|
|
190
92
|
headerKeys.push(context.rawHeaders[i]);
|
|
191
93
|
}
|
|
192
|
-
return cyrb53(headerKeys.join(','));
|
|
94
|
+
return cyrb53(headerKeys.sort().join(','));
|
|
193
95
|
}
|
|
194
96
|
|
|
195
97
|
/**
|
|
@@ -226,11 +128,6 @@ export function getCompositeDeviceHash(context) {
|
|
|
226
128
|
const ua = context.headers["user-agent"];
|
|
227
129
|
if (ua) {
|
|
228
130
|
srv.add("ua", ua);
|
|
229
|
-
// Extraire des infos supplémentaires du UA
|
|
230
|
-
const uaParts = parseUserAgent(ua);
|
|
231
|
-
if (uaParts.browser) srv.add("browser", uaParts.browser);
|
|
232
|
-
if (uaParts.os) srv.add("os_version", uaParts.os);
|
|
233
|
-
if (uaParts.device) srv.add("device_type", uaParts.device);
|
|
234
131
|
}
|
|
235
132
|
|
|
236
133
|
// 2. SIGNAL FORT: JA3 TLS Fingerprint
|
|
@@ -265,9 +162,6 @@ export function getCompositeDeviceHash(context) {
|
|
|
265
162
|
srv.add("upgrade", context.headers["upgrade-insecure-requests"]);
|
|
266
163
|
}
|
|
267
164
|
|
|
268
|
-
// 10. SIGNAL FORT: Ordonnancement des headers
|
|
269
|
-
srv.add("h_ord", getHeaderSignature(context));
|
|
270
|
-
|
|
271
165
|
// 11. SIGNAL AVANCÉ: Cookies (si disponible)
|
|
272
166
|
if (context.cookies) {
|
|
273
167
|
const cookieKeys = Object.keys(context.cookies).sort().join(',');
|
|
@@ -613,11 +507,11 @@ export const verifyPoWAndGenerateTicket = (
|
|
|
613
507
|
* Verifies a memory PoW solution.
|
|
614
508
|
* The server performs the same calculation to validate.
|
|
615
509
|
*/
|
|
616
|
-
export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret) => {
|
|
510
|
+
export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret = '') => {
|
|
617
511
|
const size = difficulty * 1024 * 1024;
|
|
618
512
|
const iterations = size / 16;
|
|
619
513
|
const buffer = new Uint32Array(size / 4);
|
|
620
|
-
const seed =
|
|
514
|
+
const seed = `:${nonce}:${clientSecret}`;
|
|
621
515
|
let h = new TextEncoder().encode(seed).reduce((acc, v) => acc + v, 0);
|
|
622
516
|
|
|
623
517
|
for (let i = 0; i < buffer.length; i++) {
|
|
@@ -762,6 +656,25 @@ function getHoneypotScore(context, honeypotConfig = {}) {
|
|
|
762
656
|
return { honeypotScore: 0 };
|
|
763
657
|
}
|
|
764
658
|
|
|
659
|
+
/**
|
|
660
|
+
* @private
|
|
661
|
+
* Map of malicious patterns grouped by type.
|
|
662
|
+
*/
|
|
663
|
+
const injectionPatterns = {
|
|
664
|
+
// SQL/NoSQL injections, including time-based attacks
|
|
665
|
+
sql: /(\$ne|' *OR *'1'='1|['";]\s*--|; ?(DROP|TRUNCATE|DELETE)|UNION SELECT|SLEEP\(|BENCHMARK\(|WAITFOR DELAY)/i,
|
|
666
|
+
// Log4Shell (JNDI injection)
|
|
667
|
+
log4shell: /\$\{jndi:(ldap|rmi|dns):/i,
|
|
668
|
+
// Server-Side Template Injection (SSTI) for engines like Jinja2, Twig, etc.
|
|
669
|
+
ssti: /\{\{.*\}\}|\{%.*%\}/,
|
|
670
|
+
// XML External Entity (XXE) injection
|
|
671
|
+
xxe: /<!ENTITY\s+.*SYSTEM/i,
|
|
672
|
+
// Path Traversal
|
|
673
|
+
traversal: /(\.\.\/|\.\.\\)/,
|
|
674
|
+
// Remote Command Execution (RCE)
|
|
675
|
+
rce: /`.*`|(^|[\n;&|]\s*)(ping|ls|whoami|cat|rm|ncat|nc|bash|sh|powershell|cmd)\b/i,
|
|
676
|
+
};
|
|
677
|
+
|
|
765
678
|
/**
|
|
766
679
|
* Calcule un score basé sur les métriques comportementales envoyées par le client.
|
|
767
680
|
* @param {object} context - Le contexte de la requête, contenant les en-têtes.
|
|
@@ -821,6 +734,29 @@ function getBehaviorScore(context) {
|
|
|
821
734
|
}
|
|
822
735
|
}
|
|
823
736
|
|
|
737
|
+
/**
|
|
738
|
+
* Calcule un score basé sur l'incohérence temporelle entre le client et le serveur pour détecter les attaques par rejeu.
|
|
739
|
+
* @param {object} context - Le contexte de la requête, contenant le timestamp de la requête.
|
|
740
|
+
* @param {object} metrics - Les métriques comportementales parsées depuis le client.
|
|
741
|
+
* @returns {{timeInconsistencyScore: number}}
|
|
742
|
+
*/
|
|
743
|
+
function getTimeInconsistencyScore(context, metrics) {
|
|
744
|
+
const REPLAY_THRESHOLD_MS = 5000; // 5 secondes
|
|
745
|
+
let score = 0;
|
|
746
|
+
|
|
747
|
+
if (metrics.clientTimestamp && context.requestTimestamp) {
|
|
748
|
+
const timeDelta = context.requestTimestamp - metrics.clientTimestamp;
|
|
749
|
+
|
|
750
|
+
// Un delta très grand est un signal fort d'attaque par rejeu.
|
|
751
|
+
// Un delta négatif peut arriver si l'horloge du client est en avance, on l'ignore.
|
|
752
|
+
if (timeDelta > REPLAY_THRESHOLD_MS) {
|
|
753
|
+
// La pénalité est proportionnelle au dépassement du seuil.
|
|
754
|
+
score = Math.min(100, (timeDelta / REPLAY_THRESHOLD_MS - 1) * 50);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return { timeInconsistencyScore: score };
|
|
758
|
+
}
|
|
759
|
+
|
|
824
760
|
/**
|
|
825
761
|
* Calcule un score d'incohérence entre les données du fingerprint client et les en-têtes serveur.
|
|
826
762
|
* @param {object} context - Le contexte de la requête.
|
|
@@ -903,8 +839,10 @@ function getRequestPatternScore(context, deviceData, patternConfig = {}) {
|
|
|
903
839
|
const now = Date.now();
|
|
904
840
|
const currentPath = context.path;
|
|
905
841
|
// Make the function robust to handle both URLSearchParams and plain objects for query.
|
|
906
|
-
|
|
907
|
-
|
|
842
|
+
const params =
|
|
843
|
+
context.query instanceof URLSearchParams
|
|
844
|
+
? new URLSearchParams(context.query.toString()) // Clone to avoid modifying the original
|
|
845
|
+
: new URLSearchParams(context.query || {});
|
|
908
846
|
params.sort(); // Sort for deterministic order
|
|
909
847
|
const currentQueryString = params.toString();
|
|
910
848
|
|
|
@@ -960,8 +898,7 @@ function getRequestPatternScore(context, deviceData, patternConfig = {}) {
|
|
|
960
898
|
// 5. (NOUVEAU) Analyse de la distribution des délais avec la loi de Benford
|
|
961
899
|
if (deviceData.timingHistory.length >= benfordMinSamples) {
|
|
962
900
|
// On concatène tous les délais en une seule chaîne de chiffres.
|
|
963
|
-
const
|
|
964
|
-
const benfordDeviation = Optimization.Operators.benfordTest(timingString);
|
|
901
|
+
const benfordDeviation = Optimization.Operators.benfordTest(deviceData.timingHistory);
|
|
965
902
|
|
|
966
903
|
// Une déviation > 0.15 est suspecte. On peut pondérer la pénalité.
|
|
967
904
|
// Une déviation de 0.3 (très suspecte) donnerait un score de 100 (0.3 / 0.3 * 100).
|
|
@@ -1242,6 +1179,9 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
1242
1179
|
// On appelle getHoneypotScore ici pour que son résultat soit inclus dans le vecteur.
|
|
1243
1180
|
const { honeypotScore } = getHoneypotScore(context, honeypotConfig);
|
|
1244
1181
|
|
|
1182
|
+
// NOUVEAU: On calcule le score d'incohérence temporelle.
|
|
1183
|
+
const { timeInconsistencyScore } = getTimeInconsistencyScore(context, JSON.parse(context.headers['x-behavior-metrics'] || '{}'));
|
|
1184
|
+
|
|
1245
1185
|
// NOUVEAU: On calcule le score d'incohérence entre les couches.
|
|
1246
1186
|
const { crossLayerInconsistencyScore } = getCrossLayerInconsistency(context);
|
|
1247
1187
|
|
|
@@ -1257,7 +1197,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
1257
1197
|
deviceData.ips = new Set(deviceData.ips);
|
|
1258
1198
|
}
|
|
1259
1199
|
// Le vecteur de suspicion est maintenant complet.
|
|
1260
|
-
return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, requestPatternScore, crossLayerInconsistencyScore };
|
|
1200
|
+
return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore };
|
|
1261
1201
|
};
|
|
1262
1202
|
|
|
1263
1203
|
// A residential user can change networks (home, 4G, public wifi).
|
|
@@ -1320,20 +1260,44 @@ const BASE_TARGET = MAX_DIFFICULTY_TARGET >> 16n;
|
|
|
1320
1260
|
* @param {number} suspicionFactor - A number from 0 to 1.
|
|
1321
1261
|
* @returns {BigInt} The target number.
|
|
1322
1262
|
*/
|
|
1323
|
-
function calculateTarget(suspicionFactor) {
|
|
1263
|
+
function calculateTarget(suspicionFactor, securityConfig = {}) {
|
|
1324
1264
|
// Difficulty range adjusted to be realistic.
|
|
1325
1265
|
// MIN_DIFFICULTY: Fast enough not to bother a slightly suspicious user.
|
|
1326
1266
|
// MAX_DIFFICULTY: Slow enough to heavily penalize a bot, but feasible for a patient human (5-30s).
|
|
1327
|
-
|
|
1328
|
-
const
|
|
1267
|
+
// NOUVEAU: La difficulté est maintenant configurable.
|
|
1268
|
+
const { cpu: cpuConfig = {} } = securityConfig;
|
|
1269
|
+
const MIN_DIFFICULTY_BITS = cpuConfig.minDifficultyBits ?? 8;
|
|
1270
|
+
const MAX_DIFFICULTY_BITS = cpuConfig.maxDifficultyBits ?? 16;
|
|
1329
1271
|
|
|
1330
1272
|
// Use linear interpolation between min and max difficulty.
|
|
1331
1273
|
const totalDifficultyBits =
|
|
1332
1274
|
MIN_DIFFICULTY_BITS +
|
|
1333
1275
|
suspicionFactor * (MAX_DIFFICULTY_BITS - MIN_DIFFICULTY_BITS);
|
|
1276
|
+
|
|
1277
|
+
if (totalDifficultyBits <= 0) return 2n ** 256n - 1n; // Si la difficulté est nulle ou négative, la cible est maximale (aucun challenge).
|
|
1278
|
+
|
|
1279
|
+
// The correct way to calculate the target is to define the number of leading zero bits required.
|
|
1280
|
+
// A target for N bits of difficulty is 2^(256-N).
|
|
1281
|
+
// We can calculate this with a left-shift on 1.
|
|
1282
|
+
const shift = 256n - BigInt(Math.floor(totalDifficultyBits));
|
|
1283
|
+
return 1n << shift;
|
|
1284
|
+
}
|
|
1334
1285
|
|
|
1335
|
-
|
|
1336
|
-
|
|
1286
|
+
/**
|
|
1287
|
+
* @private
|
|
1288
|
+
* Crée le bloc de base pour le challenge CPU.
|
|
1289
|
+
* Ce buffer contient toutes les données sauf la solution.
|
|
1290
|
+
* @param {string} nonce
|
|
1291
|
+
* @param {string} clientSecret
|
|
1292
|
+
* @param {string} fingerprint
|
|
1293
|
+
* @returns {Buffer}
|
|
1294
|
+
*/
|
|
1295
|
+
function createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint) {
|
|
1296
|
+
const sortedFingerprint = (fingerprint || '').split('|').filter(p => p).sort().join('|');
|
|
1297
|
+
// On concatène les chaînes, puis on les convertit en buffer une seule fois.
|
|
1298
|
+
// Cela garantit que le client et le serveur travaillent sur la même base binaire.
|
|
1299
|
+
const messageBase = `${nonce}:${clientSecret}:${sortedFingerprint}:`; // Le ':' final est le séparateur pour la solution.
|
|
1300
|
+
return Buffer.from(messageBase, 'utf8');
|
|
1337
1301
|
}
|
|
1338
1302
|
|
|
1339
1303
|
/**
|
|
@@ -1344,12 +1308,15 @@ export function generateCpuTargetChallenge(
|
|
|
1344
1308
|
nonce,
|
|
1345
1309
|
suspicionFactor,
|
|
1346
1310
|
originalUrl,
|
|
1311
|
+
securityConfig,
|
|
1347
1312
|
) {
|
|
1348
|
-
const target = calculateTarget(suspicionFactor);
|
|
1313
|
+
const target = calculateTarget(suspicionFactor, securityConfig);
|
|
1314
|
+
// Le baseBlock est créé ici et sera stocké dans le contexte du challenge.
|
|
1315
|
+
const baseBlock = createCpuChallengeBaseBlock(nonce, null, ''); // Pour le challenge simple, le secret et le fingerprint sont vides.
|
|
1349
1316
|
return {
|
|
1350
1317
|
type: "cpu_target",
|
|
1351
1318
|
nonce: nonce,
|
|
1352
|
-
target: target.toString(16),
|
|
1319
|
+
target: target.toString(16),
|
|
1353
1320
|
path: originalUrl,
|
|
1354
1321
|
};
|
|
1355
1322
|
}
|
|
@@ -1374,10 +1341,11 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
|
|
|
1374
1341
|
async function solve() {
|
|
1375
1342
|
const clientIp = ${JSON.stringify(clientIp)};
|
|
1376
1343
|
const nonce = ${JSON.stringify(nonce)};
|
|
1377
|
-
const cpuTarget = BigInt("0x${target}");
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1344
|
+
const cpuTarget = BigInt("0x" + "${target}");
|
|
1345
|
+
// La nouvelle version de solveCpuChallengeInline n'a plus besoin de l'IP ou du secret,
|
|
1346
|
+
// car tout est dans le baseBlock. Pour la compatibilité de ce challenge simple, on passe null.
|
|
1347
|
+
const baseBlockBytes = new TextEncoder().encode(nonce + ":");
|
|
1348
|
+
const solution = await window.solveCpuChallengeInline(baseBlockBytes, cpuTarget, (progress) => {});
|
|
1381
1349
|
window.location.href = ${JSON.stringify(path)} + "?pow_type=cpu_target&pow_nonce=" + nonce + "&pow_solution=" + solution;
|
|
1382
1350
|
}
|
|
1383
1351
|
solve();
|
|
@@ -1392,9 +1360,14 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
|
|
|
1392
1360
|
* @param {string} clientIp - The client's IP address.
|
|
1393
1361
|
* @returns {string} HTML content.
|
|
1394
1362
|
*/
|
|
1395
|
-
function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig, trapContainerHtml) {
|
|
1363
|
+
function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret, securityConfig, trapContainerHtml, originalFingerprint) {
|
|
1396
1364
|
const { nonce, target, path } = cpuChallengeDetails;
|
|
1397
1365
|
const solverCode = getPowSolverCode();
|
|
1366
|
+
// On prépare le baseBlock pour le client. Il sera envoyé sous forme de tableau d'octets.
|
|
1367
|
+
// Le fingerprint est maintenant passé directement en paramètre.
|
|
1368
|
+
const fingerprint = originalFingerprint;
|
|
1369
|
+
const baseBlock = createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint);
|
|
1370
|
+
const baseBlockBytes = `[${baseBlock.toString('utf8').split('').map(c => c.charCodeAt(0)).join(',')}]`;
|
|
1398
1371
|
|
|
1399
1372
|
const challengeScript = `
|
|
1400
1373
|
async function solve() {
|
|
@@ -1402,19 +1375,18 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
1402
1375
|
const path = ${JSON.stringify(path)};
|
|
1403
1376
|
const clientSecret = ${JSON.stringify(clientSecret)};
|
|
1404
1377
|
const clientIp = ${JSON.stringify(clientIp)};
|
|
1405
|
-
const cpuTarget = BigInt("0x${target}");
|
|
1378
|
+
const cpuTarget = BigInt("0x" + "${target}");
|
|
1406
1379
|
const memDifficulty = ${memoryDifficulty};
|
|
1380
|
+
// Le client reçoit directement le 'baseBlock' sous forme de tableau d'octets.
|
|
1381
|
+
// Il n'a plus besoin de construire le message lui-même.
|
|
1382
|
+
const baseBlock = new Uint8Array(${baseBlockBytes});
|
|
1407
1383
|
|
|
1408
1384
|
// --- CPU Challenge ---
|
|
1409
|
-
document.getElementById('loader').innerText = '⚙️ Performing CPU security calculation...';
|
|
1410
|
-
const cpuSolution = await window.solveCpuChallengeInline(clientIp, nonce, cpuTarget, clientSecret, (progress) => {
|
|
1411
|
-
// Optional progress callback
|
|
1412
|
-
});
|
|
1385
|
+
document.getElementById('loader').innerText = '⚙️ Performing CPU security calculation...'; const cpuSolution = await window.solveCpuChallengeInline(baseBlock, cpuTarget, (progress) => {});
|
|
1413
1386
|
|
|
1414
1387
|
// --- Memory Challenge ---
|
|
1415
1388
|
document.getElementById('loader').innerText = '⚙️ Performing memory allocation and calculation... (' + memDifficulty + ' MB)';
|
|
1416
|
-
await new Promise(r => setTimeout(r, 10)); // Yield to update UI
|
|
1417
|
-
|
|
1389
|
+
await new Promise(r => setTimeout(r, 10)); // Yield to update UI
|
|
1418
1390
|
let memSolution = 0;
|
|
1419
1391
|
try {
|
|
1420
1392
|
const memSeed = nonce + ":" + clientSecret;
|
|
@@ -1423,7 +1395,10 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
1423
1395
|
document.getElementById('loader').innerText = "Error: Insufficient memory. Please refresh.";
|
|
1424
1396
|
return;
|
|
1425
1397
|
}
|
|
1426
|
-
|
|
1398
|
+
|
|
1399
|
+
// Redirect with both solutions and the fingerprint used to solve.
|
|
1400
|
+
const finalUrl = path + "?pow_type=cpu_mem&pow_nonce=" + ${JSON.stringify(nonce)} + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + memSolution;
|
|
1401
|
+
window.location.href = finalUrl;
|
|
1427
1402
|
}
|
|
1428
1403
|
solve();
|
|
1429
1404
|
`;
|
|
@@ -1454,22 +1429,56 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
1454
1429
|
*/
|
|
1455
1430
|
export function verifyCpuTargetPoWAndGenerateTicket(
|
|
1456
1431
|
clientIp, // This parameter is crucial and must be the actual client IP
|
|
1457
|
-
ticketTtl,
|
|
1432
|
+
ticketTtl,
|
|
1458
1433
|
nonce,
|
|
1459
1434
|
solution,
|
|
1460
|
-
|
|
1461
|
-
target, // La cible est maintenant passée directement en hexadécimal
|
|
1435
|
+
challengeContext = {}, // Le contexte complet du challenge est maintenant passé
|
|
1462
1436
|
) {
|
|
1463
|
-
const
|
|
1464
|
-
|
|
1465
|
-
|
|
1437
|
+
const { cpuTarget, baseBlock } = challengeContext;
|
|
1438
|
+
if (!cpuTarget || !baseBlock) {
|
|
1439
|
+
console.error('[FP Server Verify] Invalid challenge context. Missing cpuTarget or baseBlock.');
|
|
1440
|
+
return null;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
// Le baseBlock est déjà un Buffer ou un tableau d'octets.
|
|
1444
|
+
// On s'assure que c'est un Buffer pour la concaténation.
|
|
1445
|
+
const baseBlockBuffer = Buffer.isBuffer(baseBlock) ? baseBlock : Buffer.from(baseBlock);
|
|
1446
|
+
const solutionBuffer = Buffer.from(String(solution), 'utf8');
|
|
1447
|
+
|
|
1448
|
+
// Concaténation binaire directe. C'est la garantie de cohérence.
|
|
1449
|
+
const finalBlock = Buffer.concat([baseBlockBuffer, solutionBuffer]);
|
|
1450
|
+
|
|
1466
1451
|
const hash = crypto
|
|
1467
1452
|
.createHash("sha256")
|
|
1468
|
-
.update(
|
|
1453
|
+
.update(finalBlock)
|
|
1469
1454
|
.digest("hex");
|
|
1470
1455
|
const hashAsInt = BigInt("0x" + hash);
|
|
1456
|
+
const targetAsInt = BigInt("0x" + cpuTarget);
|
|
1457
|
+
|
|
1458
|
+
// --- NOUVEAUX LOGS POUR LE DÉBOGAGE ---
|
|
1459
|
+
console.log('[FP Server Verify] Intermediate values:', {
|
|
1460
|
+
hashCalculated: `0x${hash}`,
|
|
1461
|
+
hashAsInt: hashAsInt.toString(), // Log as string to see full value
|
|
1462
|
+
target: `0x${cpuTarget}`,
|
|
1463
|
+
targetAsInt: targetAsInt.toString(), // Log as string to see full value
|
|
1464
|
+
});
|
|
1465
|
+
// --- FIN DES NOUVEAUX LOGS ---
|
|
1466
|
+
|
|
1467
|
+
const isValid = hashAsInt < targetAsInt;
|
|
1471
1468
|
|
|
1472
|
-
|
|
1469
|
+
// --- AJOUT DE LOGS POUR LE DÉBOGAGE ---
|
|
1470
|
+
if (!isValid) {
|
|
1471
|
+
console.log('[FP Server Verify] CPU PoW verification FAILED. Details:', {
|
|
1472
|
+
hashCalculated: `0x${hash}`,
|
|
1473
|
+
target: `0x${cpuTarget}`,
|
|
1474
|
+
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
// --- FIN DES LOGS ---
|
|
1478
|
+
|
|
1479
|
+
if (isValid) {
|
|
1480
|
+
console.log('[FP Server Verify] CPU PoW verification PASSED. Details:', {
|
|
1481
|
+
});
|
|
1473
1482
|
// The comparison is direct with native BigInts
|
|
1474
1483
|
// The proof is valid, generate the ticket
|
|
1475
1484
|
const expiry = Date.now() + (ticketTtl || 3600000); // Calcule l'expiration à partir du TTL
|
|
@@ -1483,106 +1492,6 @@ export function verifyCpuTargetPoWAndGenerateTicket(
|
|
|
1483
1492
|
return null;
|
|
1484
1493
|
}
|
|
1485
1494
|
|
|
1486
|
-
const staticExtensions = new RegExp(
|
|
1487
|
-
"\\.(js|css|png|jpg|jpeg|gif|svg|mp3|webp|ico|woff|woff2|ttf|otf|map|json|manifest|webmanifest)$",
|
|
1488
|
-
"i",
|
|
1489
|
-
);
|
|
1490
|
-
const isStaticResource = (path) => staticExtensions.test(path);
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
/**
|
|
1494
|
-
* Détermine le TTL optimal pour un ticket en utilisant un algorithme génétique multi-objectifs.
|
|
1495
|
-
* @param {number} suspicionScore - Le score de suspicion de la requête.
|
|
1496
|
-
* @returns {number} Le TTL optimal calculé en millisecondes.
|
|
1497
|
-
*/
|
|
1498
|
-
function determineOptimalTicketTtl(suspicionScore) {
|
|
1499
|
-
// Définir les bornes pour la durée de vie du ticket (5 minutes à 24 heures)
|
|
1500
|
-
const MIN_TTL = 300000;
|
|
1501
|
-
const MAX_TTL = 86400000;
|
|
1502
|
-
|
|
1503
|
-
const solverFunction = () => {
|
|
1504
|
-
const fitnessFunction = Optimization.Operators.createOptimalTtlEvaluator({ suspicionScore });
|
|
1505
|
-
|
|
1506
|
-
// Un "individu" est simplement une valeur de TTL en millisecondes.
|
|
1507
|
-
const createIndividual = () => MIN_TTL + Math.random() * (MAX_TTL - MIN_TTL);
|
|
1508
|
-
const crossover = (ttl1, ttl2) => (ttl1 + ttl2) / 2;
|
|
1509
|
-
const mutate = (ttl) => {
|
|
1510
|
-
const newTtl = ttl + (Math.random() - 0.5) * (MAX_TTL - MIN_TTL) * 0.1; // Mutation de +/- 10% max
|
|
1511
|
-
return Math.max(MIN_TTL, Math.min(MAX_TTL, newTtl));
|
|
1512
|
-
};
|
|
1513
|
-
|
|
1514
|
-
const paretoFront = Optimization.geneticAlgorithmMultiObjective(
|
|
1515
|
-
createIndividual,
|
|
1516
|
-
fitnessFunction,
|
|
1517
|
-
crossover,
|
|
1518
|
-
mutate,
|
|
1519
|
-
{
|
|
1520
|
-
generations: 40,
|
|
1521
|
-
populationSize: 30,
|
|
1522
|
-
}
|
|
1523
|
-
);
|
|
1524
|
-
|
|
1525
|
-
// Pour runMultiple, on doit retourner un objet avec une propriété "fitness" ou "energy".
|
|
1526
|
-
// Pour un front de Pareto, il n'y a pas de score unique. On choisit la meilleure solution
|
|
1527
|
-
// en fonction du score de suspicion et on lui assigne un score de 0 pour que runMultiple la sélectionne.
|
|
1528
|
-
if (!paretoFront || paretoFront.length === 0) {
|
|
1529
|
-
return { solution: null, fitness: Infinity };
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
// Stratégie de sélection :
|
|
1533
|
-
// Pour un score faible (< 50), on privilégie la solution avec le plus grand TTL (minimise la friction).
|
|
1534
|
-
// Pour un score élevé (>= 50), on privilégie la solution avec le plus petit TTL (minimise le risque).
|
|
1535
|
-
let bestSolutionInFront;
|
|
1536
|
-
if (suspicionScore < 50) {
|
|
1537
|
-
bestSolutionInFront = paretoFront.reduce((max, p) => Math.max(max, p.solution), 0);
|
|
1538
|
-
} else {
|
|
1539
|
-
bestSolutionInFront = paretoFront.reduce((min, p) => Math.min(min, p.solution), Infinity);
|
|
1540
|
-
}
|
|
1541
|
-
return { solution: bestSolutionInFront, fitness: 0 }; // fitness=0 car on a déjà la meilleure solution du cycle.
|
|
1542
|
-
};
|
|
1543
|
-
|
|
1544
|
-
// On exécute le solveur 20 fois pour trouver une solution plus stable et robuste.
|
|
1545
|
-
const { bestResult } = Optimization.runMultiple(solverFunction, 20);
|
|
1546
|
-
|
|
1547
|
-
if (!bestResult || !bestResult.solution || bestResult.solution === Infinity) {
|
|
1548
|
-
// Fallback : si l'algo ne retourne rien, on applique une règle simple et sûre.
|
|
1549
|
-
return Math.max(MIN_TTL, MAX_TTL - (suspicionScore / 100) * MAX_TTL);
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
// runMultiple choisit le meilleur résultat sur la base du score (ici, 0).
|
|
1553
|
-
// La "meilleure" solution dépendra du cycle qui a trouvé le meilleur compromis.
|
|
1554
|
-
return Math.round(bestResult.solution);
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
/**
|
|
1558
|
-
* Vérifie si une chaîne de caractères contient des patterns d'injection connus.
|
|
1559
|
-
* @param {string} str - La chaîne à vérifier.
|
|
1560
|
-
* @returns {boolean} - True si un pattern malveillant est détecté.
|
|
1561
|
-
* @private
|
|
1562
|
-
*/
|
|
1563
|
-
function isMalicious(str) {
|
|
1564
|
-
if (typeof str !== 'string') return false;
|
|
1565
|
-
// Regex pour les injections SQL et NoSQL de base
|
|
1566
|
-
// Ajout de la détection des injections basées sur le temps (SLEEP, BENCHMARK, WAITFOR) et d'autres commandes dangereuses.
|
|
1567
|
-
const injectionRegex = /(\$ne|' *OR *'1'='1|['";]\s*--|; ?(DROP|TRUNCATE|DELETE)|UNION SELECT|SLEEP\(|BENCHMARK\(|WAITFOR DELAY)/i;
|
|
1568
|
-
// Regex pour les injections plus avancées
|
|
1569
|
-
const log4ShellRegex = /\$\{jndi:(ldap|rmi|dns):/i;
|
|
1570
|
-
const sstiRegex = /\{\{.*\}\}|\{%.*%\}/; // Détecte les syntaxes de type Jinja2, Twig, etc.
|
|
1571
|
-
const xxeRegex = /<!ENTITY\s+.*SYSTEM/i;
|
|
1572
|
-
const pathTraversalRegex = /(\.\.\/|\.\.\\)/;
|
|
1573
|
-
// Regex pour les injections de commandes.
|
|
1574
|
-
// Elle détecte :
|
|
1575
|
-
// 1. L'utilisation de backticks ``.
|
|
1576
|
-
// 2. Des commandes dangereuses (rm, whoami...) qui sont soit au début de la chaîne,
|
|
1577
|
-
// soit précédées par un séparateur de commande (;, &&, ||, |) suivi d'espaces.
|
|
1578
|
-
const commandInjectionRegex = /`.*`|(^|[\n;&|]\s*)(ping|ls|whoami|cat|rm|ncat|nc|bash|sh|powershell|cmd)\b/i;
|
|
1579
|
-
|
|
1580
|
-
return injectionRegex.test(str) || log4ShellRegex.test(str) || sstiRegex.test(str) || xxeRegex.test(str) || pathTraversalRegex.test(str) || commandInjectionRegex.test(str);
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
// --- Middleware Proof-of-Work (Le péage) ---
|
|
1584
|
-
export { isMalicious };
|
|
1585
|
-
|
|
1586
1495
|
export class FingerprintEngine {
|
|
1587
1496
|
constructor(securityConfig) {
|
|
1588
1497
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
@@ -1609,7 +1518,8 @@ export class FingerprintEngine {
|
|
|
1609
1518
|
(suspicionVector.inconsistencyScore || 0) * (weights.inconsistencyScore || 0) +
|
|
1610
1519
|
(suspicionVector.honeypotScore || 0) * (weights.honeypotScore || 0) +
|
|
1611
1520
|
(suspicionVector.behaviorScore || 0) * (weights.behaviorScore || 0) +
|
|
1612
|
-
(suspicionVector.crossLayerInconsistencyScore || 0) * (weights.crossLayerInconsistencyScore || 0)
|
|
1521
|
+
(suspicionVector.crossLayerInconsistencyScore || 0) * (weights.crossLayerInconsistencyScore || 0) +
|
|
1522
|
+
(suspicionVector.timeInconsistencyScore || 0) * (weights.timeInconsistencyScore || 0);
|
|
1613
1523
|
|
|
1614
1524
|
return Math.min(100, score);
|
|
1615
1525
|
}
|
|
@@ -1746,7 +1656,7 @@ export class FingerprintEngine {
|
|
|
1746
1656
|
this._log('Whitelisted bot verified - allowing request', { clientIp });
|
|
1747
1657
|
return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'bot' } };
|
|
1748
1658
|
}
|
|
1749
|
-
|
|
1659
|
+
|
|
1750
1660
|
// Resolve identity and check for persisted "condemned" status early.
|
|
1751
1661
|
const { deviceId, deviceData, newCookie } = await resolveRequestIdentity(requestContext, this.securityConfig);
|
|
1752
1662
|
const isNewDevice = !!newCookie;
|
|
@@ -1756,7 +1666,7 @@ export class FingerprintEngine {
|
|
|
1756
1666
|
if (deviceData?.condemned) {
|
|
1757
1667
|
this._log('Device condemned - blocking request', { deviceId });
|
|
1758
1668
|
if (onDeviceCompromised) {
|
|
1759
|
-
onDeviceCompromised({ deviceId:
|
|
1669
|
+
onDeviceCompromised({ deviceId: deviceId, clientIp, reason: 'Previously condemned', score: 100, vector: { honeypotScore: 100 } });
|
|
1760
1670
|
}
|
|
1761
1671
|
return { action: 'block', status: 404, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
|
|
1762
1672
|
}
|
|
@@ -1792,6 +1702,7 @@ export class FingerprintEngine {
|
|
|
1792
1702
|
const isSuspiciousHigh = finalScore >= thresholds.high && !isBlocked;
|
|
1793
1703
|
const isSuspiciousMedium = finalScore >= thresholds.medium;
|
|
1794
1704
|
const isSuspicious = finalScore >= thresholds.low;
|
|
1705
|
+
const isVerySuspicious = finalScore >= thresholds.medium; // Seuil pour le challenge d'optimisation
|
|
1795
1706
|
|
|
1796
1707
|
// Calculate an analog "suspicion factor" (0 to 1+) for progressive difficulty
|
|
1797
1708
|
const suspicionFactor = isSuspicious
|
|
@@ -1816,10 +1727,10 @@ export class FingerprintEngine {
|
|
|
1816
1727
|
// --- NOUVELLE LOGIQUE DE PRIORITÉ ---
|
|
1817
1728
|
// Si une solution de challenge est soumise, on la traite en priorité absolue,
|
|
1818
1729
|
// avant même de recalculer le score de suspicion.
|
|
1819
|
-
const { pow_type, pow_solution, pow_solution_cpu, pow_solution_mem } = query;
|
|
1820
|
-
if (pow_nonce && (pow_solution ||
|
|
1730
|
+
const { pow_type, pow_solution, pow_solution_cpu, pow_solution_mem, pow_fp, pow_solution_population, pow_solution_work_result, pow_problem_id } = query;
|
|
1731
|
+
if (pow_nonce && (pow_solution || pow_solution_cpu)) { // Vérifie pow_solution pour la compatibilité ascendante
|
|
1821
1732
|
this._log('Challenge solution submitted', { pow_type, pow_nonce });
|
|
1822
|
-
|
|
1733
|
+
|
|
1823
1734
|
// On doit calculer le score de suspicion *avant* de valider le ticket,
|
|
1824
1735
|
// car le TTL optimal en dépend.
|
|
1825
1736
|
const preliminaryVector = suspicionVector; // Use the already calculated vector
|
|
@@ -1842,27 +1753,57 @@ export class FingerprintEngine {
|
|
|
1842
1753
|
const probationaryTtl = 30000; // 30 secondes
|
|
1843
1754
|
|
|
1844
1755
|
if (challengeContext) {
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1756
|
+
// *** NOUVELLE VÉRIFICATION CRUCIALE ***
|
|
1757
|
+
// On compare le fingerprint soumis par le solver (`pow_fp`) avec celui stocké
|
|
1758
|
+
// lors de l'émission du challenge.
|
|
1759
|
+
// --- FIX: Use submitted fingerprint, but fallback to current request's fingerprint ---
|
|
1760
|
+
// This handles API clients that might not use the full client-side library but still solve the challenge.
|
|
1761
|
+
const solverFingerprint = pow_fp || getCompositeDeviceHash(requestContext);
|
|
1762
|
+
const originalFingerprint = challengeContext.fingerprint; // This is the fingerprint of the request that *triggered* the challenge
|
|
1763
|
+
|
|
1764
|
+
let similarity;
|
|
1765
|
+
const similarityThreshold = this.securityConfig.similarityThreshold ?? 0.95;
|
|
1766
|
+
|
|
1767
|
+
// If fingerprints are simple strings (like test placeholders 'fp-probation')
|
|
1768
|
+
// and don't contain the typical structure, fall back to a strict equality check.
|
|
1769
|
+
if (!originalFingerprint?.includes(':') || !solverFingerprint?.includes(':')) {
|
|
1770
|
+
similarity = (originalFingerprint === solverFingerprint) ? 1.0 : 0.0;
|
|
1771
|
+
} else {
|
|
1772
|
+
// Use the weighted comparison for structured fingerprints.
|
|
1773
|
+
// We compare the fingerprint of the request that triggered the challenge
|
|
1774
|
+
// with the fingerprint of the request that is submitting the solution.
|
|
1775
|
+
// They should be very similar.
|
|
1776
|
+
similarity = FingerprintBuilder.compare(originalFingerprint, getCompositeDeviceHash(requestContext));
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
if (similarity < similarityThreshold) {
|
|
1780
|
+
this._log('Fingerprint mismatch - challenge solved on a different machine!', {
|
|
1781
|
+
original: originalFingerprint,
|
|
1782
|
+
solver: solverFingerprint,
|
|
1783
|
+
similarity: similarity.toFixed(4),
|
|
1784
|
+
threshold: similarityThreshold
|
|
1865
1785
|
});
|
|
1786
|
+
isValid = false;
|
|
1787
|
+
} else {
|
|
1788
|
+
optimalTtl = determineOptimalTicketTtl(preliminaryScore);
|
|
1789
|
+
finalTtl = isProbationary ? probationaryTtl : optimalTtl;
|
|
1790
|
+
this._log('Challenge context found, verifying solution', { optimalTtl, finalTtl });
|
|
1791
|
+
|
|
1792
|
+
if ((pow_type === "cpu_target" || !pow_type) && (pow_solution_cpu || pow_solution)) { // !pow_type pour compatibilité
|
|
1793
|
+
const cpuSolution = pow_solution_cpu || pow_solution;
|
|
1794
|
+
ticket = verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, cpuSolution, challengeContext);
|
|
1795
|
+
isValid = ticket !== null;
|
|
1796
|
+
this._log('CPU target challenge verification', { isValid });
|
|
1797
|
+
} else if (pow_type === "cpu_mem" && pow_solution_cpu && pow_solution_mem) { const cpuTicket = verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, pow_solution_cpu, challengeContext);
|
|
1798
|
+
const isMemValid = verifyMemoryPoW(pow_nonce, pow_solution_mem, challengeContext.memDifficulty, challengeContext.clientSecret); // Memory PoW is independent of fingerprint
|
|
1799
|
+
isValid = cpuTicket !== null && isMemValid;
|
|
1800
|
+
if (isValid) ticket = cpuTicket; // Le ticket est le même, on le réutilise
|
|
1801
|
+
this._log('Combined CPU+Memory challenge verification', {
|
|
1802
|
+
cpuValid: cpuTicket !== null,
|
|
1803
|
+
memValid: isMemValid,
|
|
1804
|
+
isValid
|
|
1805
|
+
});
|
|
1806
|
+
}
|
|
1866
1807
|
}
|
|
1867
1808
|
} else {
|
|
1868
1809
|
this._log('Challenge context not found or expired', { pow_nonce });
|
|
@@ -1878,7 +1819,7 @@ export class FingerprintEngine {
|
|
|
1878
1819
|
|
|
1879
1820
|
// NOUVELLE LOGIQUE DE REDIRECTION (plus robuste)
|
|
1880
1821
|
// 1. On part du chemin original stocké, qui peut contenir des query params.
|
|
1881
|
-
const originalUrl = new URL(challengeContext?.originalPath || path, `http://${requestContext.headers.host || 'localhost'}`);
|
|
1822
|
+
const originalUrl = new URL(challengeContext?.originalPath || requestContext.path, `http://${requestContext.headers.host || 'localhost'}`);
|
|
1882
1823
|
// 2. On crée un nouvel objet de paramètres à partir de la requête entrante (qui contient les solutions ET les params originaux).
|
|
1883
1824
|
const finalSearchParams = new URLSearchParams(requestContext.query);
|
|
1884
1825
|
|
|
@@ -1888,11 +1829,16 @@ export class FingerprintEngine {
|
|
|
1888
1829
|
finalSearchParams.delete('pow_solution');
|
|
1889
1830
|
finalSearchParams.delete('pow_solution_cpu');
|
|
1890
1831
|
finalSearchParams.delete('pow_solution_mem');
|
|
1832
|
+
finalSearchParams.delete('pow_fp'); // Ne pas oublier de nettoyer le fingerprint
|
|
1833
|
+
// NOUVEAU: Nettoyer aussi les paramètres des challenges d'optimisation et de travail utile
|
|
1834
|
+
finalSearchParams.delete('pow_solution_population');
|
|
1835
|
+
finalSearchParams.delete('pow_solution_work_result');
|
|
1836
|
+
finalSearchParams.delete('pow_problem_id');
|
|
1891
1837
|
|
|
1892
1838
|
// 4. On reconstruit le chemin final.
|
|
1893
1839
|
const finalQueryString = finalSearchParams.toString();
|
|
1894
1840
|
const finalRedirectPath = finalQueryString ? `${originalUrl.pathname}?${finalQueryString}` : originalUrl.pathname;
|
|
1895
|
-
this._log('Redirecting to clean path', { finalRedirectPath });
|
|
1841
|
+
this._log('Redirecting to clean path', { finalRedirectPath, cookieMaxAge: finalTtl });
|
|
1896
1842
|
return {
|
|
1897
1843
|
action: 'redirect',
|
|
1898
1844
|
path: finalRedirectPath,
|
|
@@ -1900,8 +1846,8 @@ export class FingerprintEngine {
|
|
|
1900
1846
|
vector: { challenge_solved: 100 },
|
|
1901
1847
|
cookie: {
|
|
1902
1848
|
name: 'pow_clearance',
|
|
1903
|
-
value: ticket,
|
|
1904
|
-
options: { httpOnly: true, secure: this.isProduction, maxAge: finalTtl }
|
|
1849
|
+
value: ticket, // The ticket itself
|
|
1850
|
+
options: { httpOnly: true, secure: this.isProduction, maxAge: finalTtl } // Options for setting the cookie
|
|
1905
1851
|
}
|
|
1906
1852
|
};
|
|
1907
1853
|
} else {
|
|
@@ -1910,7 +1856,68 @@ export class FingerprintEngine {
|
|
|
1910
1856
|
this._log('Challenge solution invalid', { pow_nonce });
|
|
1911
1857
|
suspicionVector.honeypotScore = 100; // Invalid solution is a strong bot signal.
|
|
1912
1858
|
finalScore = this.calculateFinalScore(suspicionVector);
|
|
1859
|
+
// --- FIX: After invalidating a solution, immediately check if the new score triggers a block ---
|
|
1860
|
+
const newBlockThreshold = thresholds.block ?? 95;
|
|
1861
|
+
if (finalScore >= newBlockThreshold) {
|
|
1862
|
+
this._log('Request blocked after invalid challenge solution', { finalScore, newBlockThreshold });
|
|
1863
|
+
return { action: 'block', status: 404, body: 'Forbidden', score: finalScore, vector: suspicionVector };
|
|
1864
|
+
}
|
|
1865
|
+
// If not blocked, the request will proceed to be re-challenged.
|
|
1866
|
+
}
|
|
1867
|
+
} else if (pow_nonce && pow_type === 'optimization_task' && pow_solution_population) {
|
|
1868
|
+
this._log('Optimization task solution submitted', { pow_nonce });
|
|
1869
|
+
const challengeContext = await store.get(`secret:${pow_nonce}`);
|
|
1870
|
+
let isValid = false;
|
|
1871
|
+
|
|
1872
|
+
if (challengeContext?.optimizationProblem) {
|
|
1873
|
+
try {
|
|
1874
|
+
const submittedChromosomes = JSON.parse(pow_solution_population);
|
|
1875
|
+
// Vérification simple : le client a-t-il renvoyé le bon nombre de solutions ?
|
|
1876
|
+
if (Array.isArray(submittedChromosomes) && submittedChromosomes.length === challengeContext.optimizationProblem.population.length) {
|
|
1877
|
+
// Le serveur recalcule la fitness pour la nouvelle population.
|
|
1878
|
+
const fitnessFunction = Optimization.Operators.createFullSecurityConfigEvaluator({ trafficData: challengeContext.optimizationProblem.trafficData });
|
|
1879
|
+
const newPopulation = submittedChromosomes.map(chromosome => ({ chromosome, fitness: fitnessFunction(chromosome) }));
|
|
1880
|
+
|
|
1881
|
+
// On met à jour le problème principal avec la nouvelle population.
|
|
1882
|
+
challengeContext.optimizationProblem.population = newPopulation;
|
|
1883
|
+
await store.set(`device:${deviceId}`, deviceData); // Sauvegarde l'état mis à jour
|
|
1884
|
+
isValid = true;
|
|
1885
|
+
}
|
|
1886
|
+
} catch (e) {
|
|
1887
|
+
this._log('Error parsing optimization solution', { error: e.message });
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
if (isValid) {
|
|
1892
|
+
await store.delete(`secret:${pow_nonce}`);
|
|
1893
|
+
// La solution est valide, on accorde un ticket et on redirige.
|
|
1894
|
+
const ticket = "valid_ticket_placeholder"; // Générer un vrai ticket ici
|
|
1895
|
+
return { action: 'redirect', path: path, score: 0, vector: { challenge_solved: 100 }, cookie: { name: 'pow_clearance', value: ticket, options: { httpOnly: true, secure: this.isProduction, maxAge: 60000 } } };
|
|
1896
|
+
} else {
|
|
1897
|
+
this._log('Optimization task solution invalid', { pow_nonce });
|
|
1898
|
+
suspicionVector.honeypotScore = 100;
|
|
1899
|
+
finalScore = this.calculateFinalScore(suspicionVector);
|
|
1913
1900
|
}
|
|
1901
|
+
} else if (pow_nonce && pow_type === 'useful_work_task' && pow_solution_work_result && pow_problem_id) {
|
|
1902
|
+
this._log('Useful work solution submitted', { problemId: pow_problem_id });
|
|
1903
|
+
const challengeContext = await store.get(`secret:${pow_nonce}`);
|
|
1904
|
+
if (challengeContext) {
|
|
1905
|
+
try {
|
|
1906
|
+
const workResult = JSON.parse(pow_solution_work_result);
|
|
1907
|
+
problemManager.integrateSolution(pow_problem_id, workResult);
|
|
1908
|
+
|
|
1909
|
+
await store.delete(`secret:${pow_nonce}`);
|
|
1910
|
+
// Accorder un ticket de passage comme pour un PoW normal
|
|
1911
|
+
const ticket = "valid_ticket_placeholder"; // Générer un vrai ticket ici
|
|
1912
|
+
return { action: 'redirect', path: path, score: 0, vector: { challenge_solved: 100 }, cookie: { name: 'pow_clearance', value: ticket, options: { httpOnly: true, secure: this.isProduction, maxAge: 60000 } } };
|
|
1913
|
+
|
|
1914
|
+
} catch (e) {
|
|
1915
|
+
this._log('Error parsing useful work solution', { error: e.message });
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
// Si la validation échoue, on pénalise fortement
|
|
1919
|
+
suspicionVector.honeypotScore = 100;
|
|
1920
|
+
finalScore = this.calculateFinalScore(suspicionVector);
|
|
1914
1921
|
}
|
|
1915
1922
|
// --- FIN DE LA LOGIQUE DE PRIORITÉ ---
|
|
1916
1923
|
|
|
@@ -1918,7 +1925,7 @@ export class FingerprintEngine {
|
|
|
1918
1925
|
if (isBlocked) {
|
|
1919
1926
|
this._log('Request blocked - score exceeded block threshold', { finalScore, blockThreshold });
|
|
1920
1927
|
if (onDeviceCompromised) {
|
|
1921
|
-
onDeviceCompromised({ deviceId:
|
|
1928
|
+
onDeviceCompromised({ deviceId: deviceId, clientIp, reason: 'Score exceeded block threshold', score: finalScore, vector: suspicionVector });
|
|
1922
1929
|
}
|
|
1923
1930
|
return { action: 'block', status: 404, body: 'Forbidden', score: finalScore, vector: suspicionVector };
|
|
1924
1931
|
}
|
|
@@ -1930,7 +1937,7 @@ export class FingerprintEngine {
|
|
|
1930
1937
|
this._log('Honeypot trap URL triggered - condemning device', { path, deviceId });
|
|
1931
1938
|
deviceData.condemned = true; // This device is a bot. Condemn it.
|
|
1932
1939
|
if (onDeviceCompromised) {
|
|
1933
|
-
onDeviceCompromised({ deviceId:
|
|
1940
|
+
onDeviceCompromised({ deviceId: deviceId, clientIp, reason: 'Triggered signed honeypot trap URL', score: 100, vector: { honeypotScore: 100 } });
|
|
1934
1941
|
}
|
|
1935
1942
|
if (logger) {
|
|
1936
1943
|
logger({ type: 'trap_triggered', deviceId: cookies?.device_id, score: 100, path: path, timestamp: Date.now() });
|
|
@@ -1961,20 +1968,32 @@ export class FingerprintEngine {
|
|
|
1961
1968
|
const nonce = crypto.randomBytes(16).toString("hex");
|
|
1962
1969
|
const clientSecret = crypto.randomBytes(16).toString("hex");
|
|
1963
1970
|
|
|
1964
|
-
//
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1971
|
+
// Pour les scores élevés, on choisit aléatoirement entre un challenge de travail utile et un PoW classique.
|
|
1972
|
+
// Cela rend l'automatisation plus difficile pour un attaquant.
|
|
1973
|
+
if (isSuspicious && this.securityConfig.enableUsefulWork && Math.random() > 0.5) {
|
|
1974
|
+
this._log('Issuing a useful work challenge', { finalScore });
|
|
1975
|
+
|
|
1976
|
+
const { problemId, task } = problemManager.dispatchWork(suspicionFactor);
|
|
1977
|
+
|
|
1978
|
+
await store.set(`secret:${nonce}`, { clientSecret, originalPath: path }, 300);
|
|
1968
1979
|
|
|
1969
|
-
|
|
1970
|
-
|
|
1980
|
+
const challengePayload = {
|
|
1981
|
+
challenge: {
|
|
1982
|
+
type: 'useful_work_task',
|
|
1983
|
+
nonce: nonce,
|
|
1984
|
+
clientSecret: clientSecret,
|
|
1985
|
+
usefulWorkTask: { problemId, task }
|
|
1986
|
+
}
|
|
1987
|
+
};
|
|
1988
|
+
return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: challengePayload };
|
|
1989
|
+
} else if (isSuspicious) { // Pour les scores bas/moyens ou si le travail utile n'est pas choisi
|
|
1971
1990
|
// Generate some trap URLs to embed in the challenge page.
|
|
1972
1991
|
// These links are visually hidden but present in the DOM to trap bots.
|
|
1973
1992
|
const trapUrls = Array.from({ length: 3 }, () => generateTrapUrl(nonce));
|
|
1974
1993
|
const trapLinksHtml = trapUrls.map(url => `<a href="${url}" tabindex="-1">config</a>`).join(' ');
|
|
1975
1994
|
|
|
1976
|
-
|
|
1977
|
-
const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path);
|
|
1995
|
+
// On passe la configuration pour que la difficulté soit calculée correctement.
|
|
1996
|
+
const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path, this.securityConfig);
|
|
1978
1997
|
|
|
1979
1998
|
// La difficulté mémoire démarre à 0 et augmente seulement après un certain seuil de suspicion.
|
|
1980
1999
|
// Par exemple, elle ne commence à augmenter qu'à partir de 25% du chemin entre 'low' et 'high'.
|
|
@@ -1990,13 +2009,18 @@ export class FingerprintEngine {
|
|
|
1990
2009
|
memDifficulty,
|
|
1991
2010
|
cpuTarget: cpuChallengeDetails.target
|
|
1992
2011
|
});
|
|
2012
|
+
// (NOUVEAU) On stocke le fingerprint de la requête qui a déclenché le challenge. We call it via __internal to allow mocking.
|
|
2013
|
+
const originalFingerprint = requestContext.headers['x-device-fingerprint'] || __internal.getCompositeDeviceHash(requestContext);
|
|
1993
2014
|
|
|
1994
2015
|
// Store the entire challenge context with a short TTL (e.g., 5 minutes)
|
|
2016
|
+
const baseBlock = createCpuChallengeBaseBlock(nonce, clientSecret, originalFingerprint);
|
|
1995
2017
|
await store.set(`secret:${nonce}`, {
|
|
1996
2018
|
clientSecret,
|
|
1997
2019
|
cpuTarget: cpuChallengeDetails.target,
|
|
1998
2020
|
suspicionScore: finalScore, // *** FIX: Store the score that triggered the challenge ***
|
|
2021
|
+
fingerprint: originalFingerprint, // *** NOUVEAU ***
|
|
1999
2022
|
memDifficulty: memDifficulty,
|
|
2023
|
+
baseBlock: baseBlock, // *** NOUVEAU: Le bloc de base est stocké pour la vérification ***
|
|
2000
2024
|
originalPath: path, // *** FIX: Store the original path ***
|
|
2001
2025
|
}, this.securityConfig.challengeTtl || 300); // NOUVEAU: TTL configurable (5min par défaut)
|
|
2002
2026
|
|
|
@@ -2017,7 +2041,7 @@ export class FingerprintEngine {
|
|
|
2017
2041
|
}
|
|
2018
2042
|
|
|
2019
2043
|
// Check if the request is an API request to return a JSON challenge
|
|
2020
|
-
const isApi = requestContext.rawReq && this.securityConfig
|
|
2044
|
+
const isApi = requestContext.rawReq && this.securityConfig?.isApiRequest?.(requestContext.rawReq);
|
|
2021
2045
|
|
|
2022
2046
|
if (isApi) {
|
|
2023
2047
|
// For API clients, send a JSON response with challenge details.
|
|
@@ -2028,13 +2052,14 @@ export class FingerprintEngine {
|
|
|
2028
2052
|
clientSecret: clientSecret, // The client needs this to solve the challenge
|
|
2029
2053
|
cpuTarget: cpuChallengeDetails.target,
|
|
2030
2054
|
memDifficulty: memDifficulty,
|
|
2055
|
+
baseBlock: [...baseBlock], // Envoyer le buffer comme un tableau d'octets
|
|
2031
2056
|
}
|
|
2032
2057
|
};
|
|
2033
2058
|
this._log('API challenge response generated', { challengePayload });
|
|
2034
2059
|
return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: challengePayload };
|
|
2035
2060
|
} else {
|
|
2036
2061
|
// For browsers, send the HTML page.
|
|
2037
|
-
const trapContainer = `<div style="position:absolute;left:-9999px;top:-9999px;" aria-hidden="true">${trapLinksHtml}</div>`; const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp, clientSecret, this.securityConfig, trapContainer);
|
|
2062
|
+
const trapContainer = `<div style="position:absolute;left:-9999px;top:-9999px;" aria-hidden="true">${trapLinksHtml}</div>`; const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp, clientSecret, this.securityConfig, trapContainer, originalFingerprint);
|
|
2038
2063
|
this._log('Browser challenge page generated', {
|
|
2039
2064
|
pageLength: page.length,
|
|
2040
2065
|
hasTrapContainer: true
|
|
@@ -2110,6 +2135,176 @@ export class FingerprintEngine {
|
|
|
2110
2135
|
}
|
|
2111
2136
|
}
|
|
2112
2137
|
|
|
2138
|
+
const staticExtensions = new RegExp(
|
|
2139
|
+
"\\.(js|css|png|jpg|jpeg|gif|svg|mp3|webp|ico|woff|woff2|ttf|otf|map|json|manifest|webmanifest)$",
|
|
2140
|
+
"i",
|
|
2141
|
+
);
|
|
2142
|
+
const isStaticResource = (path) => staticExtensions.test(path);
|
|
2143
|
+
|
|
2144
|
+
|
|
2145
|
+
/**
|
|
2146
|
+
* Détermine le TTL optimal pour un ticket en utilisant un algorithme génétique multi-objectifs.
|
|
2147
|
+
* @param {number} suspicionScore - Le score de suspicion de la requête.
|
|
2148
|
+
* @returns {number} Le TTL optimal calculé en millisecondes.
|
|
2149
|
+
*/
|
|
2150
|
+
function determineOptimalTicketTtl(suspicionScore) {
|
|
2151
|
+
// Définir les bornes pour la durée de vie du ticket (5 minutes à 24 heures)
|
|
2152
|
+
const MIN_TTL = 300000;
|
|
2153
|
+
const MAX_TTL = 86400000;
|
|
2154
|
+
|
|
2155
|
+
const solverFunction = () => {
|
|
2156
|
+
const fitnessFunction = Optimization.Operators.createOptimalTtlEvaluator({ suspicionScore });
|
|
2157
|
+
|
|
2158
|
+
// Un "individu" est simplement une valeur de TTL en millisecondes.
|
|
2159
|
+
const createIndividual = () => MIN_TTL + Math.random() * (MAX_TTL - MIN_TTL);
|
|
2160
|
+
const crossover = (ttl1, ttl2) => (ttl1 + ttl2) / 2;
|
|
2161
|
+
const mutate = (ttl) => {
|
|
2162
|
+
const newTtl = ttl + (Math.random() - 0.5) * (MAX_TTL - MIN_TTL) * 0.1; // Mutation de +/- 10% max
|
|
2163
|
+
return Math.max(MIN_TTL, Math.min(MAX_TTL, newTtl));
|
|
2164
|
+
};
|
|
2165
|
+
|
|
2166
|
+
const paretoFront = Optimization.geneticAlgorithmMultiObjective(
|
|
2167
|
+
createIndividual,
|
|
2168
|
+
fitnessFunction,
|
|
2169
|
+
crossover,
|
|
2170
|
+
mutate,
|
|
2171
|
+
{
|
|
2172
|
+
generations: 40,
|
|
2173
|
+
populationSize: 30,
|
|
2174
|
+
}
|
|
2175
|
+
);
|
|
2176
|
+
|
|
2177
|
+
// Pour runMultiple, on doit retourner un objet avec une propriété "fitness" ou "energy".
|
|
2178
|
+
// Pour un front de Pareto, il n'y a pas de score unique. On choisit la meilleure solution
|
|
2179
|
+
// en fonction du score de suspicion et on lui assigne un score de 0 pour que runMultiple la sélectionne.
|
|
2180
|
+
if (!paretoFront || paretoFront.length === 0) {
|
|
2181
|
+
return { solution: null, fitness: Infinity };
|
|
2182
|
+
}
|
|
2183
|
+
|
|
2184
|
+
// Stratégie de sélection :
|
|
2185
|
+
// Pour un score faible (< 50), on privilégie la solution avec le plus grand TTL (minimise la friction).
|
|
2186
|
+
// Pour un score élevé (>= 50), on privilégie la solution avec le plus petit TTL (minimise le risque).
|
|
2187
|
+
let bestSolutionInFront;
|
|
2188
|
+
if (suspicionScore < 50) {
|
|
2189
|
+
bestSolutionInFront = paretoFront.reduce((max, p) => Math.max(max, p.solution), 0);
|
|
2190
|
+
} else {
|
|
2191
|
+
bestSolutionInFront = paretoFront.reduce((min, p) => Math.min(min, p.solution), Infinity);
|
|
2192
|
+
}
|
|
2193
|
+
return { solution: bestSolutionInFront, fitness: 0 }; // fitness=0 car on a déjà la meilleure solution du cycle.
|
|
2194
|
+
};
|
|
2195
|
+
|
|
2196
|
+
// On exécute le solveur 20 fois pour trouver une solution plus stable et robuste.
|
|
2197
|
+
const { bestResult } = Optimization.runMultiple(solverFunction, 20);
|
|
2198
|
+
|
|
2199
|
+
if (!bestResult || !bestResult.solution || bestResult.solution === Infinity) {
|
|
2200
|
+
// Fallback : si l'algo ne retourne rien, on applique une règle simple et sûre.
|
|
2201
|
+
return Math.max(MIN_TTL, MAX_TTL - (suspicionScore / 100) * MAX_TTL);
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
// runMultiple choisit le meilleur résultat sur la base du score (ici, 0).
|
|
2205
|
+
// La "meilleure" solution dépendra du cycle qui a trouvé le meilleur compromis.
|
|
2206
|
+
return Math.round(bestResult.solution);
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
/**
|
|
2210
|
+
* Vérifie si une chaîne de caractères contient des patterns d'injection connus.
|
|
2211
|
+
* @private
|
|
2212
|
+
* @param {string} str - La chaîne à vérifier.
|
|
2213
|
+
* @param {string[]} [typesToDetect=['sql', 'log4shell', 'ssti', 'xxe', 'traversal', 'rce']] - Les types d'injections à détecter.
|
|
2214
|
+
* @returns {boolean} - True si un pattern malveillant est détecté.
|
|
2215
|
+
*/
|
|
2216
|
+
function isMalicious(str, typesToDetect = Object.keys(injectionPatterns)) {
|
|
2217
|
+
if (typeof str !== 'string') return false;
|
|
2218
|
+
|
|
2219
|
+
for (const type of typesToDetect) {
|
|
2220
|
+
const regex = injectionPatterns[type];
|
|
2221
|
+
if (regex && regex.test(str)) {
|
|
2222
|
+
return true;
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
return false;
|
|
2227
|
+
}
|
|
2228
|
+
|
|
2229
|
+
// --- Middleware Proof-of-Work (Le péage) ---
|
|
2230
|
+
export { isMalicious };
|
|
2231
|
+
|
|
2232
|
+
/**
|
|
2233
|
+
* Returns a default list of security analyzers for honeypot detection.
|
|
2234
|
+
* This list can be used as a base and extended with custom rules.
|
|
2235
|
+
* Currently includes an XSS detection analyzer.
|
|
2236
|
+
* @returns {Array<Function>}
|
|
2237
|
+
*/
|
|
2238
|
+
export const default_analyzers = () => [
|
|
2239
|
+
// Analyzer for Cross-Site Scripting (XSS) detection.
|
|
2240
|
+
// It uses the 'xss' library, which should be installed by the user (`npm install xss`).
|
|
2241
|
+
// If 'xss' is not available, this analyzer will be safely ignored.
|
|
2242
|
+
xss_analyzer
|
|
2243
|
+
];
|
|
2244
|
+
|
|
2245
|
+
export const xss_analyzer = async (data) => {
|
|
2246
|
+
try {
|
|
2247
|
+
// Dynamically import the 'xss' library.
|
|
2248
|
+
// The module is loaded only once by Node's cache.
|
|
2249
|
+
const xss = (await import('xss')).default;
|
|
2250
|
+
const originalData = JSON.stringify(data);
|
|
2251
|
+
// If the sanitized string is different, it means malicious HTML/JS was found and removed.
|
|
2252
|
+
return xss(originalData) !== originalData;
|
|
2253
|
+
} catch (error) {
|
|
2254
|
+
// This catch block handles the case where the 'xss' module is not installed.
|
|
2255
|
+
if (error.code === 'ERR_MODULE_NOT_FOUND') {
|
|
2256
|
+
console.warn('[Fingerprint] Warning: The "xss" package is not installed. The default XSS analyzer is disabled. Run "npm install xss" to enable it.');
|
|
2257
|
+
// To avoid repeated warnings, we can replace this function with a no-op.
|
|
2258
|
+
this.isXssAnalyzerAvailable = false; // A flag to prevent future attempts.
|
|
2259
|
+
}
|
|
2260
|
+
return false; // In case of any error, we assume the data is not malicious.
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
/**
|
|
2264
|
+
* Returns a powerful WAF (Web Application Firewall) analyzer based on ModSecurity.
|
|
2265
|
+
* This analyzer is highly effective against a wide range of attacks (SQLi, XSS, RCE, etc.)
|
|
2266
|
+
* by using the OWASP Core Rule Set.
|
|
2267
|
+
*
|
|
2268
|
+
* **Note:** This is an optional and advanced feature.
|
|
2269
|
+
* 1. The user must install the package: `npm install modsecurity-nodejs`
|
|
2270
|
+
* 2. ModSecurity rules (like the OWASP CRS) must be available on the server.
|
|
2271
|
+
*
|
|
2272
|
+
* If the package is not installed, the analyzer will be safely ignored.
|
|
2273
|
+
*
|
|
2274
|
+
* @param {string} rulesPath - The path to the ModSecurity rules configuration file (e.g., `crs-setup.conf`).
|
|
2275
|
+
* @returns {Function} An analyzer function to be used in the `honeypot.analyzers` array.
|
|
2276
|
+
*/
|
|
2277
|
+
export const modsecurity_analyzer = (rulesPath) => {
|
|
2278
|
+
let wafInstance = null; // Singleton instance for the WAF
|
|
2279
|
+
|
|
2280
|
+
return async (data) => {
|
|
2281
|
+
if (!rulesPath) {
|
|
2282
|
+
console.warn('[Fingerprint] ModSecurity analyzer disabled: `rulesPath` is not provided.');
|
|
2283
|
+
return false;
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
try {
|
|
2287
|
+
if (!wafInstance) {
|
|
2288
|
+
// Dynamically import the library only when needed.
|
|
2289
|
+
const { ModSecurity } = await import('modsecurity-nodejs');
|
|
2290
|
+
wafInstance = new ModSecurity();
|
|
2291
|
+
wafInstance.init();
|
|
2292
|
+
wafInstance.addRules(rulesPath);
|
|
2293
|
+
console.log('[Fingerprint] ModSecurity WAF analyzer initialized successfully.');
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
// The `transaction` method checks the data against the loaded rules.
|
|
2297
|
+
// It returns `null` if no rules are matched, or an object with intervention details if a threat is found.
|
|
2298
|
+
const result = wafInstance.transaction(data);
|
|
2299
|
+
return result !== null; // A non-null result means a threat was detected.
|
|
2300
|
+
} catch (error) {
|
|
2301
|
+
if (error.code === 'ERR_MODULE_NOT_FOUND') {
|
|
2302
|
+
console.warn('[Fingerprint] Warning: "modsecurity-nodejs" is not installed. The WAF analyzer is disabled. Run "npm install modsecurity-nodejs" to enable it.');
|
|
2303
|
+
}
|
|
2304
|
+
return false; // Assume data is safe if any error occurs.
|
|
2305
|
+
}
|
|
2306
|
+
};
|
|
2307
|
+
};
|
|
2113
2308
|
/**
|
|
2114
2309
|
* Returns a default list of whitelisting rules for common and legitimate web crawlers.
|
|
2115
2310
|
* This list can be used as a base and extended with custom rules.
|
|
@@ -2228,9 +2423,8 @@ export const powMiddleware = (securityConfig) => {
|
|
|
2228
2423
|
|
|
2229
2424
|
// Provide a default for isApiRequest if not specified by the user.
|
|
2230
2425
|
// This makes API challenge handling work more seamlessly out-of-the-box.
|
|
2231
|
-
if (!securityConfig
|
|
2232
|
-
|
|
2233
|
-
securityConfig.thresholds.isApiRequest = (req) =>
|
|
2426
|
+
if (!securityConfig?.isApiRequest) {
|
|
2427
|
+
securityConfig.isApiRequest = (req) =>
|
|
2234
2428
|
req.headers?.accept?.includes('application/json');
|
|
2235
2429
|
}
|
|
2236
2430
|
|
|
@@ -2242,9 +2436,10 @@ export const powMiddleware = (securityConfig) => {
|
|
|
2242
2436
|
query: req.query,
|
|
2243
2437
|
body: req.body,
|
|
2244
2438
|
headers: req.headers,
|
|
2245
|
-
isStatic: isStaticResource(req.path),
|
|
2439
|
+
isStatic: securityConfig?.isStaticResource?.(req.path) || isStaticResource(req.path),
|
|
2246
2440
|
// Pass the original request object for the isApiRequest function
|
|
2247
2441
|
rawReq: req,
|
|
2442
|
+
requestTimestamp: Date.now(), // Timestamp de début de requête
|
|
2248
2443
|
// Add the newly required properties for full decoupling
|
|
2249
2444
|
rawHeaders: req.rawHeaders,
|
|
2250
2445
|
// Pass the raw request object for advanced inspection (e.g., JA3)
|
|
@@ -2306,6 +2501,7 @@ export const __internal = {
|
|
|
2306
2501
|
getBehaviorScore, // Expose for testing
|
|
2307
2502
|
getCrossLayerInconsistency, // Expose for testing
|
|
2308
2503
|
// Expose page generators for security testing
|
|
2504
|
+
getTimeInconsistencyScore,
|
|
2309
2505
|
generateCpuTargetChallengePage,
|
|
2310
2506
|
generateCombinedPoWChallengePage,
|
|
2311
2507
|
};
|