@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/README.md +19 -10
- package/fingerprint.builder.js +171 -160
- package/fingerprint.client.js +5 -0
- package/fingerprint.js +317 -113
- package/library.js +1727 -1694
- package/package.json +1 -1
- package/problem-manager.js +523 -423
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
|
-
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
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
|
|
243
|
+
* @returns {{ja3: string|null, ja4: string|null}} An object containing JA3 and JA4 hashes.
|
|
239
244
|
*/
|
|
240
|
-
function
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
257
|
+
ja3 = ja3FromHeader;
|
|
245
258
|
}
|
|
246
259
|
|
|
247
|
-
//
|
|
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 (!
|
|
250
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
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.
|
|
329
|
-
const ja3 =
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
726
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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 } =
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
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
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
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]
|
|
2904
|
-
console.log("[AutoTuning]
|
|
2905
|
-
console.log("[AutoTuning]
|
|
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
|
/**
|