@anonympins/fingerprint 0.2.3 → 0.3.0

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
@@ -1,7 +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
+ import { getProblemManager, problemManager } from "./problem-manager.js";
5
5
  import { Optimization } from "./library.js";
6
6
  import { cyrb53, FingerprintBuilder } from "./fingerprint.builder.js";
7
7
  import { readFileSync } from "node:fs";
@@ -47,7 +47,8 @@ const securityProfiles = {
47
47
  behaviorScore: 0.7,
48
48
  honeypotScore: 1.0,
49
49
  crossLayerInconsistencyScore: 0.4,
50
- timeInconsistencyScore: 0.9
50
+ timeInconsistencyScore: 0.9,
51
+ tlsSpoofingScore: 0.8 // NOUVEAU: Poids pour la détection de spoofing TLS
51
52
  },
52
53
  thresholds: { low: 20, medium: 45, high: 75, block: 95 },
53
54
  patterns: {
@@ -77,7 +78,8 @@ const securityProfiles = {
77
78
  behaviorScore: 0.8,
78
79
  honeypotScore: 1.0,
79
80
  crossLayerInconsistencyScore: 0.6,
80
- timeInconsistencyScore: 1.0
81
+ timeInconsistencyScore: 1.0,
82
+ tlsSpoofingScore: 1.0 // NOUVEAU: Plus agressif pour le spoofing TLS
81
83
  },
82
84
  thresholds: { low: 10, medium: 35, high: 65, block: 90 },
83
85
  patterns: {
@@ -108,7 +110,8 @@ const securityProfiles = {
108
110
  behaviorScore: 0.2, // Lower weight, as browser behavior is not applicable
109
111
  honeypotScore: 1.0,
110
112
  crossLayerInconsistencyScore: 0.5,
111
- timeInconsistencyScore: 0.8
113
+ timeInconsistencyScore: 0.8,
114
+ tlsSpoofingScore: 0.7 // NOUVEAU: Important pour les API
112
115
  },
113
116
  thresholds: { low: 25, medium: 50, high: 80, block: 95 },
114
117
  patterns: {
@@ -140,7 +143,8 @@ const securityProfiles = {
140
143
  behaviorScore: 0.5, // Less emphasis on complex interactions
141
144
  honeypotScore: 1.0, // Crucial for comment spam
142
145
  crossLayerInconsistencyScore: 0.4,
143
- timeInconsistencyScore: 0.8
146
+ timeInconsistencyScore: 0.8,
147
+ tlsSpoofingScore: 0.6 // NOUVEAU: Moins critique pour les blogs
144
148
  },
145
149
  thresholds: { low: 25, medium: 55, high: 80, block: 95 },
146
150
  patterns: {
@@ -174,7 +178,8 @@ const securityProfiles = {
174
178
  behaviorScore: 0.8, // Important for checkout/login forms
175
179
  honeypotScore: 1.0,
176
180
  crossLayerInconsistencyScore: 0.7,
177
- timeInconsistencyScore: 0.9
181
+ timeInconsistencyScore: 0.9,
182
+ tlsSpoofingScore: 0.9 // NOUVEAU: Très important pour l'e-commerce
178
183
  },
179
184
  thresholds: { low: 15, medium: 40, high: 70, block: 90 },
180
185
  patterns: {
@@ -231,48 +236,56 @@ const getPowSolverCode = () => {
231
236
  };
232
237
 
233
238
  /**
234
- * Calculates the JA3 fingerprint hash from the TLS Client Hello message.
235
- * JA3 is a more reliable way to identify client applications (e.g., a specific browser or a script)
236
- * based on the specifics of its TLS handshake.
239
+ * Extracts TLS fingerprints (JA3 and JA4) from request context.
240
+ * Prioritizes headers from reverse proxies (x-ja4-hash) and falls back to JA3 calculation
241
+ * from raw socket data if available.
237
242
  * @param {object} context - The request context, containing the raw request object.
238
- * @returns {string|null} The MD5 hash of the JA3 string, or null if it cannot be computed.
243
+ * @returns {{ja3: string|null, ja4: string|null}} An object containing JA3 and JA4 hashes.
239
244
  */
240
- function getJa3Hash(context) {
241
- // 1. Prefer the JA3 hash from a trusted reverse proxy (e.g., Nginx, Cloudflare).
242
- const ja3FromHeader = context.headers['x-ja3-hash'];
245
+ function getTlsFingerprint(context) {
246
+ let ja3 = null;
247
+ let ja4 = null;
248
+
249
+ // 1. Prefer JA4 hash from a trusted reverse proxy header.
250
+ const ja4FromHeader = context.headers['x-ja4-hash'];
251
+ if (ja4FromHeader) {
252
+ ja4 = ja4FromHeader;
253
+ }
254
+ // 2. Prefer JA3 hash from a trusted reverse proxy header.
255
+ const ja3FromHeader = context.headers['x-ja3-hash']; // Assuming a proxy might provide JA3 too
243
256
  if (ja3FromHeader) {
244
- return ja3FromHeader;
257
+ ja3 = ja3FromHeader;
245
258
  }
246
259
 
247
- // 2. Fallback to calculating from the raw socket if available (requires Node.js to handle TLS).
260
+ // 3. Fallback to calculating from the raw socket if available and if headers were not present.
248
261
  const clientHello = context.rawReq?.socket?.clientHello;
249
- if (!clientHello) {
250
- return null;
251
- }
252
-
253
- try {
254
- const { version, ciphers, extensions, ellipticCurves, ellipticCurvePointFormats } = clientHello;
262
+ if (clientHello && !ja3) { // Only calculate if ja3 is not already set
263
+ try {
264
+ const { version, ciphers, extensions, ellipticCurves, ellipticCurvePointFormats } = clientHello;
255
265
 
256
- // The official JA3 spec includes the TLS version.
257
- // Node.js provides it as a string like 'TLSv1.3', we need the corresponding decimal value.
258
- const tlsVersionMap = {
259
- 'TLSv1': 769, 'TLSv1.1': 770, 'TLSv1.2': 771, 'TLSv1.3': 772
260
- };
261
- const tlsVersionId = tlsVersionMap[version] || 0;
262
-
263
- const ja3String = [
264
- tlsVersionId,
265
- // The ciphers array from clientHello is an array of objects, not just IDs.
266
- Array.isArray(ciphers) ? ciphers.join('-') : '',
267
- extensions?.join('-') || '',
268
- ellipticCurves?.join('-') || '',
269
- ellipticCurvePointFormats?.join('-') || ''
270
- ].join(',');
271
-
272
- return crypto.createHash('md5').update(ja3String).digest('hex');
273
- } catch (e) {
274
- return null; // Could fail if clientHello structure is unexpected.
266
+ // The official JA3 spec includes the TLS version.
267
+ // Node.js provides it as a string like 'TLSv1.3', we need the corresponding decimal value.
268
+ const tlsVersionMap = {
269
+ 'TLSv1': 769, 'TLSv1.1': 770, 'TLSv1.2': 771, 'TLSv1.3': 772
270
+ };
271
+ const tlsVersionId = tlsVersionMap[version] || 0;
272
+
273
+ const ja3String = [
274
+ tlsVersionId,
275
+ // The ciphers array from clientHello is an array of objects, not just IDs.
276
+ Array.isArray(ciphers) ? ciphers.join('-') : '',
277
+ extensions?.join('-') || '',
278
+ ellipticCurves?.join('-') || '',
279
+ ellipticCurvePointFormats?.join('-') || ''
280
+ ].join(',');
281
+
282
+ ja3 = crypto.createHash('md5').update(ja3String).digest('hex');
283
+ } catch (e) {
284
+ // Could fail if clientHello structure is unexpected.
285
+ ja3 = null;
286
+ }
275
287
  }
288
+ return { ja3, ja4 };
276
289
  }
277
290
  /**
278
291
  * Creates a stable hash based on device characteristics, independent of the IP.
@@ -304,7 +317,7 @@ export function getDeviceHash(context) {
304
317
  return getCompositeDeviceHash(context);
305
318
  }
306
319
 
307
- export function getCompositeDeviceHash(context) {
320
+ function getCompositeDeviceHash(context) {
308
321
  const srv = new FingerprintBuilder();
309
322
 
310
323
  // Si un fingerprint client est fourni, on l'intègre comme un signal fort,
@@ -322,70 +335,95 @@ export function getCompositeDeviceHash(context) {
322
335
  // 1. SIGNAL FORT: User Agent (poids élevé)
323
336
  const ua = context.headers["user-agent"];
324
337
  if (ua) {
325
- srv.add("ua", ua);
338
+ srv.add("ua", ua); // User-Agent
326
339
  }
327
340
 
328
- // 2. SIGNAL FORT: JA3 TLS Fingerprint
329
- const ja3 = getJa3Hash(context);
341
+ // 2. SIGNAUX DE BAS NIVEAU (Transport & Réseau) - Très fiables si fournis par un proxy
342
+ const { ja3, ja4 } = getTlsFingerprint(context);
330
343
  if (ja3) srv.add("ja3", ja3);
344
+ if (ja4) srv.add("ja4", ja4);
345
+
346
+ const h2Fingerprint = context.headers['x-http2-fingerprint'];
347
+ if (h2Fingerprint) srv.add("h2", h2Fingerprint);
348
+
349
+ const tcpFingerprint = context.headers['x-tcp-fingerprint'];
350
+ if (tcpFingerprint) srv.add("tcp", tcpFingerprint);
351
+
352
+ // 3. SIGNAUX DE HAUT NIVEAU (Applicatif) Moins fiables, mais utiles pour la corroboration
353
+ const headersToCapture = {
354
+ "ch_ua": "sec-ch-ua",
355
+ "ch_platform": "sec-ch-ua-platform",
356
+ "ch_mobile": "sec-ch-ua-mobile",
357
+ "ch_model": "sec-ch-ua-model",
358
+ "ch_arch": "sec-ch-ua-arch",
359
+ "ch_bitness": "sec-ch-ua-bitness",
360
+ "upgrade_req": "upgrade-insecure-requests",
361
+ "accept_lang": "accept-language",
362
+ "accept_enc": "accept-encoding",
363
+ "accept": "accept"
364
+ };
331
365
 
332
- // 3. SIGNAL MOYEN: Client Hints (modern browsers)
333
- if (context.headers["sec-ch-ua"]) {
334
- srv.add("ch_ua", context.headers["sec-ch-ua"]);
335
- }
336
- if (context.headers["sec-ch-ua-platform"]) {
337
- srv.add("ch_platform", context.headers["sec-ch-ua-platform"]);
338
- }
339
- if (context.headers["sec-ch-ua-mobile"]) {
340
- srv.add("ch_mobile", context.headers["sec-ch-ua-mobile"]);
341
- }
342
- if (context.headers["sec-ch-ua-model"]) {
343
- srv.add("ch_model", context.headers["sec-ch-ua-model"]);
344
- }
345
- if (context.headers["sec-ch-ua-arch"]) {
346
- srv.add("ch_arch", context.headers["sec-ch-ua-arch"]);
347
- }
348
- if (context.headers["sec-ch-ua-bitness"]) {
349
- srv.add("ch_bitness", context.headers["sec-ch-ua-bitness"]);
366
+ for (const [key, headerName] of Object.entries(headersToCapture)) {
367
+ const headerValue = context.headers[headerName];
368
+ if (headerValue) {
369
+ srv.add(key, headerValue);
370
+ }
350
371
  }
351
372
 
352
- // 4. SIGNAL MOYEN: HTTP Version et protocole
373
+ // 4. SIGNAUX DE CONTEXTE (HTTP Version, Cookies)
353
374
  if (context.httpVersion) {
354
375
  srv.add("http_ver", context.httpVersion);
355
376
  }
356
- if (context.headers["upgrade-insecure-requests"]) {
357
- srv.add("upgrade", context.headers["upgrade-insecure-requests"]);
358
- }
359
-
360
- // 11. SIGNAL AVANCÉ: Cookies (si disponible)
361
377
  if (context.cookies) {
362
378
  const cookieKeys = Object.keys(context.cookies).sort().join(',');
363
- srv.add("cookie_keys", cookieKeys);
364
- }
365
-
366
- // 12. SIGNAL AVANCÉ: Format de la requête
367
- if (context.rawHeaders) {
368
- // Vérifier des headers spécifiques qui indiquent le client
369
- const clientHeaders = ['x-requested-with', 'x-forwarded-for', 'x-real-ip', 'cf-connecting-ip'];
370
- clientHeaders.forEach(h => {
371
- if (context.headers[h]) {
372
- srv.add(h.replace(/-/g, '_'), context.headers[h]);
373
- }
374
- });
375
- }
376
-
377
- // 13. OPTIONNEL: IP (version simplifiée pour les réseaux partagés)
378
- // Ne pas inclure l'IP complète, mais un hash du réseau /24 ou /16
379
- // pour détecter les changements de réseau tout en protégeant la vie privée
380
- const ip = context.clientIp || context.headers['x-forwarded-for']?.split(',')[0]?.trim();
381
- if (ip && isPrivateIp(ip)) {
382
- // Pour les IP privées, on peut prendre le /24
383
- const networkHash = hashNetwork(ip, 24);
384
- srv.add("network", networkHash);
379
+ if (cookieKeys) {
380
+ srv.add("cookie_keys", cookieKeys);
381
+ }
385
382
  }
386
383
 
387
384
  return srv.toString();
388
385
  }
386
+ export { getCompositeDeviceHash };
387
+
388
+ /**
389
+ * @private
390
+ * A knowledge base of known TLS (JA3) fingerprints for common browsers.
391
+ * This helps in detecting inconsistencies between the TLS layer and the HTTP User-Agent.
392
+ * The key is the JA3 hash, and the value is the browser family.
393
+ * This list is not exhaustive but covers many common cases.
394
+ */
395
+ const tlsFingerprintDb = {
396
+ // --- Chrome (Desktop) ---
397
+ 'e188a442b87f422c5a1e80b05399435b': 'Chrome', // Chrome 107, Windows 10
398
+ 'd8e35855049321c6042a4325c697858f': 'Chrome', // Chrome 114, Windows 11
399
+ 'a9f90958d44533748c139a5d1895b925': 'Chrome', // Chrome 116, macOS
400
+ '3b5379916d2b3882253c42885956a350': 'Chrome', // Chrome 124, Linux
401
+
402
+ // --- Chrome (Mobile) ---
403
+ '59822058c95c33d2d06e52f410855c8c': 'Chrome', // Chrome 120, Android 13
404
+
405
+ // --- Firefox (Desktop) ---
406
+ 'b386946a5a586163c7c533636b45c355': 'Firefox', // Firefox 102, Windows 10
407
+ '66236495a523c1785f8f3a105b248b11': 'Firefox', // Firefox 115, Windows 11
408
+ 'b73d470006575b5e35167a0b5a8540e2': 'Firefox', // Firefox 121, macOS
409
+ '8443d7562933834333943465d52363cf': 'Firefox', // Firefox 125, Linux
410
+
411
+ // --- Firefox (Mobile) ---
412
+ '02720628957d38c6111a18433abe833f': 'Firefox', // Firefox 125, Android 14
413
+
414
+ // --- Safari & iOS (Shared TLS Stack) ---
415
+ // On iOS, all browsers (Chrome, Firefox, etc.) must use WebKit, which uses Apple's TLS stack.
416
+ // Therefore, they all share the same JA3 fingerprint as Safari on that OS version.
417
+ 'b633f21d532d35967c8753c38536b4d3': 'Safari', // Safari 16, macOS
418
+ '4d7a28d5f55b359b69100a311013f03e': ['Safari', 'Chrome', 'Firefox'], // Safari 17, iOS 17 (and other browsers on iOS 17)
419
+ '8dd3d7532873575314df23c447543001': ['Safari', 'Chrome', 'Firefox'], // Safari 17.4, iOS 17.4
420
+
421
+ // --- Common Libraries & Bots (for spoofing detection) ---
422
+ '47344a349b75c4e82333475553b5f358': 'Python', // Python 3.10 `requests` library
423
+ 'b29587b8a143c42546133ad7704b3310': 'Go', // Go 1.19 `http` library
424
+ 'd435b5223b2884c5a832b842637e245f': 'Java', // Java 11 `HttpClient`
425
+ 'c72366b9551263d990b7fa574225332c': 'curl', // curl 7.81.0
426
+ };
389
427
 
390
428
  // Fonctions utilitaires
391
429
  function parseUserAgent(ua) {
@@ -522,7 +560,10 @@ export const verifyTspChallenge = (
522
560
  targetMaxDistance,
523
561
  cities,
524
562
  ) => {
525
- try {
563
+ // Input validation: ensure the solution is a non-empty string before trying to parse it.
564
+ if (typeof solutionPathJson !== 'string' || solutionPathJson.length === 0) return false;
565
+
566
+ try {
526
567
  const solutionPath = JSON.parse(solutionPathJson);
527
568
  if (!Array.isArray(solutionPath) || solutionPath.length !== numCities)
528
569
  return false;
@@ -703,6 +744,13 @@ export const verifyPoWAndGenerateTicket = (
703
744
  * The server performs the same calculation to validate.
704
745
  */
705
746
  export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret = '') => {
747
+ // Hard cap on memory difficulty to prevent DoS attacks from malicious clients
748
+ // submitting an arbitrarily large difficulty value.
749
+ const MAX_ALLOWED_MEM_DIFFICULTY = 128; // 128MB
750
+ if (difficulty > MAX_ALLOWED_MEM_DIFFICULTY) {
751
+ console.warn(`[Security] Memory PoW verification attempt with excessive difficulty: ${difficulty}MB. Denied.`);
752
+ return false;
753
+ }
706
754
  const size = difficulty * 1024 * 1024;
707
755
  const iterations = size / 16;
708
756
  const buffer = new Uint32Array(size / 4);
@@ -722,8 +770,9 @@ export const verifyMemoryPoW = (nonce, solution, difficulty = 16, clientSecret =
722
770
  return finalHash === parseInt(solution, 10);
723
771
  };
724
772
  export const isTicketValid = (ip, ticket) => {
725
- if (!ticket) return false;
726
- const [expiry, sig] = ticket.split(":");
773
+ // Input validation: ensure the ticket is a non-empty string with the correct format.
774
+ if (typeof ticket !== 'string' || !ticket.includes(':')) return false;
775
+ const [expiry, sig] = ticket.split(':');
727
776
  if (!expiry || !sig || Date.now() > parseInt(expiry, 10)) return false;
728
777
  const expectedSig = crypto
729
778
  .createHmac("sha256", getPowSecret())
@@ -895,6 +944,17 @@ function getBehaviorScore(context) {
895
944
  score += 40;
896
945
  }
897
946
 
947
+ // 3. (NOUVEAU) Analyse de la longueur de l'historique de navigation.
948
+ // Un historique court est suspect (nouvel onglet, bot), un historique long est un bon signe.
949
+ if (typeof metrics.historyLength === 'number') {
950
+ if (metrics.historyLength === 1) {
951
+ score += 15; // Légère pénalité pour un historique de session vierge.
952
+ } else if (metrics.historyLength >= 5) {
953
+ score -= 20; // Bonus : un historique long est un fort indicateur humain.
954
+ } else if (metrics.historyLength >= 2) {
955
+ score -= 10; // Petit bonus pour une navigation de base.
956
+ }
957
+ }
898
958
  // 3. Vérification de la plausibilité et de la distribution des métriques.
899
959
  // Un bot pourrait envoyer des valeurs aléatoires, mais elles ne suivront probablement pas
900
960
  // des distributions naturelles (comme la loi de Benford pour les premiers chiffres).
@@ -923,7 +983,7 @@ function getBehaviorScore(context) {
923
983
  if (keystrokeDeviation > 0.15) score += 40;
924
984
  }
925
985
 
926
- return { behaviorScore: Math.min(100, score) };
986
+ return { behaviorScore: Math.max(0, Math.min(100, score)) }; // Assure que le score reste entre 0 et 100
927
987
  } catch (e) {
928
988
  return { behaviorScore: 10 }; // En-tête malformé = légèrement suspect.
929
989
  }
@@ -1007,6 +1067,56 @@ function getCrossLayerInconsistency(context) {
1007
1067
  }
1008
1068
  }
1009
1069
 
1070
+ /**
1071
+ * Calcule un score d'incohérence entre les données du fingerprint TLS (JA3/JA4) et les en-têtes serveur (User-Agent).
1072
+ * Cela permet de détecter le spoofing de fingerprint TLS.
1073
+ * @param {object} context - Le contexte de la requête.
1074
+ * @returns {{tlsSpoofingScore: number}}
1075
+ */
1076
+ function getTlsSpoofingScore(context, getTlsFingerprintFn = getTlsFingerprint) {
1077
+ const { ja3, ja4 } = getTlsFingerprintFn(context) || { ja3: null, ja4: null }; // Defensive check
1078
+ const ua = context.headers["user-agent"] || '';
1079
+
1080
+ // 1. Penalize if a TLS fingerprint is present but the User-Agent is generic or missing.
1081
+ // This is a strong indicator of a non-browser client trying to look legitimate.
1082
+ if ((ja3 || ja4) && (!ua || ua.length < 10 || ua.toLowerCase().includes('python') || ua.toLowerCase().includes('curl'))) {
1083
+ return { tlsSpoofingScore: 50 };
1084
+ }
1085
+
1086
+ // 2. If no JA3 hash is available, we cannot perform the consistency check.
1087
+ if (ja3 && ua) {
1088
+ // Look up the expected browser family (or families) from our database.
1089
+ let expectedBrowsers = tlsFingerprintDb[ja3];
1090
+
1091
+ if (expectedBrowsers) {
1092
+ // Ensure it's always an array for consistent logic.
1093
+ if (!Array.isArray(expectedBrowsers)) {
1094
+ expectedBrowsers = [expectedBrowsers];
1095
+ }
1096
+
1097
+ // Parse the User-Agent to get the claimed browser.
1098
+ const { browser: claimedBrowser } = parseUserAgent(ua);
1099
+
1100
+ // Check if the claimed browser is one of the legitimate possibilities for this JA3 hash.
1101
+ // We use `some` to see if the claimed browser starts with any of the expected browser names.
1102
+ // (e.g., "Chrome/116" starts with "Chrome").
1103
+ const isMatch = expectedBrowsers.some(expected => claimedBrowser?.startsWith(expected));
1104
+
1105
+ if (claimedBrowser && !isMatch) {
1106
+ return { tlsSpoofingScore: 80 }; // High score for a clear mismatch.
1107
+ }
1108
+ }
1109
+ }
1110
+
1111
+ // If we reach here, either:
1112
+ // - No JA3 was available.
1113
+ // - The JA3 was not in our database (we can't make a decision).
1114
+ // - The JA3 and User-Agent were consistent.
1115
+ // In all these cases, the score is 0.
1116
+ return { tlsSpoofingScore: 0 };
1117
+ }
1118
+
1119
+
1010
1120
  /**
1011
1121
  * Calcule un score basé sur la détection explicite de frameworks d'automatisation.
1012
1122
  * @param {object} context - Le contexte de la requête.
@@ -1365,6 +1475,9 @@ export const getSuspicionVector = async (context, securityConfig) => {
1365
1475
  // On appelle getHoneypotScore ici pour que son résultat soit inclus dans le vecteur.
1366
1476
  const { honeypotScore } = getHoneypotScore(context, honeypotConfig);
1367
1477
 
1478
+ // NOUVEAU: On calcule le score de spoofing TLS.
1479
+ const { tlsSpoofingScore } = getTlsSpoofingScore(context);
1480
+
1368
1481
  // NOUVEAU: On appelle getBotScore pour détecter les marqueurs d'automatisation.
1369
1482
  const { botScore } = getBotScore(context);
1370
1483
 
@@ -1386,7 +1499,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
1386
1499
  deviceData.ips = new Set(deviceData.ips);
1387
1500
  }
1388
1501
  // Le vecteur de suspicion est maintenant complet.
1389
- return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore };
1502
+ return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore };
1390
1503
  };
1391
1504
 
1392
1505
  // A residential user can change networks (home, 4G, public wifi).
@@ -1687,9 +1800,45 @@ export class FingerprintEngine {
1687
1800
  this.securityConfig = securityConfig;
1688
1801
  this.isProduction = isProduction;
1689
1802
  this._allowlist = this._buildAllowlist();
1803
+ this._validateConfig(securityConfig); // Validate the configuration
1690
1804
  this.verbose = securityConfig.verbose || false;
1691
1805
  }
1692
1806
 
1807
+ /**
1808
+ * Validates the security configuration object to detect potential typos or missing essential keys.
1809
+ * @private
1810
+ * @param {object} config - The security configuration object.
1811
+ */
1812
+ _validateConfig(config) {
1813
+ if (!config) {
1814
+ console.warn('[Fingerprint] Warning: No securityConfig provided. Using default behaviors, which may not be secure.');
1815
+ return;
1816
+ }
1817
+
1818
+ const knownKeys = new Set([
1819
+ 'weights', 'thresholds', 'cpu', 'ticketMaxAge', 'challengeTtl',
1820
+ 'deviceIdCookieMaxAge', 'challengePagePath', 'verbose', 'patterns',
1821
+ 'honeypot', 'whitelist', 'isStaticResource', 'isApiRequest', 'logger',
1822
+ 'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices',
1823
+ 'similarityThreshold'
1824
+ ]);
1825
+
1826
+ // 1. Check for essential keys
1827
+ if (!config.weights) {
1828
+ console.warn('[Fingerprint] Warning: `securityConfig.weights` is not defined. Suspicion scores will be 0.');
1829
+ }
1830
+ if (!config.thresholds) {
1831
+ console.warn('[Fingerprint] Warning: `securityConfig.thresholds` is not defined. Challenges may not be issued correctly.');
1832
+ }
1833
+
1834
+ // 2. Check for unknown (potentially misspelled) keys
1835
+ for (const key in config) {
1836
+ if (!knownKeys.has(key)) {
1837
+ console.warn(`[Fingerprint] Warning: Unknown key '${key}' found in securityConfig. This might be a typo.`);
1838
+ }
1839
+ }
1840
+ }
1841
+
1693
1842
  _log(message, data = {}) {
1694
1843
  if (this.verbose) {
1695
1844
  console.log(`[FingerprintEngine] ${message}`, data);
@@ -1709,6 +1858,7 @@ export class FingerprintEngine {
1709
1858
  (suspicionVector.behaviorScore || 0) * (weights.behaviorScore || 0) +
1710
1859
  (suspicionVector.botScore || 0) * (weights.botScore || 0) + // Ajout du nouveau score
1711
1860
  (suspicionVector.crossLayerInconsistencyScore || 0) * (weights.crossLayerInconsistencyScore || 0) +
1861
+ (suspicionVector.tlsSpoofingScore || 0) * (weights.tlsSpoofingScore || 0) + // NOUVEAU: TLS Spoofing
1712
1862
  (suspicionVector.timeInconsistencyScore || 0) * (weights.timeInconsistencyScore || 0);
1713
1863
 
1714
1864
  return Math.min(100, score);
@@ -2094,7 +2244,7 @@ export class FingerprintEngine {
2094
2244
  // We compare the fingerprint of the request that triggered the challenge
2095
2245
  // with the fingerprint of the request that is submitting the solution.
2096
2246
  // They should be very similar.
2097
- similarity = FingerprintBuilder.compare(originalFingerprint, getCompositeDeviceHash(requestContext));
2247
+ similarity = FingerprintBuilder.compare(originalFingerprint, solverFingerprint);
2098
2248
  }
2099
2249
 
2100
2250
  if (similarity < similarityThreshold) {
@@ -2128,6 +2278,15 @@ export class FingerprintEngine {
2128
2278
  }
2129
2279
  } else {
2130
2280
  this._log('Challenge context not found or expired', { pow_nonce });
2281
+ // --- NOUVELLE MESURE DE SÉCURITÉ ---
2282
+ // Si un client soumet un nonce invalide ou expiré, c'est une tentative de probing.
2283
+ // On applique une pénalité maximale pour bloquer ou re-challenger lourdement.
2284
+ suspicionVector.honeypotScore = 100;
2285
+ finalScore = this.calculateFinalScore(suspicionVector);
2286
+ this._log('Invalid nonce submitted (probing attempt) - applying max penalty', { newFinalScore: finalScore });
2287
+ // La logique continue vers la section `if (isValid)` qui échouera,
2288
+ // puis le score élevé sera utilisé pour bloquer ou re-challenger.
2289
+ isValid = false; // On s'assure que la validation échoue.
2131
2290
  }
2132
2291
  if (isValid) {
2133
2292
  // La solution est valide. On supprime le secret et on redirige.
@@ -2225,7 +2384,7 @@ export class FingerprintEngine {
2225
2384
  if (challengeContext) {
2226
2385
  try {
2227
2386
  const workResult = JSON.parse(pow_solution_work_result);
2228
- problemManager.integrateSolution(pow_problem_id, workResult);
2387
+ getProblemManager(this.securityConfig.usefulWorkConfigPath).integrateSolution(pow_problem_id, workResult);
2229
2388
 
2230
2389
  await store.delete(`secret:${pow_nonce}`);
2231
2390
  // Accorder un ticket de passage comme pour un PoW normal
@@ -2305,10 +2464,13 @@ export class FingerprintEngine {
2305
2464
 
2306
2465
  // Pour les scores élevés, on choisit aléatoirement entre un challenge de travail utile et un PoW classique.
2307
2466
  // Cela rend l'automatisation plus difficile pour un attaquant.
2308
- if (isSuspicious && this.securityConfig.enableUsefulWork && Math.random() > 0.5) {
2467
+ // Utilisation de crypto pour un choix plus sécurisé.
2468
+ const shouldUseUsefulWork = this.securityConfig.enableUsefulWork && crypto.randomBytes(1).readUInt8(0) / 255 > 0.5;
2469
+
2470
+ if (isSuspicious && shouldUseUsefulWork) {
2309
2471
  this._log('Issuing a useful work challenge', { finalScore });
2310
2472
 
2311
- const { problemId, task } = problemManager.dispatchWork(suspicionFactor);
2473
+ const { problemId, task } = getProblemManager(this.securityConfig.usefulWorkConfigPath).dispatchWork(suspicionFactor);
2312
2474
 
2313
2475
  await store.set(`secret:${nonce}`, { clientSecret, originalPath: path }, 300);
2314
2476
 
@@ -2611,6 +2773,7 @@ export const xss_analyzer = async (data) => {
2611
2773
  */
2612
2774
  export const modsecurity_analyzer = (rulesPath) => {
2613
2775
  let wafInstance = null; // Singleton instance for the WAF
2776
+ let isModSecurityAvailable = true; // Flag specific to this analyzer instance
2614
2777
 
2615
2778
  return async (data) => {
2616
2779
  if (!rulesPath) {
@@ -2618,8 +2781,12 @@ export const modsecurity_analyzer = (rulesPath) => {
2618
2781
  return false;
2619
2782
  }
2620
2783
 
2784
+ if (!isModSecurityAvailable) {
2785
+ return false; // Skip if the module is known to be unavailable
2786
+ }
2787
+
2621
2788
  try {
2622
- if (!wafInstance) {
2789
+ if (!wafInstance && isModSecurityAvailable) {
2623
2790
  // Dynamically import the library only when needed.
2624
2791
  const { ModSecurity } = await import('modsecurity-nodejs');
2625
2792
  wafInstance = new ModSecurity();
@@ -2634,7 +2801,8 @@ export const modsecurity_analyzer = (rulesPath) => {
2634
2801
  return result !== null; // A non-null result means a threat was detected.
2635
2802
  } catch (error) {
2636
2803
  if (error.code === 'ERR_MODULE_NOT_FOUND') {
2637
- console.warn('[Fingerprint] Warning: "modsecurity-nodejs" is not installed. The WAF analyzer is disabled. Run "npm install modsecurity-nodejs" to enable it.');
2804
+ console.warn('[Fingerprint] Warning: "modsecurity-nodejs" is not installed. The WAF analyzer is now disabled. Run "npm install modsecurity-nodejs" to enable it.');
2805
+ isModSecurityAvailable = false; // Disable for future calls
2638
2806
  }
2639
2807
  return false; // Assume data is safe if any error occurs.
2640
2808
  }
@@ -2749,6 +2917,11 @@ export const default_whitelist = () => [
2749
2917
  export const powMiddleware = (securityConfig) => {
2750
2918
  const engine = new FingerprintEngine(securityConfig);
2751
2919
 
2920
+ // Initialize the problem manager with the configured path, if provided.
2921
+ if (securityConfig.enableUsefulWork && securityConfig.usefulWorkConfigPath) {
2922
+ getProblemManager(securityConfig.usefulWorkConfigPath, store); // This correctly initializes the singleton
2923
+ }
2924
+
2752
2925
  if (securityConfig.autotuning) {
2753
2926
  startThresholdAutoTuning({
2754
2927
  securityConfig: securityConfig,
@@ -2837,8 +3010,11 @@ export const __internal = {
2837
3010
  getCrossLayerInconsistency, // Expose for testing
2838
3011
  // Expose page generators for security testing
2839
3012
  getTimeInconsistencyScore,
3013
+ getTlsFingerprint, // NOUVEAU: Expose pour les tests
3014
+ getTlsSpoofingScore, // NOUVEAU: Expose pour les tests
2840
3015
  generateCpuTargetChallengePage,
2841
3016
  generateCombinedPoWChallengePage,
3017
+ problemManager, // Re-export the problemManager promise
2842
3018
  };
2843
3019
 
2844
3020
  // --- THRESHOLD AUTO-TUNING SECTION ---
@@ -2854,8 +3030,16 @@ let autoTuningJobId = null;
2854
3030
  * @param {number} maxDataPoints - The maximum number of data points to keep after an optimization cycle.
2855
3031
  */
2856
3032
  function runThresholdOptimization(securityConfig, trafficData, minDataPoints, maxDataPoints) {
2857
- if (trafficData.length < minDataPoints) {
3033
+ const highConfidenceLogs = trafficData.filter(log => log.type === 'challenge_solved' || log.type === 'trap_triggered').length;
3034
+ const highConfidenceRatio = trafficData.length > 0 ? highConfidenceLogs / trafficData.length : 0;
3035
+ const MIN_CONFIDENCE_RATIO = 0.05; // Exiger au moins 5% de signaux forts.
3036
+
3037
+ if (trafficData.length < minDataPoints || highConfidenceRatio < MIN_CONFIDENCE_RATIO) {
3038
+ if (trafficData.length < minDataPoints) {
2858
3039
  console.log(`[AutoTuning] Reporté : ${trafficData.length}/${minDataPoints} points de données.`);
3040
+ } else {
3041
+ console.log(`[AutoTuning] Reporté : Ratio de confiance insuffisant (${(highConfidenceRatio * 100).toFixed(2)}% < ${(MIN_CONFIDENCE_RATIO * 100).toFixed(2)}%).`);
3042
+ }
2859
3043
  return;
2860
3044
  }
2861
3045
 
@@ -2886,23 +3070,43 @@ function runThresholdOptimization(securityConfig, trafficData, minDataPoints, ma
2886
3070
  }
2887
3071
  }
2888
3072
 
2889
- // Appliquer la nouvelle configuration optimisée
3073
+ // --- NOUVEAU : Logique d'inertie pour l'application de la configuration ---
3074
+ // Au lieu d'appliquer directement la nouvelle configuration, on fait "glisser"
3075
+ // l'ancienne vers la nouvelle, avec une vélocité de changement maximale.
2890
3076
  const newConfig = bestSolution.solution;
3077
+ const MAX_CHANGE_VELOCITY = 0.15; // 15% de changement maximum par cycle
2891
3078
 
2892
- // S'assurer que les objets de configuration existent avant d'utiliser Object.assign
2893
- if (!securityConfig.thresholds) securityConfig.thresholds = {};
2894
- if (!securityConfig.weights) securityConfig.weights = {};
2895
- if (!securityConfig.patterns) securityConfig.patterns = {};
3079
+ /**
3080
+ * Met à jour un objet de configuration (ex: thresholds, weights) en douceur.
3081
+ * @param {object} currentConfig - La configuration actuelle à modifier.
3082
+ * @param {object} targetConfig - La configuration cible proposée par l'optimiseur.
3083
+ */
3084
+ const applyInertialUpdate = (currentConfig, targetConfig) => {
3085
+ if (!currentConfig || !targetConfig) return; // Vérifier aussi currentConfig
3086
+ for (const key in targetConfig) {
3087
+ if (Object.hasOwnProperty.call(currentConfig, key)) {
3088
+ const currentValue = currentConfig[key];
3089
+ const targetValue = targetConfig[key];
3090
+ const delta = targetValue - currentValue;
3091
+ const maxChange = Math.abs(currentValue * MAX_CHANGE_VELOCITY);
3092
+
3093
+ // Limite le changement à la vélocité maximale
3094
+ const change = Math.max(-maxChange, Math.min(maxChange, delta));
3095
+
3096
+ currentConfig[key] += change;
3097
+ }
3098
+ }
3099
+ };
2896
3100
 
2897
- Object.assign(securityConfig.thresholds, newConfig.thresholds || {});
2898
- Object.assign(securityConfig.weights, newConfig.weights || {});
2899
- Object.assign(securityConfig.patterns, newConfig.patterns || {});
3101
+ applyInertialUpdate(securityConfig.thresholds, newConfig.thresholds);
3102
+ applyInertialUpdate(securityConfig.weights, newConfig.weights);
3103
+ applyInertialUpdate(securityConfig.patterns, newConfig.patterns);
2900
3104
 
2901
3105
  console.log("[AutoTuning] Nouvelle configuration de sécurité optimisée appliquée.");
2902
3106
  console.log("[AutoTuning] Objectifs atteints :", { falsePositiveRate: bestSolution.objectives[0].toFixed(4), falseNegativeRate: bestSolution.objectives[1].toFixed(4) });
2903
- console.log("[AutoTuning] Seuils :", securityConfig.thresholds);
2904
- console.log("[AutoTuning] Poids :", securityConfig.weights);
2905
- console.log("[AutoTuning] Patterns :", securityConfig.patterns);
3107
+ console.log("[AutoTuning] Nouveaux seuils :", securityConfig.thresholds);
3108
+ console.log("[AutoTuning] Nouveaux poids :", securityConfig.weights);
3109
+ console.log("[AutoTuning] Nouveaux patterns :", securityConfig.patterns);
2906
3110
  }
2907
3111
 
2908
3112
  /**