@anonympins/fingerprint 0.1.0 → 0.1.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/fingerprint.js CHANGED
@@ -4,6 +4,9 @@ import { BlockList } from "node:net";
4
4
  import dns from "node:dns/promises";
5
5
  import { Optimization } from "./library.js";
6
6
  import { cyrb53, FingerprintBuilder } from "./fingerprint.builder.js";
7
+ import { readFileSync } from "node:fs";
8
+ import { fileURLToPath } from "node:url";
9
+ import { dirname, join } from "node:path";
7
10
  export { createRedisStore } from "./redis-store.js";
8
11
  export { createMongoDbStore } from "./mongodb-store.js";
9
12
 
@@ -19,6 +22,86 @@ const getPowSecret = () => {
19
22
  return secret || "fallback-dev-secret-32-chars-minimum";
20
23
  };
21
24
 
25
+ /**
26
+ * Loads the pow.solver.js content for inlining in HTML pages.
27
+ * @returns {string} The solver JavaScript code.
28
+ */
29
+ const getPowSolverCode = () => {
30
+ try {
31
+ const __filename = fileURLToPath(import.meta.url);
32
+ const __dirname = dirname(__filename);
33
+ const solverPath = join(__dirname, 'pow.solver.inline.js'); // Use the inline version
34
+ return readFileSync(solverPath, 'utf-8');
35
+ } catch (error) {
36
+ console.warn('Could not load pow.solver.js for inlining, using fallback inline code');
37
+ // Fallback inline code if file cannot be loaded
38
+ return `(function(global){
39
+ async function solveCpuTargetInline(clientIp, nonce, target, clientSecret, progressCallback){
40
+ const cpuTarget = BigInt(target);
41
+ let cpuSolution = 0;
42
+ while(true){
43
+ const msg = clientSecret ? clientIp+':'+nonce+':'+cpuSolution+':'+clientSecret : clientIp+':'+nonce+':'+cpuSolution;
44
+ const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg));
45
+ const hashHex = Array.from(new Uint8Array(buf)).map(b=>b.toString(16).padStart(2,'0')).join('');
46
+ if(BigInt('0x'+hashHex) < cpuTarget) break;
47
+ cpuSolution++;
48
+ if(cpuSolution % 100000 === 0) await new Promise(r=>setTimeout(r,0));
49
+ }
50
+ return cpuSolution;
51
+ }
52
+ async function solveMemory(seed, difficulty){
53
+ const size = difficulty * 1024 * 1024;
54
+ const buffer = new Uint32Array(size / 4);
55
+ let h = new TextEncoder().encode(seed).reduce((acc,v)=>acc+v,0);
56
+ for(let i=0;i<buffer.length;i++) buffer[i] = h = Math.imul(h^i,1597334677);
57
+ let solution = 0;
58
+ const iterations = size / 16;
59
+ let addr = buffer.length > 0 ? buffer[0] % buffer.length : 0;
60
+ for(let i=0;i<iterations;i++){
61
+ addr = buffer[addr] % buffer.length;
62
+ solution ^= addr;
63
+ }
64
+ return solution;
65
+ }
66
+ async function solveTsp(cities, targetMaxDistance){
67
+ function distance(c1,c2){return Math.sqrt(Math.pow(c1.x-c2.x,2)+Math.pow(c1.y-c2.y,2));}
68
+ function evaluatePathDistance(cities,path){
69
+ let total=0;
70
+ for(let i=0;i<path.length-1;i++) total+=distance(cities[path[i]],cities[path[i+1]]);
71
+ total+=distance(cities[path[path.length-1]],cities[path[0]]);
72
+ return total;
73
+ }
74
+ function solveTspNearestNeighbor(cities){
75
+ const n=cities.length;
76
+ if(n===0)return[];
77
+ let path=[0];
78
+ let visited=new Array(n).fill(false);
79
+ visited[0]=true;
80
+ for(let i=1;i<n;i++){
81
+ let nearest=-1, minDist=Infinity;
82
+ for(let j=0;j<n;j++){
83
+ if(!visited[j]){
84
+ const d=distance(cities[path[i-1]],cities[j]);
85
+ if(d<minDist){minDist=d;nearest=j;}
86
+ }
87
+ }
88
+ path.push(nearest);
89
+ visited[nearest]=true;
90
+ }
91
+ return path;
92
+ }
93
+ await new Promise(r=>setTimeout(r,10));
94
+ const solutionPath=solveTspNearestNeighbor(cities);
95
+ const solutionDistance=evaluatePathDistance(cities,solutionPath);
96
+ return{path:solutionPath,distance:solutionDistance};
97
+ }
98
+ global.solveCpuChallengeInline=solveCpuTargetInline;
99
+ global.solveMemoryChallenge=solveMemory;
100
+ global.solveTspChallenge=solveTsp;
101
+ })(typeof window!=='undefined'?window:global);`;
102
+ }
103
+ };
104
+
22
105
  /**
23
106
  * Calculates the JA3 fingerprint hash from the TLS Client Hello message.
24
107
  * JA3 is a more reliable way to identify client applications (e.g., a specific browser or a script)
@@ -81,24 +164,160 @@ export function getDeviceHash(context) {
81
164
  // Prioritize the rich client-side fingerprint if provided.
82
165
  const clientFp = context.headers['x-device-fingerprint'];
83
166
  if (clientFp && typeof clientFp === 'string' && clientFp.includes('cvs:')) {
84
- // Basic validation to ensure it looks like our client-side fingerprint.
85
167
  return clientFp;
86
168
  }
87
169
 
88
- // Fallback to server-side only fingerprinting if the header is missing.
89
170
  const srv = new FingerprintBuilder();
90
- srv.add("ua", context.headers["user-agent"]);
91
- if (context.headers["sec-ch-ua-platform"])
92
- srv.add("os", context.headers["sec-ch-ua-platform"]);
93
- if (context.headers["sec-ch-ua"]) srv.add("ch", context.headers["sec-ch-ua"]);
94
- // Add JA3 hash if available. This is a very strong signal.
171
+
172
+ // 1. SIGNAL FORT: User Agent (poids élevé)
173
+ const ua = context.headers["user-agent"];
174
+ if (ua) {
175
+ srv.add("ua", ua);
176
+ // Extraire des infos supplémentaires du UA
177
+ const uaParts = parseUserAgent(ua);
178
+ if (uaParts.browser) srv.add("browser", uaParts.browser);
179
+ if (uaParts.os) srv.add("os_version", uaParts.os);
180
+ if (uaParts.device) srv.add("device_type", uaParts.device);
181
+ }
182
+
183
+ // 2. SIGNAL FORT: JA3 TLS Fingerprint
95
184
  const ja3 = getJa3Hash(context);
96
185
  if (ja3) srv.add("ja3", ja3);
97
186
 
187
+ // 3. SIGNAL MOYEN: Client Hints (modern browsers)
188
+ if (context.headers["sec-ch-ua"]) {
189
+ srv.add("ch_ua", context.headers["sec-ch-ua"]);
190
+ }
191
+ if (context.headers["sec-ch-ua-platform"]) {
192
+ srv.add("ch_platform", context.headers["sec-ch-ua-platform"]);
193
+ }
194
+ if (context.headers["sec-ch-ua-mobile"]) {
195
+ srv.add("ch_mobile", context.headers["sec-ch-ua-mobile"]);
196
+ }
197
+ if (context.headers["sec-ch-ua-model"]) {
198
+ srv.add("ch_model", context.headers["sec-ch-ua-model"]);
199
+ }
200
+ if (context.headers["sec-ch-ua-arch"]) {
201
+ srv.add("ch_arch", context.headers["sec-ch-ua-arch"]);
202
+ }
203
+ if (context.headers["sec-ch-ua-bitness"]) {
204
+ srv.add("ch_bitness", context.headers["sec-ch-ua-bitness"]);
205
+ }
206
+
207
+ // 4. SIGNAL MOYEN: HTTP Version et protocole
208
+ if (context.httpVersion) {
209
+ srv.add("http_ver", context.httpVersion);
210
+ }
211
+ if (context.headers["upgrade-insecure-requests"]) {
212
+ srv.add("upgrade", context.headers["upgrade-insecure-requests"]);
213
+ }
214
+
215
+ // 10. SIGNAL FORT: Ordonnancement des headers
98
216
  srv.add("h_ord", getHeaderSignature(context));
217
+
218
+ // 11. SIGNAL AVANCÉ: Cookies (si disponible)
219
+ if (context.cookies) {
220
+ const cookieKeys = Object.keys(context.cookies).sort().join(',');
221
+ srv.add("cookie_keys", cookieKeys);
222
+ }
223
+
224
+ // 12. SIGNAL AVANCÉ: Format de la requête
225
+ if (context.rawHeaders) {
226
+ // Vérifier des headers spécifiques qui indiquent le client
227
+ const clientHeaders = ['x-requested-with', 'x-forwarded-for', 'x-real-ip', 'cf-connecting-ip'];
228
+ clientHeaders.forEach(h => {
229
+ if (context.headers[h]) {
230
+ srv.add(h.replace(/-/g, '_'), context.headers[h]);
231
+ }
232
+ });
233
+ }
234
+
235
+ // 13. OPTIONNEL: IP (version simplifiée pour les réseaux partagés)
236
+ // Ne pas inclure l'IP complète, mais un hash du réseau /24 ou /16
237
+ // pour détecter les changements de réseau tout en protégeant la vie privée
238
+ const ip = context.clientIp || context.headers['x-forwarded-for']?.split(',')[0]?.trim();
239
+ if (ip && isPrivateIp(ip)) {
240
+ // Pour les IP privées, on peut prendre le /24
241
+ const networkHash = hashNetwork(ip, 24);
242
+ srv.add("network", networkHash);
243
+ }
244
+
99
245
  return srv.toString();
100
246
  }
101
247
 
248
+ // Fonctions utilitaires
249
+ function parseUserAgent(ua) {
250
+ // Parser basique du User-Agent
251
+ const result = {};
252
+
253
+ // Détection du navigateur
254
+ if (ua.includes('Chrome') && !ua.includes('Edg')) {
255
+ result.browser = 'Chrome';
256
+ const match = ua.match(/Chrome\/(\d+)/);
257
+ if (match) result.browser += `/${match[1]}`;
258
+ } else if (ua.includes('Firefox')) {
259
+ result.browser = 'Firefox';
260
+ const match = ua.match(/Firefox\/(\d+)/);
261
+ if (match) result.browser += `/${match[1]}`;
262
+ } else if (ua.includes('Safari') && !ua.includes('Chrome')) {
263
+ result.browser = 'Safari';
264
+ const match = ua.match(/Version\/(\d+)/);
265
+ if (match) result.browser += `/${match[1]}`;
266
+ } else if (ua.includes('Edg')) {
267
+ result.browser = 'Edge';
268
+ const match = ua.match(/Edg\/(\d+)/);
269
+ if (match) result.browser += `/${match[1]}`;
270
+ }
271
+
272
+ // Détection de l'OS
273
+ if (ua.includes('Windows NT 10.0')) result.os = 'Windows 10';
274
+ else if (ua.includes('Windows NT 6.1')) result.os = 'Windows 7';
275
+ else if (ua.includes('Mac OS X')) result.os = 'macOS';
276
+ else if (ua.includes('Linux') && !ua.includes('Android')) result.os = 'Linux';
277
+ else if (ua.includes('Android')) result.os = 'Android';
278
+ else if (ua.includes('iPhone') || ua.includes('iPad')) result.os = 'iOS';
279
+
280
+ // Détection du type d'appareil
281
+ if (ua.includes('Mobile')) result.device = 'mobile';
282
+ else if (ua.includes('Tablet')) result.device = 'tablet';
283
+ else result.device = 'desktop';
284
+
285
+ return result;
286
+ }
287
+
288
+ function normalizeReferer(referer) {
289
+ try {
290
+ const url = new URL(referer);
291
+ return `${url.protocol}//${url.hostname}`;
292
+ } catch {
293
+ return referer;
294
+ }
295
+ }
296
+
297
+ function isPrivateIp(ip) {
298
+ // Vérifier si l'IP est privée
299
+ const parts = ip.split('.');
300
+ if (parts.length !== 4) return false;
301
+ const first = parseInt(parts[0]);
302
+ return (first === 10) || (first === 172 && parseInt(parts[1]) >= 16 && parseInt(parts[1]) <= 31) || (first === 192 && parseInt(parts[1]) === 168);
303
+ }
304
+
305
+ function hashNetwork(ip, prefix = 24) {
306
+ // Hash du réseau (masque /24 ou /16)
307
+ const parts = ip.split('.');
308
+ if (parts.length !== 4) return null;
309
+ const maskBytes = prefix / 8;
310
+ const network = parts.slice(0, maskBytes).join('.');
311
+ // Hash simple
312
+ let hash = 0;
313
+ for (let i = 0; i < network.length; i++) {
314
+ const char = network.charCodeAt(i);
315
+ hash = ((hash << 5) - hash) + char;
316
+ hash = hash & hash;
317
+ }
318
+ return hash.toString(16);
319
+ }
320
+
102
321
  /**
103
322
  * Generates the HTML content for a TSP (Traveling Salesperson Problem) challenge.
104
323
  * @param {string} nonce - Unique nonce for the challenge.
@@ -116,6 +335,7 @@ const generateTspChallenge = (
116
335
  path = "",
117
336
  ) => {
118
337
  const citiesJson = JSON.stringify(cities);
338
+ const solverCode = getPowSolverCode();
119
339
  return `
120
340
  <html>
121
341
  <head><title>Advanced Security Check (Level 3)</title></head>
@@ -123,66 +343,17 @@ const generateTspChallenge = (
123
343
  <h1>Ultimate Verification (Level 3)</h1>
124
344
  <p>Please solve this small optimization problem to prove you are human.</p>
125
345
  <div id="loader" style="margin:20px;">⚙️ Calculating route... (${numCities} cities)</div>
346
+ <script>${solverCode}</script>
126
347
  <script>
127
348
  const cities = ${citiesJson};
128
349
  const nonce = "${nonce}";
129
350
  const targetMaxDistance = ${targetMaxDistance};
130
351
 
131
- // Utility function to calculate the distance between two cities
132
- function distance(city1, city2) {
133
- return Math.sqrt(Math.pow(city1.x - city2.x, 2) + Math.pow(city1.y - city2.y, 2));
134
- }
135
-
136
- // Utility function to evaluate the total distance of a path
137
- function evaluatePathDistance(cities, path) {
138
- let totalDistance = 0;
139
- for (let i = 0; i < path.length - 1; i++) {
140
- totalDistance += distance(cities[path[i]], cities[path[i + 1]]);
141
- }
142
- totalDistance += distance(cities[path[path.length - 1]], cities[path[0]]); // Return to start
143
- return totalDistance;
144
- }
145
-
146
- // Solveur simple du TSP (heuristique du plus proche voisin)
147
- function solveTspNearestNeighbor(cities) {
148
- const numCities = cities.length;
149
- if (numCities === 0) return [];
150
-
151
- let currentPath = [];
152
- let visited = new Array(numCities).fill(false);
153
-
154
- let currentCityIndex = 0; // Always start with the first city for reproducibility
155
- currentPath.push(currentCityIndex);
156
- visited[currentCityIndex] = true;
157
-
158
- for (let i = 1; i < numCities; i++) {
159
- let nearestCityIndex = -1;
160
- let minDistance = Infinity;
161
-
162
- for (let j = 0; j < numCities; j++) {
163
- if (!visited[j]) {
164
- const dist = distance(cities[currentCityIndex], cities[j]);
165
- if (dist < minDistance) {
166
- minDistance = dist;
167
- nearestCityIndex = j;
168
- }
169
- }
170
- }
171
- currentCityIndex = nearestCityIndex;
172
- currentPath.push(currentCityIndex);
173
- visited[currentCityIndex] = true;
174
- }
175
- return currentPath;
176
- }
177
-
178
352
  async function solve() {
179
- // To avoid freezing the browser, yield the thread from time to time
180
- await new Promise(resolve => setTimeout(resolve, 10));
181
- const solutionPath = solveTspNearestNeighbor(cities);
182
- const solutionDistance = evaluatePathDistance(cities, solutionPath);
183
-
184
- if (solutionDistance <= targetMaxDistance) {
185
- window.location.href = "${path}" + "?pow_type=tsp&pow_nonce=" + nonce + "&pow_solution=" + JSON.stringify(solutionPath);
353
+ const result = await window.solveTspChallenge(cities, targetMaxDistance);
354
+
355
+ if (result.distance <= targetMaxDistance) {
356
+ window.location.href = "${path}" + "?pow_type=tsp&pow_nonce=" + nonce + "&pow_solution=" + JSON.stringify(result.path);
186
357
  } else {
187
358
  document.getElementById('loader').innerText = "Error: Could not find a sufficient solution. Please try again.";
188
359
  }
@@ -507,42 +678,19 @@ function getHoneypotScore(context, honeypotConfig = {}) {
507
678
  }
508
679
  }
509
680
 
510
- // 3. Check for injection attempts in values
511
681
  if (detectInjections) {
512
- // Regex for common SQL injection patterns
513
- // WARNING: These are generic and may cause false positives.
514
- // Consider using a dedicated WAF library or more specific regex for your application.
515
- const sqlRegex = new RegExp(
516
- "('|\"|;|--|#|/\\*.*\\*/)|\\b(union|select|insert|update|delete|drop|truncate|from|where|and|or)\\b",
517
- "i"
518
- );
519
- // Regex for common NoSQL (MongoDB) injection patterns (e.g., keys starting with '$')
520
- // This looks for keys like "$where", "$ne", etc. in a stringified JSON.
521
- const nosqlKeyRegex = /"\$(where|ne|gt|lt|in|nin)":/;
522
- // Regex for common Remote Code Execution (RCE) patterns
523
- const rceRegex = new RegExp(
524
- // File traversal, command execution functions, and shell commands
525
- // Added process, child_process to catch Node.js specific RCE.
526
- "(\\.\\./|\\.\\.\\\\)|\\b(exec|system|shell_exec|passthru|popen|proc_open|eval|assert|require|include|process|child_process)(_once)?\\s*\\(|\\b(wget|curl|bash|sh|powershell|php)\\b",
527
- "i"
528
- );
529
- // Regex for Log4Shell (JNDI injection)
530
- const log4shellRegex = new RegExp("\\$\\{jndi:", "i");
531
- // Regex for Server-Side Template Injection (SSTI)
532
- const sstiRegex = new RegExp(
533
- "(\\{\\{|\\{%|#\\{)[^}]+(config|settings|self|class|application|request|session|process|env)", "i"
534
- );
535
-
682
+ // 3. Check for injection attempts in values using the centralized isMalicious function.
536
683
  const inspect = (obj) => {
537
684
  for (const key in obj) {
538
685
  if (Object.prototype.hasOwnProperty.call(obj, key)) {
539
686
  const value = obj[key];
540
687
  if (typeof value === 'string') {
541
- if (rceRegex.test(value) || sqlRegex.test(value) || log4shellRegex.test(value) || sstiRegex.test(value)) return true;
688
+ if (isMalicious(value)) return true;
542
689
  } else if (typeof value === 'object' && value !== null) {
543
- // For NoSQL, we check the stringified version of the object to find keys like "$gt"
544
- // This is more accurate when done on the object itself.
545
- if (nosqlKeyRegex.test(JSON.stringify(value))) return true;
690
+ // For nested objects (like in NoSQL injections), we stringify them once
691
+ // to check for malicious patterns within their structure or values.
692
+ if (isMalicious(JSON.stringify(value))) return true;
693
+ // Then, we recurse to check individual string values inside.
546
694
  if (inspect(value)) return true;
547
695
  }
548
696
  }
@@ -595,8 +743,8 @@ function getRequestPatternScore(context, deviceData, patternConfig = {}) {
595
743
 
596
744
  // Default values for the pattern detection logic, which can be overridden by the auto-tuner.
597
745
  const {
598
- velocityThreshold = 200, velocityWeight = 30,
599
- burstThreshold = 500, burstWeight = 50,
746
+ velocityThreshold = 800, velocityWeight = 30,
747
+ burstThreshold = 1500, burstWeight = 50,
600
748
  scrapeThreshold = 1000, scrapeWeight = 20, scrapeBurstWeight = 40,
601
749
  historySize = 10,
602
750
  decayFactor = 0.9,
@@ -892,6 +1040,9 @@ async function getBehavioralIndicators(context, deviceData) {
892
1040
  * @returns {Promise<{historyScore: number, rotationScore: number, headerAnomalyScore: number, inconsistencyScore: number, honeypotScore: number}>}
893
1041
  */
894
1042
  export const getSuspicionVector = async (context, securityConfig) => {
1043
+ // On récupère la configuration du honeypot pour l'utiliser ici.
1044
+ const honeypotConfig = securityConfig.honeypot || {};
1045
+
895
1046
  const { deviceId, deviceData, consistencyScore, newCookie } = await resolveRequestIdentity(context, securityConfig);
896
1047
 
897
1048
  const clientIp = context.clientIp;
@@ -923,6 +1074,9 @@ export const getSuspicionVector = async (context, securityConfig) => {
923
1074
 
924
1075
  const { behaviorScore } = getBehaviorScore(context); // Appel de la fonction
925
1076
 
1077
+ // On appelle getHoneypotScore ici pour que son résultat soit inclus dans le vecteur.
1078
+ const { honeypotScore } = getHoneypotScore(context, honeypotConfig);
1079
+
926
1080
  const { requestPatternScore } = getRequestPatternScore(context, deviceData, securityConfig.patterns);
927
1081
 
928
1082
  // Save the updated device state to the store
@@ -934,8 +1088,8 @@ export const getSuspicionVector = async (context, securityConfig) => {
934
1088
  if (Array.isArray(deviceData.ips)) {
935
1089
  deviceData.ips = new Set(deviceData.ips);
936
1090
  }
937
- // Correction : Ajouter behaviorScore à l'objet retourné
938
- return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, requestPatternScore };
1091
+ // Le vecteur de suspicion est maintenant complet.
1092
+ return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, requestPatternScore };
939
1093
  };
940
1094
 
941
1095
  // A residential user can change networks (home, 4G, public wifi).
@@ -1040,27 +1194,23 @@ export function generateCpuTargetChallenge(
1040
1194
  */
1041
1195
  function generateCpuTargetChallengePage(challengeDetails, clientIp) {
1042
1196
  const { nonce, target, path } = challengeDetails;
1197
+ const solverCode = getPowSolverCode();
1043
1198
  return `
1044
1199
  <html><head><title>Security Check</title></head>
1045
1200
  <body style="font-family:sans-serif; text-align:center; padding-top:50px;">
1046
1201
  <h1>Please wait... (Level 1)</h1>
1047
1202
  <p>We are verifying that you are not a bot. This may take a few seconds.</p>
1048
1203
  <div id="loader" style="margin:20px;">⚙️ Performing CPU security calculation...</div>
1204
+ <script>${solverCode}</script>
1049
1205
  <script>
1050
1206
  async function solve() {
1051
- const target = BigInt("0x${target}");
1052
- let solution = 0;
1053
- while (true) {
1054
- const msg = "${clientIp}:${nonce}:" + solution;
1055
- const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg));
1056
- const hashHex = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
1057
- if (BigInt('0x' + hashHex) < target) {
1058
- window.location.href = "${path}?pow_type=cpu_target&pow_nonce=${nonce}&pow_solution=" + solution;
1059
- break;
1060
- }
1061
- solution++;
1062
- if (solution % 100000 === 0) await new Promise(r => setTimeout(r, 0));
1063
- }
1207
+ const clientIp = "${clientIp}";
1208
+ const nonce = "${nonce}";
1209
+ const cpuTarget = BigInt("0x${target}");
1210
+ const solution = await window.solveCpuChallengeInline(clientIp, nonce, cpuTarget, null, (progress) => {
1211
+ // Optional progress callback
1212
+ });
1213
+ window.location.href = "${path}?pow_type=cpu_target&pow_nonce=${nonce}&pow_solution=" + solution;
1064
1214
  }
1065
1215
  solve();
1066
1216
  </script>
@@ -1076,49 +1226,37 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
1076
1226
  */
1077
1227
  function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty, clientIp, clientSecret) {
1078
1228
  const { nonce, target, path } = cpuChallengeDetails;
1229
+ const solverCode = getPowSolverCode();
1079
1230
  return `
1080
1231
  <html><head><title>Advanced Security Check</title></head>
1081
1232
  <body style="font-family:sans-serif; text-align:center; padding-top:50px;">
1082
1233
  <h1>Enhanced Verification... (Level 2)</h1>
1083
1234
  <p>Your activity requires an additional security check. This may take a few moments.</p>
1084
1235
  <div id="loader" style="margin:20px;">⚙️ Initializing combined verification...</div>
1236
+ <script>${solverCode}</script>
1085
1237
  <script>
1086
1238
  async function solve() {
1087
1239
  const nonce = "${nonce}";
1088
1240
  const path = "${path}";
1089
- const clientSecret = "${clientSecret}"; // Secret is now available to the client
1241
+ const clientSecret = "${clientSecret}";
1242
+ const clientIp = "${clientIp}";
1243
+ const cpuTarget = BigInt("0x${target}");
1244
+ const memDifficulty = ${memoryDifficulty};
1090
1245
 
1091
1246
  // --- CPU Challenge ---
1092
1247
  document.getElementById('loader').innerText = '⚙️ Performing CPU security calculation...';
1093
- const cpuTarget = BigInt("0x${target}");
1094
- let cpuSolution = 0;
1095
- while (true) {
1096
- const msg = "${clientIp}:${nonce}:" + cpuSolution + ":" + clientSecret;
1097
- const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(msg));
1098
- const hashHex = Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
1099
- if (BigInt('0x' + hashHex) < cpuTarget) break;
1100
- cpuSolution++;
1101
- if (cpuSolution % 100000 === 0) await new Promise(r => setTimeout(r, 0));
1102
- }
1248
+ const cpuSolution = await window.solveCpuChallengeInline(clientIp, nonce, cpuTarget, clientSecret, (progress) => {
1249
+ // Optional progress callback
1250
+ });
1103
1251
 
1104
1252
  // --- Memory Challenge ---
1105
- document.getElementById('loader').innerText = '⚙️ Performing memory allocation and calculation... (${memoryDifficulty} MB)';
1253
+ document.getElementById('loader').innerText = '⚙️ Performing memory allocation and calculation... (' + memDifficulty + ' MB)';
1106
1254
  await new Promise(r => setTimeout(r, 10)); // Yield to update UI
1107
1255
 
1108
1256
  let memSolution = 0;
1109
1257
  try {
1110
- const size = ${memoryDifficulty} * 1024 * 1024;
1111
- const iterations = size / 16;
1112
- const buffer = new Uint32Array(size / 4);
1113
- const seed = nonce + ":" + clientSecret;
1114
- let h = new TextEncoder().encode(seed).reduce((acc, v) => acc + v, 0);
1115
- for (let i = 0; i < buffer.length; i++) {
1116
- buffer[i] = h = Math.imul(h ^ i, 1597334677);
1117
- }
1118
- for(let i = 0; i < iterations; i++) {
1119
- const addr = buffer[i % buffer.length] % buffer.length;
1120
- memSolution ^= buffer[addr];
1121
- }
1258
+ const memSeed = nonce + ":" + clientSecret;
1259
+ memSolution = await window.solveMemoryChallenge(memSeed, memDifficulty);
1122
1260
  } catch(e) {
1123
1261
  document.getElementById('loader').innerText = "Error: Insufficient memory. Please refresh.";
1124
1262
  return;
@@ -1166,7 +1304,7 @@ export function verifyCpuTargetPoWAndGenerateTicket(
1166
1304
  }
1167
1305
 
1168
1306
  const staticExtensions = new RegExp(
1169
- "\\.(js|css|png|jpg|jpeg|gif|svg|mp3|webp|ico|woff|woff2|ttf|otf|map)$",
1307
+ "\\.(js|css|png|jpg|jpeg|gif|svg|mp3|webp|ico|woff|woff2|ttf|otf|map|json|manifest)$",
1170
1308
  "i",
1171
1309
  );
1172
1310
  const isStaticResource = (path) => staticExtensions.test(path);
@@ -1236,6 +1374,27 @@ function determineOptimalTicketTtl(suspicionScore) {
1236
1374
  return bestResult.solution;
1237
1375
  }
1238
1376
 
1377
+ /**
1378
+ * Vérifie si une chaîne de caractères contient des patterns d'injection connus.
1379
+ * @param {string} str - La chaîne à vérifier.
1380
+ * @returns {boolean} - True si un pattern malveillant est détecté.
1381
+ * @private
1382
+ */
1383
+ function isMalicious(str) {
1384
+ // Regex pour les injections SQL et NoSQL de base
1385
+ // Ajout de la détection des injections basées sur le temps (SLEEP, BENCHMARK, WAITFOR) et d'autres commandes dangereuses.
1386
+ const injectionRegex = /(\$ne|' OR '1'='1|['";]\s*--|; ?(DROP|TRUNCATE|DELETE)|UNION SELECT|SLEEP\(|BENCHMARK\(|WAITFOR DELAY)/i;
1387
+ // Regex pour les injections plus avancées
1388
+ const log4ShellRegex = /\$\{jndi:(ldap|rmi|dns):/i;
1389
+ const sstiRegex = /\{\{.*\}\}|\{%.*%\}/; // Détecte les syntaxes de type Jinja2, Twig, etc.
1390
+ const xxeRegex = /<!ENTITY\s+.*SYSTEM/i;
1391
+ const pathTraversalRegex = /(\.\.\/|\.\.\\)/;
1392
+ // NOUVEAU : Regex pour les injections de commandes basiques.
1393
+ // Cible les séparateurs de commandes et les backticks d'exécution.
1394
+ const commandInjectionRegex = /(&&|\|\||;|\n|`)/;
1395
+
1396
+ return injectionRegex.test(str) || log4ShellRegex.test(str) || sstiRegex.test(str) || xxeRegex.test(str) || pathTraversalRegex.test(str) || commandInjectionRegex.test(str);
1397
+ }
1239
1398
 
1240
1399
  // --- Middleware Proof-of-Work (Le péage) ---
1241
1400
  export class FingerprintEngine {
@@ -1244,6 +1403,13 @@ export class FingerprintEngine {
1244
1403
  this.securityConfig = securityConfig;
1245
1404
  this.isProduction = isProduction;
1246
1405
  this._allowlist = this._buildAllowlist();
1406
+ this.verbose = securityConfig.verbose || false;
1407
+ }
1408
+
1409
+ _log(message, data = {}) {
1410
+ if (this.verbose) {
1411
+ console.log(`[FingerprintEngine] ${message}`, data);
1412
+ }
1247
1413
  }
1248
1414
  calculateFinalScore = function(suspicionVector) {
1249
1415
  const { weights } = this.securityConfig;
@@ -1361,12 +1527,17 @@ export class FingerprintEngine {
1361
1527
 
1362
1528
  const { clientIp = "unknown", path, cookies, query, isStatic } = requestContext;
1363
1529
  const { weights, thresholds, logger, onDeviceCompromised } = this.securityConfig;
1530
+
1531
+ this._log('Processing request', { clientIp, path, isStatic });
1532
+
1364
1533
  if (isStatic) {
1534
+ this._log('Static resource - skipping checks');
1365
1535
  return { action: 'next', score: 0, vector: {} };
1366
1536
  }
1367
1537
 
1368
1538
  // 1. Check static IP allowlist first for maximum performance.
1369
1539
  if (this._isIpInAllowlist(clientIp)) {
1540
+ this._log('IP in allowlist - allowing request', { clientIp });
1370
1541
  return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'allowlist' } };
1371
1542
  }
1372
1543
 
@@ -1385,6 +1556,7 @@ export class FingerprintEngine {
1385
1556
 
1386
1557
  // Check if the request is from a verified, whitelisted bot (e.g., Googlebot)
1387
1558
  if (await this._verifyWhitelistedBot(requestContext)) {
1559
+ this._log('Whitelisted bot verified - allowing request', { clientIp });
1388
1560
  return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'bot' } };
1389
1561
  }
1390
1562
 
@@ -1393,31 +1565,50 @@ export class FingerprintEngine {
1393
1565
  // avant même de recalculer le score de suspicion.
1394
1566
  const { pow_type, pow_solution, pow_solution_cpu, pow_solution_mem } = query;
1395
1567
  if (pow_nonce && (pow_solution || (pow_solution_cpu && pow_solution_mem))) {
1568
+ this._log('Challenge solution submitted', { pow_type, pow_nonce });
1569
+
1396
1570
  // On doit calculer le score de suspicion *avant* de valider le ticket,
1397
1571
  // car le TTL optimal en dépend.
1398
1572
  const preliminaryVector = await __internal.getSuspicionVector(requestContext, this.securityConfig);
1399
1573
  const preliminaryScore = this.calculateFinalScore(preliminaryVector);
1574
+
1575
+ this._log('Preliminary suspicion vector calculated', {
1576
+ vector: preliminaryVector,
1577
+ score: preliminaryScore
1578
+ });
1579
+
1400
1580
  let isValid = false;
1401
1581
  const challengeContext = await store.get(`secret:${pow_nonce}`);
1402
1582
  let ticket = null;
1403
1583
 
1404
1584
  if (challengeContext) {
1405
1585
  const optimalTtl = determineOptimalTicketTtl(preliminaryScore);
1586
+ this._log('Challenge context found, verifying solution', { optimalTtl });
1587
+
1406
1588
  if (pow_type === "cpu_target") {
1407
1589
  // On passe la durée de vie du ticket configurée
1408
1590
  ticket = verifyCpuTargetPoWAndGenerateTicket(clientIp, optimalTtl, pow_nonce, pow_solution, null, challengeContext.clientSecret, challengeContext.cpuTarget);
1409
1591
  isValid = ticket !== null;
1592
+ this._log('CPU target challenge verification', { isValid });
1410
1593
  } else if (pow_type === "cpu_mem") {
1411
1594
  const cpuTicket = verifyCpuTargetPoWAndGenerateTicket(clientIp, optimalTtl, pow_nonce, pow_solution_cpu, null, challengeContext.clientSecret, challengeContext.cpuTarget);
1412
1595
  const isMemValid = verifyMemoryPoW(pow_nonce, pow_solution_mem, challengeContext.memDifficulty, challengeContext.clientSecret);
1413
1596
  isValid = cpuTicket !== null && isMemValid;
1414
1597
  if (isValid) ticket = cpuTicket; // Le ticket est le même, on le réutilise
1598
+ this._log('Combined CPU+Memory challenge verification', {
1599
+ cpuValid: cpuTicket !== null,
1600
+ memValid: isMemValid,
1601
+ isValid
1602
+ });
1415
1603
  }
1604
+ } else {
1605
+ this._log('Challenge context not found or expired', { pow_nonce });
1416
1606
  }
1417
1607
 
1418
1608
  if (isValid) {
1419
1609
  // La solution est valide. On supprime le secret et on redirige.
1420
1610
  await store.delete(`secret:${pow_nonce}`);
1611
+ this._log('Challenge solution valid - issuing ticket', { ticketMaxAge: this.securityConfig.ticketMaxAge || 3600000 });
1421
1612
 
1422
1613
  if (logger) {
1423
1614
  logger({ type: 'challenge_solved', deviceId: cookies?.device_id, score: preliminaryScore, challengeType: pow_type, timestamp: Date.now() });
@@ -1442,6 +1633,8 @@ export class FingerprintEngine {
1442
1633
  // Si la solution est INVALIDE, on ne fait rien ici. La requête continuera son cours normal,
1443
1634
  // sera recalculée comme suspecte, et probablement bloquée ou re-challengée, ce qui est le comportement souhaité.
1444
1635
  // On pourrait même ajouter une pénalité ici si on le voulait.
1636
+ this._log('Challenge solution invalid', { reason: challengeContext ? 'Invalid solution' : 'Nonce not found or expired' });
1637
+
1445
1638
  if (logger && challengeContext) {
1446
1639
  logger({ type: 'challenge_failed', deviceId: cookies?.device_id, reason: 'Invalid PoW solution', timestamp: Date.now() });
1447
1640
  } else if (logger && !challengeContext) {
@@ -1453,32 +1646,44 @@ export class FingerprintEngine {
1453
1646
  // Resolve identity and check for persisted "condemned" status early.
1454
1647
  const { deviceId, deviceData, newCookie } = await resolveRequestIdentity(requestContext, this.securityConfig);
1455
1648
  const isNewDevice = !!newCookie;
1649
+
1650
+ this._log('Identity resolved', { deviceId, isNewDevice, hasDeviceData: !!deviceData });
1456
1651
 
1457
1652
  if (deviceData?.condemned) {
1653
+ this._log('Device condemned - blocking request', { deviceId });
1458
1654
  if (onDeviceCompromised) {
1459
1655
  onDeviceCompromised({ deviceId: cookies?.device_id, clientIp, reason: 'Previously condemned', score: 100, vector: { honeypotScore: 100 } });
1460
1656
  }
1461
- return { action: 'block', status: 403, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
1657
+ return { action: 'block', status: 404, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
1462
1658
  }
1463
1659
 
1464
1660
  // The engine now works with the context directly, no more rawReq dependency here.
1465
1661
  const suspicionVector = await __internal.getSuspicionVector(requestContext, this.securityConfig);
1466
- const { honeypotScore } = getHoneypotScore(requestContext, this.securityConfig.honeypot);
1467
- suspicionVector.honeypotScore = honeypotScore;
1468
- // Le behaviorScore est déjà dans le vecteur de suspicion via getSuspicionVector
1662
+ // honeypotScore et behaviorScore sont maintenant inclus directement dans le vecteur de suspicion.
1663
+
1664
+ this._log('Suspicion vector calculated', {
1665
+ vector: suspicionVector,
1666
+ weights: this.securityConfig.weights
1667
+ });
1469
1668
 
1470
1669
  let finalScore = this.calculateFinalScore(suspicionVector);
1670
+
1671
+ this._log('Final score calculated', { finalScore });
1471
1672
 
1472
1673
  // Si c'est un nouvel appareil, on lui impose un challenge de base, même si son score est bas.
1473
1674
  // Cela augmente le coût pour les bots qui tentent de simplement supprimer leurs cookies.
1474
1675
  // NOUVEAU : Cette logique est maintenant configurable.
1475
1676
  const challengeNewDevices = this.securityConfig.challengeNewDevices === true;
1476
1677
  if (isNewDevice && finalScore < thresholds.low) {
1678
+ this._log('New device - enforcing minimum challenge score', {
1679
+ originalScore: finalScore,
1680
+ enforcedScore: thresholds.low
1681
+ });
1477
1682
  finalScore = thresholds.low;
1478
1683
  }
1479
1684
 
1480
- const isBlocked = finalScore >= (thresholds.block || 95);
1481
-
1685
+ const blockThreshold = thresholds.block ?? 95;
1686
+ const isBlocked = finalScore >= blockThreshold;
1482
1687
 
1483
1688
  const isSuspiciousHigh = finalScore >= thresholds.high && !isBlocked;
1484
1689
  const isSuspiciousMedium = finalScore >= thresholds.medium;
@@ -1492,20 +1697,32 @@ export class FingerprintEngine {
1492
1697
  )
1493
1698
  : 0;
1494
1699
 
1700
+ this._log('Suspicion levels evaluated', {
1701
+ finalScore,
1702
+ isBlocked,
1703
+ isSuspiciousHigh,
1704
+ isSuspiciousMedium,
1705
+ isSuspicious,
1706
+ suspicionFactor,
1707
+ thresholds: { low: thresholds.low, medium: thresholds.medium, high: thresholds.high, block: blockThreshold }
1708
+ });
1709
+
1495
1710
  const powCookie = cookies?.pow_clearance;
1496
1711
 
1497
1712
  // If the action is to block, we should still include the score and vector for logging/testing.
1498
1713
  if (isBlocked) {
1714
+ this._log('Request blocked - score exceeded block threshold', { finalScore, blockThreshold });
1499
1715
  if (onDeviceCompromised) {
1500
1716
  onDeviceCompromised({ deviceId: cookies?.device_id, clientIp, reason: 'Score exceeded block threshold', score: finalScore, vector: suspicionVector });
1501
1717
  }
1502
- return { action: 'block', status: 403, body: 'Forbidden', score: finalScore, vector: suspicionVector };
1718
+ return { action: 'block', status: 404, body: 'Forbidden', score: finalScore, vector: suspicionVector };
1503
1719
  }
1504
1720
 
1505
1721
  // Honeypot: Check if the request is for a trap URL generated in a previous challenge.
1506
1722
  // This requires a nonce from a *previous* challenge, which we can look up via the device ID.
1507
1723
  const lastNonce = deviceData?.lastChallengeNonce;
1508
1724
  if (lastNonce && query.sig && verifyTrapUrl(path, query.sig, lastNonce)) {
1725
+ this._log('Honeypot trap URL triggered - condemning device', { path, deviceId });
1509
1726
  deviceData.condemned = true; // This device is a bot. Condemn it.
1510
1727
  if (onDeviceCompromised) {
1511
1728
  onDeviceCompromised({ deviceId: cookies?.device_id, clientIp, reason: 'Triggered signed honeypot trap URL', score: 100, vector: { honeypotScore: 100 } });
@@ -1514,22 +1731,26 @@ export class FingerprintEngine {
1514
1731
  logger({ type: 'trap_triggered', deviceId: cookies?.device_id, score: 100, path: path, timestamp: Date.now() });
1515
1732
  }
1516
1733
  await store.set(`device:${cookies.device_id}`, deviceData); // No TTL for condemned status
1517
- return { action: 'block', status: 403, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
1734
+ return { action: 'block', status: 404, score: 100, vector: { honeypotScore: 100 } };
1518
1735
  }
1519
1736
 
1520
1737
  if (isSuspicious && !isTicketValid(clientIp, powCookie)) {
1738
+ this._log('Suspicious request without valid ticket - issuing challenge', { finalScore, hasPowCookie: !!powCookie });
1739
+
1521
1740
  // Honeypot: Direct probing of challenge endpoints is highly suspicious.
1522
1741
  // A legitimate user only hits these endpoints via the challenge page itself.
1523
1742
  // If we see a pow_nonce on a request that IS suspicious but has no valid ticket,
1524
1743
  // AND it's not a legitimate response to a challenge we issued, it's a probe.
1525
1744
  const isChallengeResponse = query.pow_solution || (query.pow_solution_cpu && query.pow_solution_mem);
1526
1745
  if (pow_nonce && !isChallengeResponse) {
1746
+ this._log('Honeypot probe detected - blocking request', { path, pow_nonce });
1527
1747
  if (logger) {
1528
1748
  logger({ type: 'honeypot_probe', deviceId: cookies?.device_id, score: finalScore, path: path, timestamp: Date.now() });
1529
1749
  }
1530
1750
  suspicionVector.honeypotScore = 100; // Bot is probing. Max penalty.
1531
- const newFinalScore = finalScore - (honeypotScore * (weights.honeypotScore || 0)) + (100 * (weights.honeypotScore || 0));
1532
- return { action: 'block', status: 403, body: 'Forbidden', score: newFinalScore, vector: suspicionVector };
1751
+ // Recalculate the final score with the updated vector.
1752
+ const newFinalScore = this.calculateFinalScore(suspicionVector);
1753
+ return { action: 'block', status: 404, body: 'Forbidden', score: newFinalScore, vector: suspicionVector };
1533
1754
  }
1534
1755
 
1535
1756
  // --- SELECTION AND SENDING OF THE APPROPRIATE CHALLENGE ---
@@ -1559,6 +1780,13 @@ export class FingerprintEngine {
1559
1780
  const maxMemDifficulty = 48; // 48Mo pour les plus suspects
1560
1781
  const memDifficulty = Math.round(minMemDifficulty + memActivationFactor * (maxMemDifficulty - minMemDifficulty));
1561
1782
 
1783
+ this._log('Challenge parameters calculated', {
1784
+ suspicionFactor,
1785
+ memActivationFactor,
1786
+ memDifficulty,
1787
+ cpuTarget: cpuChallengeDetails.target
1788
+ });
1789
+
1562
1790
  // Store the entire challenge context with a short TTL (e.g., 5 minutes)
1563
1791
  await store.set(`secret:${nonce}`, {
1564
1792
  clientSecret,
@@ -1572,6 +1800,12 @@ export class FingerprintEngine {
1572
1800
  await store.set(`device:${deviceId}`, deviceData); // Utiliser le deviceId résolu, pas celui des cookies
1573
1801
  }
1574
1802
 
1803
+ this._log('Challenge issued', {
1804
+ nonce,
1805
+ challengeTtl: this.securityConfig.challengeTtl || 300,
1806
+ trapUrlsCount: trapUrls.length
1807
+ });
1808
+
1575
1809
  if (logger) {
1576
1810
  logger({ type: 'challenge_issued', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now() });
1577
1811
  }
@@ -1590,17 +1824,24 @@ export class FingerprintEngine {
1590
1824
  memDifficulty: memDifficulty,
1591
1825
  }
1592
1826
  };
1593
- return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 429, body: challengePayload };
1827
+ this._log('API challenge response generated', { challengePayload });
1828
+ return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: challengePayload };
1594
1829
  } else {
1595
1830
  // For browsers, send the HTML page.
1596
1831
  const trapContainer = `<div style="position:absolute;left:-9999px;top:-9999px;" aria-hidden="true">${trapLinksHtml}</div>`;
1597
1832
  const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp, clientSecret).replace('</body>', `${trapContainer}</body>`);
1598
- return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 429, body: page };
1833
+ this._log('Browser challenge page generated', {
1834
+ pageLength: page.length,
1835
+ hasTrapContainer: true
1836
+ });
1837
+ return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: page };
1599
1838
  }
1600
1839
  }
1601
1840
  }
1602
1841
 
1603
1842
  // Basic log for each non-static request that passed without a challenge
1843
+ this._log('Request passed - no challenge required', { finalScore, hasValidTicket: isTicketValid(clientIp, powCookie) });
1844
+
1604
1845
  if (logger) {
1605
1846
  logger({ type: 'request_passed', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now() });
1606
1847
  }
@@ -1780,6 +2021,14 @@ export const powMiddleware = (securityConfig) => {
1780
2021
  });
1781
2022
  }
1782
2023
 
2024
+ // Provide a default for isApiRequest if not specified by the user.
2025
+ // This makes API challenge handling work more seamlessly out-of-the-box.
2026
+ if (!securityConfig.thresholds?.isApiRequest) {
2027
+ if (!securityConfig.thresholds) securityConfig.thresholds = {};
2028
+ securityConfig.thresholds.isApiRequest = (req) =>
2029
+ req.headers?.accept?.includes('application/json');
2030
+ }
2031
+
1783
2032
  return async (req, res, next) => {
1784
2033
  const requestContext = {
1785
2034
  clientIp: req.ip || req.socket?.remoteAddress || "unknown",
@@ -1841,6 +2090,7 @@ export const powMiddleware = (securityConfig) => {
1841
2090
  */
1842
2091
  export const __internal = {
1843
2092
  getDeviceHash,
2093
+ isMalicious,
1844
2094
  getSuspicionVector,
1845
2095
  cyrb53, // Export for testing
1846
2096
  FingerprintBuilder, // Export for testing