@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/README.md +494 -493
- package/fingerprint.client.js +19 -8
- package/fingerprint.js +394 -144
- package/library.js +1577 -1577
- package/package.json +76 -75
- package/pow.solver.js +214 -0
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (
|
|
94
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
//
|
|
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 (
|
|
688
|
+
if (isMalicious(value)) return true;
|
|
542
689
|
} else if (typeof value === 'object' && value !== null) {
|
|
543
|
-
// For
|
|
544
|
-
//
|
|
545
|
-
if (
|
|
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 =
|
|
599
|
-
burstThreshold =
|
|
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
|
-
//
|
|
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
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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}";
|
|
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
|
|
1094
|
-
|
|
1095
|
-
|
|
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... (
|
|
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
|
|
1111
|
-
|
|
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:
|
|
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
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
-
|
|
1532
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|