@anonympins/fingerprint 0.1.4 → 0.2.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 +96 -36
- package/fingerprint.client.js +16 -0
- package/fingerprint.js +350 -147
- package/library.js +25 -19
- package/package.json +1 -1
- package/pow.solver.js +164 -1
package/README.md
CHANGED
|
@@ -18,14 +18,20 @@ The process unfolds in three steps:
|
|
|
18
18
|
* **Header Anomalies**: Missing `User-Agent`, `Accept-Language`, etc.
|
|
19
19
|
* **Device Behavior**: Rapid fingerprint changes (User-Agent rotation).
|
|
20
20
|
* **IP Behavior**: An excessive number of different devices seen from the same IP, or a single device using a large number of IPs (proxy rotation).
|
|
21
|
-
* **Inconsistency**: A low similarity score between the current fingerprint and the
|
|
21
|
+
* **Inconsistency**: A low similarity score between the current fingerprint and the one initially associated with the `device_id` (cookie theft detection).
|
|
22
|
+
* **Cross-Layer Inconsistency**: Mismatches between client-side data (e.g., OS reported by the browser) and server-side headers (e.g., `User-Agent`).
|
|
22
23
|
* **Request Patterns**: Repetitive, rapid-fire, or sequential requests typical of scraping bots. The parameters for detecting these patterns (e.g., request velocity, burst detection) are dynamically adjusted by the auto-tuner for optimal performance.
|
|
23
24
|
* **Honeypot Trap**: Detection of bots that automatically fill hidden form fields or probe for common but unused URL parameters (e.g., `?debug=true`).
|
|
24
25
|
3. **Dynamic Challenge**: If the suspicion score exceeds a certain threshold, a challenge is presented to the user. The difficulty and type of challenge depend on the score:
|
|
25
|
-
* **Low to Medium Suspicion**: A combined CPU and Memory Proof-of-Work (PoW) challenge is issued. The difficulty of both the CPU (hash calculation) and Memory (allocation and computation) components scales progressively with the suspicion score. For low scores, the memory challenge is negligible, making it primarily a CPU task.
|
|
26
|
+
* **Low to Medium Suspicion**: A combined **CPU and Memory Proof-of-Work (PoW)** challenge is issued. The difficulty of both the CPU (hash calculation) and Memory (allocation and computation) components scales progressively with the suspicion score. For low scores, the memory challenge is negligible, making it primarily a CPU task.
|
|
26
27
|
* **High Suspicion**: For the most suspicious requests, the system issues a high-difficulty combined CPU/Memory challenge. The architecture allows for plugging in more complex challenges like CAPTCHAs if needed.
|
|
28
|
+
* **New Devices**: To increase the cost for bots that simply clear their cookies, new (unseen) devices are systematically presented with a minimal, almost imperceptible challenge on their first visit, even if their suspicion score is low.
|
|
27
29
|
|
|
28
|
-
Once the challenge is solved, a clearance "ticket" is issued via a secure cookie, exempting the user from new challenges
|
|
30
|
+
Once the challenge is solved, a clearance "ticket" is issued via a secure cookie, exempting the user from new challenges. The duration of this ticket is dynamic:
|
|
31
|
+
- **Probationary Ticket**: If the request was moderately suspicious, a very short-lived "probationary" ticket (e.g., 30 seconds) is issued. This forces the client to be re-evaluated quickly, increasing security.
|
|
32
|
+
- **Optimal TTL Ticket**: For less suspicious requests, a genetic algorithm calculates the optimal ticket duration, balancing security (shorter TTL for higher risk) and user experience (longer TTL for lower risk).
|
|
33
|
+
|
|
34
|
+
For API clients, the challenge is delivered as a `404` JSON response, and the client library can automatically solve it and retry the original request.
|
|
29
35
|
|
|
30
36
|
## Features
|
|
31
37
|
|
|
@@ -33,7 +39,7 @@ Once the challenge is solved, a clearance "ticket" is issued via a secure cookie
|
|
|
33
39
|
- **Secure Ticket System**: Uses HMAC-SHA256 signatures to validate clearances and prevent tampering.
|
|
34
40
|
- **Pluggable Datastore**: Supports external datastores like Redis for state persistence and scalability across multiple server instances.
|
|
35
41
|
- **Express.js Middleware**: Easy integration into an Express application with `powMiddleware`. The datastore must support setting a Time-To-Live (TTL) for challenge secrets.
|
|
36
|
-
- **Timing Attack Protection**: Uses `crypto.timingSafeEqual` for secure
|
|
42
|
+
- **Timing Attack Protection**: Uses `crypto.timingSafeEqual` for secure validation of tickets and other signatures.
|
|
37
43
|
- **Bot Whitelisting**: Includes a DNS-based verification mechanism to reliably identify and whitelist legitimate crawlers like Googlebot and Bingbot, preventing them from being challenged. The results are cached for optimal performance.
|
|
38
44
|
- **Automatic Parameter Tuning**: Includes a genetic algorithm-based optimizer (`startThresholdAutoTuning`) that analyzes real traffic to dynamically adjust not only suspicion thresholds (`low`, `medium`, `high`) but also the parameters for behavioral pattern detection, improving accuracy and reducing false positives over time.
|
|
39
45
|
|
|
@@ -61,7 +67,7 @@ The `powMiddleware` requires a configuration object defining the weights of susp
|
|
|
61
67
|
import express from 'express';
|
|
62
68
|
import bodyParser from 'body-parser';
|
|
63
69
|
import cookieParser from 'cookie-parser';
|
|
64
|
-
import { powMiddleware, default_whitelist } from './fingerprint.js'; // Adjust the path
|
|
70
|
+
import { powMiddleware, default_whitelist, default_analyzers } from './fingerprint.js'; // Adjust the path
|
|
65
71
|
|
|
66
72
|
const app = express();
|
|
67
73
|
app.use(cookieParser());
|
|
@@ -81,17 +87,16 @@ const securityConfig = {
|
|
|
81
87
|
headerAnomalyScore: 0.1, // Penalizes abnormal headers (missing UA, etc.)
|
|
82
88
|
requestPatternScore: 0.6,// Penalizes bot-like request sequences (scraping, etc.)
|
|
83
89
|
inconsistencyScore: 0.8, // Strongly penalizes inconsistency between the current and initial fingerprint (stolen cookie)
|
|
84
|
-
behaviorScore: 0.7,
|
|
85
|
-
honeypotScore: 1.0
|
|
90
|
+
behaviorScore: 0.7, // Penalizes non-human interactions (no mouse/keyboard activity)
|
|
91
|
+
honeypotScore: 1.0, // Strongly penalizes bots filling hidden form fields
|
|
92
|
+
crossLayerInconsistencyScore: 0.4, // Penalizes mismatches between client-side data (e.g., OS) and server-side headers (e.g., User-Agent)
|
|
93
|
+
timeInconsistencyScore: 0.9 // Strongly penalizes large time gaps between client metric collection and server reception (replay attack)
|
|
86
94
|
},
|
|
87
|
-
// A new, non-suspicious device will always have its score adjusted to a minimum of 1, ensuring it receives a minimal, almost imperceptible challenge on its first visit.
|
|
88
95
|
thresholds: {
|
|
89
96
|
low: 20, // Score from which a CPU challenge is issued
|
|
90
97
|
medium: 45, // Score for a more difficult combined CPU/Memory challenge
|
|
91
98
|
high: 75, // Score for a very difficult challenge
|
|
92
|
-
block: 95, // Score above which the request is blocked outright (HTTP
|
|
93
|
-
isStaticResource: (req) => req.path.startsWith('/static/'), // Optional: Custom function to identify static resources
|
|
94
|
-
isApiRequest: (req) => req.path.startsWith('/api/') || req.headers.accept?.includes('application/json') // Optional: Custom function to identify API requests
|
|
99
|
+
block: 95, // Score above which the request is blocked outright (HTTP 404)
|
|
95
100
|
},
|
|
96
101
|
// (Optional) Configure the duration (in milliseconds) for various temporary data.
|
|
97
102
|
ticketMaxAge: 3600000, // 1 hour. Duration for which a solved challenge ticket is valid.
|
|
@@ -114,27 +119,20 @@ const securityConfig = {
|
|
|
114
119
|
// List of URL paths that should never be accessed by a legitimate user.
|
|
115
120
|
// A request to one of these paths will immediately flag the device as malicious.
|
|
116
121
|
trapUrls: ['/wp-admin', '/.env', '/admin.php', '/phpmyadmin'], // (Optional)
|
|
117
|
-
// Automatically detect common
|
|
118
|
-
|
|
119
|
-
//
|
|
122
|
+
// Automatically detect common injection patterns. Can be a boolean or an array of specific types.
|
|
123
|
+
// - `true`: Enables all available detections (default).
|
|
124
|
+
// - `false`: Disables injection detection.
|
|
125
|
+
// - `['sql', 'rce']`: Enables only SQL injection and Remote Command Execution detection.
|
|
126
|
+
detectInjections: ['sql', 'rce', 'traversal', 'xxe', 'ssti', 'log4shell'], // (Optional, default: true)
|
|
127
|
+
// (Optional) Plug in external analyzers. This allows you to extend detection with specialized libraries or custom logic.
|
|
120
128
|
// Each function receives an object with all query and body data and should return `true` if a threat is detected.
|
|
121
129
|
analyzers: [
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return waf.isMalicious(JSON.stringify(data));
|
|
129
|
-
},
|
|
130
|
-
// Example 2: Using a specialized library for XSS detection.
|
|
131
|
-
// (npm install xss)
|
|
132
|
-
(data) => {
|
|
133
|
-
const xss = require('xss');
|
|
134
|
-
const originalData = JSON.stringify(data);
|
|
135
|
-
// If the sanitized string is different from the original, it means malicious HTML/JS was found and removed.
|
|
136
|
-
return xss(originalData) !== originalData;
|
|
137
|
-
},
|
|
130
|
+
...default_analyzers(), // Includes the default XSS analyzer.
|
|
131
|
+
|
|
132
|
+
// Example 2: Enable a powerful WAF with ModSecurity and the OWASP Core Rule Set.
|
|
133
|
+
// Requires `npm install modsecurity-nodejs` and downloading the OWASP CRS rules.
|
|
134
|
+
// modsecurity_analyzer('/path/to/owasp-crs/crs-setup.conf'),
|
|
135
|
+
|
|
138
136
|
// Example 3: A custom function to detect specific keywords (e.g., for anti-spam).
|
|
139
137
|
(data) => {
|
|
140
138
|
const spamKeywords = ['viagra', 'free money', 'crypto pump'];
|
|
@@ -161,8 +159,10 @@ const securityConfig = {
|
|
|
161
159
|
...default_whitelist(), // Use the defaults
|
|
162
160
|
{ userAgent: 'MyIndustrySpecificBot', hostnameSuffix: '.my-bot-verifier.com' }, // Add a custom bot
|
|
163
161
|
],
|
|
164
|
-
//
|
|
165
|
-
|
|
162
|
+
// Optional: Custom function to identify static resources
|
|
163
|
+
isStaticResource: (req) => req.path.startsWith('/static/'),
|
|
164
|
+
// Optional: Custom function to identify API requests
|
|
165
|
+
isApiRequest: (req) => req.path.startsWith('/api/') || req.headers.accept?.includes('application/json'),
|
|
166
166
|
// The logger is required for auto-tuning. It collects data on requests.
|
|
167
167
|
logger: (log) => trafficData.push(log),
|
|
168
168
|
// (Optional) Configuration for the automatic threshold and pattern tuning.
|
|
@@ -171,6 +171,8 @@ const securityConfig = {
|
|
|
171
171
|
interval: 1800000, // Optimization cycle every 30 minutes (in ms).
|
|
172
172
|
minDataPoints: 200 // Minimum requests before starting an optimization cycle.
|
|
173
173
|
},
|
|
174
|
+
// Enables problem solving for suspicious activity (configurable in problems.config.json)
|
|
175
|
+
enableUsefulWork: true
|
|
174
176
|
};
|
|
175
177
|
|
|
176
178
|
// Create an instance of the middleware with your security configuration.
|
|
@@ -196,7 +198,39 @@ app.use((req, res, next) => {
|
|
|
196
198
|
|
|
197
199
|
app.listen(3000, () => console.log('Server started on port 3000'));
|
|
198
200
|
```
|
|
199
|
-
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Advanced Behavioral Analysis
|
|
206
|
+
|
|
207
|
+
The FingerprintEngine includes sophisticated behavioral analysis to detect non-human patterns. This analysis is performed by the `getRequestPatternScore` function, which is a stateful check that looks for repetitive or unnaturally fast requests from a single device.
|
|
208
|
+
|
|
209
|
+
This function uses several configurable parameters to identify suspicious behavior:
|
|
210
|
+
|
|
211
|
+
### Core Pattern Detection
|
|
212
|
+
|
|
213
|
+
These parameters form the basis of the request pattern analysis:
|
|
214
|
+
|
|
215
|
+
* `velocityThreshold`: (Default: 800ms) Penalizes requests that are too fast to be humanly possible. If the time since the last request from a device is less than this value, the suspicion score increases.
|
|
216
|
+
* `burstThreshold`: (Default: 1500ms) Adds a significant penalty for multiple identical requests (same path and query parameters) occurring in a very short time frame. This is a strong indicator of automated retries or brute-force attacks.
|
|
217
|
+
* `scrapeThreshold`: (Default: 1000ms) Penalizes sequential requests to the same path but with different query parameters. This pattern is typical of scraping bots that iterate through pages or product IDs.
|
|
218
|
+
* `sequenceLength`: (Default: 3) Detects repetitive sequences of requests (e.g., A -> B -> C -> A -> B -> C), which is a common pattern for scripted bots navigating a site.
|
|
219
|
+
|
|
220
|
+
### Statistical Analysis (Benford's Law)
|
|
221
|
+
|
|
222
|
+
To counter more advanced bots that might try to randomize their request timings, the engine employs statistical analysis based on Benford's Law.
|
|
223
|
+
|
|
224
|
+
* **How it works**: Benford's Law states that in many naturally occurring sets of numbers, the leading digit is more likely to be small. For example, the number 1 appears as the leading digit about 30% of the time, while 9 appears less than 5% of the time. The timings between a human's requests tend to follow this natural distribution, whereas a bot's randomized delays often do not.
|
|
225
|
+
|
|
226
|
+
* `benfordMinSamples`: (Default: 15) The minimum number of request timings to collect before performing a Benford's Law test.
|
|
227
|
+
* `benfordWeight`: (Default: 50) The weight applied to the suspicion score if the distribution of timings significantly deviates from Benford's Law.
|
|
228
|
+
|
|
229
|
+
### Configuration and Auto-Tuning
|
|
230
|
+
|
|
231
|
+
All these parameters are part of the `patterns` object within the main security configuration and can be fine-tuned.
|
|
232
|
+
|
|
233
|
+
## Customizing the Challenge Page
|
|
200
234
|
|
|
201
235
|
You can provide your own HTML template for the Proof-of-Work challenge page to maintain a consistent user experience with your brand.
|
|
202
236
|
|
|
@@ -251,7 +285,7 @@ The main Express middleware. It orchestrates identification, suspicion calculati
|
|
|
251
285
|
|
|
252
286
|
#### `configureStore(store)`
|
|
253
287
|
Allows replacing the in-memory store with an external datastore (like Redis) for persistence and scaling.
|
|
254
|
-
The library provides ready-to-use adapters for popular datastores like Redis and
|
|
288
|
+
The library provides ready-to-use adapters for popular datastores like **Redis**, **MongoDB**, and any **SQL database** supported by Knex.js. These adapters automatically handle the Time-To-Live (TTL) required for temporary data like challenge secrets.
|
|
255
289
|
|
|
256
290
|
**Redis Example:**
|
|
257
291
|
|
|
@@ -286,6 +320,32 @@ configureStore(mongoStore);
|
|
|
286
320
|
// db.sessions.createIndex({ "expiresAt": 1 }, { expireAfterSeconds: 0 })
|
|
287
321
|
```
|
|
288
322
|
|
|
323
|
+
**SQL Example (with Knex.js):**
|
|
324
|
+
|
|
325
|
+
```javascript
|
|
326
|
+
import { configureStore } from './fingerprint.js';
|
|
327
|
+
import { createSqlStore } from './sql-store.js';
|
|
328
|
+
import knex from 'knex';
|
|
329
|
+
|
|
330
|
+
const knexClient = knex({
|
|
331
|
+
client: 'pg', // or 'mysql', 'sqlite3', etc.
|
|
332
|
+
connection: process.env.DATABASE_URL,
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const sqlStore = createSqlStore(knexClient, 'fingerprint_sessions'); // 'fingerprint_sessions' is the table name
|
|
336
|
+
configureStore(sqlStore);
|
|
337
|
+
|
|
338
|
+
// IMPORTANT: For automatic expiration of challenges and other temporary data to work,
|
|
339
|
+
// your table must have an `expiresAt` column. The store will handle cleanup of expired rows,
|
|
340
|
+
// but you must create the table yourself.
|
|
341
|
+
// Example schema for PostgreSQL:
|
|
342
|
+
// CREATE TABLE fingerprint_sessions (
|
|
343
|
+
// "key" VARCHAR(255) PRIMARY KEY,
|
|
344
|
+
// "value" TEXT NOT NULL,
|
|
345
|
+
// "expiresAt" TIMESTAMPTZ
|
|
346
|
+
// );
|
|
347
|
+
```
|
|
348
|
+
|
|
289
349
|
#### `identifyRequest(req, res)`
|
|
290
350
|
An asynchronous function that returns an identification string for a given request, based on its suspicion level (`device:<id>`, `suspicious_medium:<ip>`, etc.). Useful for integration with a custom rate-limiter.
|
|
291
351
|
|
|
@@ -472,7 +532,7 @@ Although not exported for direct public use, understanding its role can be usefu
|
|
|
472
532
|
|
|
473
533
|
While `powMiddleware` is convenient for Express, you can use the `FingerprintEngine` directly in any Node.js server environment (e.g., native `http`, Fastify, Koa). This gives you full control over the request/response cycle.
|
|
474
534
|
|
|
475
|
-
**For concrete examples with Koa and Fastify, see our Framework Integration Guide.**
|
|
535
|
+
**For concrete examples with Koa and Fastify, see our [Framework Integration Guide](https://github.com/anonympins/fingerprint/blob/main/INTEGRATION.md).**
|
|
476
536
|
|
|
477
537
|
The engine is a named export from the main module.
|
|
478
538
|
|
|
@@ -498,7 +558,7 @@ const server = http.createServer(async (req, res) => {
|
|
|
498
558
|
clientIp: req.socket.remoteAddress,
|
|
499
559
|
path: req.url.split('?')[0],
|
|
500
560
|
cookies: {}, // Parse cookies from req.headers.cookie
|
|
501
|
-
query: new URL(req.url, `http://${req.headers.host}`).searchParams,
|
|
561
|
+
query: Object.fromEntries(new URL(req.url, `http://${req.headers.host}`).searchParams),
|
|
502
562
|
headers: req.headers,
|
|
503
563
|
rawReq: req, // Pass the raw request
|
|
504
564
|
rawRes: res, // Pass the raw response for cookie setting
|
package/fingerprint.client.js
CHANGED
|
@@ -202,6 +202,9 @@ const ClientLibrary = {
|
|
|
202
202
|
* @returns {ClientBehaviorMetrics}
|
|
203
203
|
*/
|
|
204
204
|
getClientBehaviorMetrics() {
|
|
205
|
+
// Ajoute un timestamp au moment de la collecte pour la détection de rejeu.
|
|
206
|
+
metrics.clientTimestamp = Date.now();
|
|
207
|
+
|
|
205
208
|
// Normalise l'entropie de la souris
|
|
206
209
|
if (mouseMovements > 10) {
|
|
207
210
|
metrics.mouseEntropy /= mouseMovements;
|
|
@@ -357,6 +360,17 @@ const ClientLibrary = {
|
|
|
357
360
|
url.searchParams.set(`pow_solution_${key}`, String(value));
|
|
358
361
|
});
|
|
359
362
|
|
|
363
|
+
// Pour le challenge d'optimisation, la solution est un tableau d'objets
|
|
364
|
+
if (solution.population) {
|
|
365
|
+
url.searchParams.set('pow_solution_population', JSON.stringify(solution.population));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Pour le challenge de travail utile
|
|
369
|
+
if (solution.work_result) {
|
|
370
|
+
url.searchParams.set('pow_solution_work_result', JSON.stringify(solution.work_result));
|
|
371
|
+
url.searchParams.set('pow_problem_id', solution.problem_id);
|
|
372
|
+
}
|
|
373
|
+
|
|
360
374
|
// On utilise la chaîne d'intercepteurs pour la requête réessayée,
|
|
361
375
|
// ce qui garantit que le fetch original est appelé avec le bon contexte.
|
|
362
376
|
// Cela évite de réintroduire l'erreur "Illegal invocation".
|
|
@@ -417,6 +431,7 @@ const ClientLibrary = {
|
|
|
417
431
|
* @property {number} mouseEntropy - Entropie des mouvements de la souris.
|
|
418
432
|
* @property {number} keystrokeLatency - Latence moyenne entre les frappes.
|
|
419
433
|
* @property {boolean} honeypotInteraction - Vrai si un honeypot a été touché.
|
|
434
|
+
* @property {number} clientTimestamp - Timestamp (Date.now()) de la collecte des métriques.
|
|
420
435
|
*/
|
|
421
436
|
|
|
422
437
|
/** @type {ClientBehaviorMetrics} */
|
|
@@ -424,6 +439,7 @@ const metrics = {
|
|
|
424
439
|
mouseEntropy: 0,
|
|
425
440
|
keystrokeLatency: 0,
|
|
426
441
|
honeypotInteraction: false,
|
|
442
|
+
clientTimestamp: 0,
|
|
427
443
|
};
|
|
428
444
|
|
|
429
445
|
let lastMousePos = { x: 0, y: 0 };
|
package/fingerprint.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import { BlockList } from "node:net";
|
|
3
3
|
import dns from "node:dns/promises";
|
|
4
|
+
import { problemManager } from "./problem-manager.js";
|
|
4
5
|
import { Optimization } from "./library.js";
|
|
5
6
|
import { cyrb53, FingerprintBuilder } from "./fingerprint.builder.js";
|
|
6
7
|
import { readFileSync } from "node:fs";
|
|
@@ -762,6 +763,25 @@ function getHoneypotScore(context, honeypotConfig = {}) {
|
|
|
762
763
|
return { honeypotScore: 0 };
|
|
763
764
|
}
|
|
764
765
|
|
|
766
|
+
/**
|
|
767
|
+
* @private
|
|
768
|
+
* Map of malicious patterns grouped by type.
|
|
769
|
+
*/
|
|
770
|
+
const injectionPatterns = {
|
|
771
|
+
// SQL/NoSQL injections, including time-based attacks
|
|
772
|
+
sql: /(\$ne|' *OR *'1'='1|['";]\s*--|; ?(DROP|TRUNCATE|DELETE)|UNION SELECT|SLEEP\(|BENCHMARK\(|WAITFOR DELAY)/i,
|
|
773
|
+
// Log4Shell (JNDI injection)
|
|
774
|
+
log4shell: /\$\{jndi:(ldap|rmi|dns):/i,
|
|
775
|
+
// Server-Side Template Injection (SSTI) for engines like Jinja2, Twig, etc.
|
|
776
|
+
ssti: /\{\{.*\}\}|\{%.*%\}/,
|
|
777
|
+
// XML External Entity (XXE) injection
|
|
778
|
+
xxe: /<!ENTITY\s+.*SYSTEM/i,
|
|
779
|
+
// Path Traversal
|
|
780
|
+
traversal: /(\.\.\/|\.\.\\)/,
|
|
781
|
+
// Remote Command Execution (RCE)
|
|
782
|
+
rce: /`.*`|(^|[\n;&|]\s*)(ping|ls|whoami|cat|rm|ncat|nc|bash|sh|powershell|cmd)\b/i,
|
|
783
|
+
};
|
|
784
|
+
|
|
765
785
|
/**
|
|
766
786
|
* Calcule un score basé sur les métriques comportementales envoyées par le client.
|
|
767
787
|
* @param {object} context - Le contexte de la requête, contenant les en-têtes.
|
|
@@ -821,6 +841,29 @@ function getBehaviorScore(context) {
|
|
|
821
841
|
}
|
|
822
842
|
}
|
|
823
843
|
|
|
844
|
+
/**
|
|
845
|
+
* Calcule un score basé sur l'incohérence temporelle entre le client et le serveur pour détecter les attaques par rejeu.
|
|
846
|
+
* @param {object} context - Le contexte de la requête, contenant le timestamp de la requête.
|
|
847
|
+
* @param {object} metrics - Les métriques comportementales parsées depuis le client.
|
|
848
|
+
* @returns {{timeInconsistencyScore: number}}
|
|
849
|
+
*/
|
|
850
|
+
function getTimeInconsistencyScore(context, metrics) {
|
|
851
|
+
const REPLAY_THRESHOLD_MS = 5000; // 5 secondes
|
|
852
|
+
let score = 0;
|
|
853
|
+
|
|
854
|
+
if (metrics.clientTimestamp && context.requestTimestamp) {
|
|
855
|
+
const timeDelta = context.requestTimestamp - metrics.clientTimestamp;
|
|
856
|
+
|
|
857
|
+
// Un delta très grand est un signal fort d'attaque par rejeu.
|
|
858
|
+
// Un delta négatif peut arriver si l'horloge du client est en avance, on l'ignore.
|
|
859
|
+
if (timeDelta > REPLAY_THRESHOLD_MS) {
|
|
860
|
+
// La pénalité est proportionnelle au dépassement du seuil.
|
|
861
|
+
score = Math.min(100, (timeDelta / REPLAY_THRESHOLD_MS - 1) * 50);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return { timeInconsistencyScore: score };
|
|
865
|
+
}
|
|
866
|
+
|
|
824
867
|
/**
|
|
825
868
|
* Calcule un score d'incohérence entre les données du fingerprint client et les en-têtes serveur.
|
|
826
869
|
* @param {object} context - Le contexte de la requête.
|
|
@@ -960,8 +1003,7 @@ function getRequestPatternScore(context, deviceData, patternConfig = {}) {
|
|
|
960
1003
|
// 5. (NOUVEAU) Analyse de la distribution des délais avec la loi de Benford
|
|
961
1004
|
if (deviceData.timingHistory.length >= benfordMinSamples) {
|
|
962
1005
|
// On concatène tous les délais en une seule chaîne de chiffres.
|
|
963
|
-
const
|
|
964
|
-
const benfordDeviation = Optimization.Operators.benfordTest(timingString);
|
|
1006
|
+
const benfordDeviation = Optimization.Operators.benfordTest(deviceData.timingHistory);
|
|
965
1007
|
|
|
966
1008
|
// Une déviation > 0.15 est suspecte. On peut pondérer la pénalité.
|
|
967
1009
|
// Une déviation de 0.3 (très suspecte) donnerait un score de 100 (0.3 / 0.3 * 100).
|
|
@@ -1242,6 +1284,9 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
1242
1284
|
// On appelle getHoneypotScore ici pour que son résultat soit inclus dans le vecteur.
|
|
1243
1285
|
const { honeypotScore } = getHoneypotScore(context, honeypotConfig);
|
|
1244
1286
|
|
|
1287
|
+
// NOUVEAU: On calcule le score d'incohérence temporelle.
|
|
1288
|
+
const { timeInconsistencyScore } = getTimeInconsistencyScore(context, JSON.parse(context.headers['x-behavior-metrics'] || '{}'));
|
|
1289
|
+
|
|
1245
1290
|
// NOUVEAU: On calcule le score d'incohérence entre les couches.
|
|
1246
1291
|
const { crossLayerInconsistencyScore } = getCrossLayerInconsistency(context);
|
|
1247
1292
|
|
|
@@ -1257,7 +1302,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
1257
1302
|
deviceData.ips = new Set(deviceData.ips);
|
|
1258
1303
|
}
|
|
1259
1304
|
// Le vecteur de suspicion est maintenant complet.
|
|
1260
|
-
return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, requestPatternScore, crossLayerInconsistencyScore };
|
|
1305
|
+
return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore };
|
|
1261
1306
|
};
|
|
1262
1307
|
|
|
1263
1308
|
// A residential user can change networks (home, 4G, public wifi).
|
|
@@ -1404,17 +1449,20 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
1404
1449
|
const clientIp = ${JSON.stringify(clientIp)};
|
|
1405
1450
|
const cpuTarget = BigInt("0x${target}");
|
|
1406
1451
|
const memDifficulty = ${memoryDifficulty};
|
|
1452
|
+
// The client-side fingerprint library must be available to generate the fingerprint
|
|
1453
|
+
// of the machine solving the challenge. This assumes a client library is loaded.
|
|
1454
|
+
// We need a function to get the client fingerprint. Let's assume it's available on window.
|
|
1455
|
+
const getClientFingerprint = () => (window.ClientLibrary && typeof window.ClientLibrary.getDeviceFingerprint === 'function') ? window.ClientLibrary.getDeviceFingerprint() : '';
|
|
1456
|
+
const fingerprint = getClientFingerprint();
|
|
1407
1457
|
|
|
1408
1458
|
// --- CPU Challenge ---
|
|
1409
1459
|
document.getElementById('loader').innerText = '⚙️ Performing CPU security calculation...';
|
|
1410
|
-
const cpuSolution = await window.solveCpuChallengeInline(clientIp, nonce, cpuTarget, clientSecret, (progress) => {
|
|
1460
|
+
const cpuSolution = await window.solveCpuChallengeInline(clientIp, nonce, cpuTarget, clientSecret, fingerprint, (progress) => {
|
|
1411
1461
|
// Optional progress callback
|
|
1412
1462
|
});
|
|
1413
|
-
|
|
1414
1463
|
// --- Memory Challenge ---
|
|
1415
1464
|
document.getElementById('loader').innerText = '⚙️ Performing memory allocation and calculation... (' + memDifficulty + ' MB)';
|
|
1416
1465
|
await new Promise(r => setTimeout(r, 10)); // Yield to update UI
|
|
1417
|
-
|
|
1418
1466
|
let memSolution = 0;
|
|
1419
1467
|
try {
|
|
1420
1468
|
const memSeed = nonce + ":" + clientSecret;
|
|
@@ -1423,7 +1471,7 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
1423
1471
|
document.getElementById('loader').innerText = "Error: Insufficient memory. Please refresh.";
|
|
1424
1472
|
return;
|
|
1425
1473
|
}
|
|
1426
|
-
window.location.href = path + "?pow_type=cpu_mem&pow_nonce=" + ${JSON.stringify(nonce)} + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + memSolution;
|
|
1474
|
+
window.location.href = path + "?pow_type=cpu_mem&pow_nonce=" + ${JSON.stringify(nonce)} + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + memSolution + "&pow_fp=" + encodeURIComponent(fingerprint);
|
|
1427
1475
|
}
|
|
1428
1476
|
solve();
|
|
1429
1477
|
`;
|
|
@@ -1458,10 +1506,11 @@ export function verifyCpuTargetPoWAndGenerateTicket(
|
|
|
1458
1506
|
nonce,
|
|
1459
1507
|
solution,
|
|
1460
1508
|
clientSecret, // Le secret est maintenant requis
|
|
1461
|
-
target, // La cible est maintenant passée directement en hexadécimal
|
|
1509
|
+
target, // La cible est maintenant passée directement en hexadécimal,
|
|
1510
|
+
fingerprint, // Le fingerprint du SOLVER, soumis par le client
|
|
1462
1511
|
) {
|
|
1463
1512
|
const message = clientSecret
|
|
1464
|
-
? `${nonce}:${solution}:${clientSecret}`
|
|
1513
|
+
? `${nonce}:${solution}:${clientSecret}:${fingerprint}`
|
|
1465
1514
|
: `${clientIp}:${nonce}:${solution}`; // L'IP est utilisée uniquement pour les challenges sans secret (plus anciens/simples)
|
|
1466
1515
|
const hash = crypto
|
|
1467
1516
|
.createHash("sha256")
|
|
@@ -1483,106 +1532,6 @@ export function verifyCpuTargetPoWAndGenerateTicket(
|
|
|
1483
1532
|
return null;
|
|
1484
1533
|
}
|
|
1485
1534
|
|
|
1486
|
-
const staticExtensions = new RegExp(
|
|
1487
|
-
"\\.(js|css|png|jpg|jpeg|gif|svg|mp3|webp|ico|woff|woff2|ttf|otf|map|json|manifest|webmanifest)$",
|
|
1488
|
-
"i",
|
|
1489
|
-
);
|
|
1490
|
-
const isStaticResource = (path) => staticExtensions.test(path);
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
/**
|
|
1494
|
-
* Détermine le TTL optimal pour un ticket en utilisant un algorithme génétique multi-objectifs.
|
|
1495
|
-
* @param {number} suspicionScore - Le score de suspicion de la requête.
|
|
1496
|
-
* @returns {number} Le TTL optimal calculé en millisecondes.
|
|
1497
|
-
*/
|
|
1498
|
-
function determineOptimalTicketTtl(suspicionScore) {
|
|
1499
|
-
// Définir les bornes pour la durée de vie du ticket (5 minutes à 24 heures)
|
|
1500
|
-
const MIN_TTL = 300000;
|
|
1501
|
-
const MAX_TTL = 86400000;
|
|
1502
|
-
|
|
1503
|
-
const solverFunction = () => {
|
|
1504
|
-
const fitnessFunction = Optimization.Operators.createOptimalTtlEvaluator({ suspicionScore });
|
|
1505
|
-
|
|
1506
|
-
// Un "individu" est simplement une valeur de TTL en millisecondes.
|
|
1507
|
-
const createIndividual = () => MIN_TTL + Math.random() * (MAX_TTL - MIN_TTL);
|
|
1508
|
-
const crossover = (ttl1, ttl2) => (ttl1 + ttl2) / 2;
|
|
1509
|
-
const mutate = (ttl) => {
|
|
1510
|
-
const newTtl = ttl + (Math.random() - 0.5) * (MAX_TTL - MIN_TTL) * 0.1; // Mutation de +/- 10% max
|
|
1511
|
-
return Math.max(MIN_TTL, Math.min(MAX_TTL, newTtl));
|
|
1512
|
-
};
|
|
1513
|
-
|
|
1514
|
-
const paretoFront = Optimization.geneticAlgorithmMultiObjective(
|
|
1515
|
-
createIndividual,
|
|
1516
|
-
fitnessFunction,
|
|
1517
|
-
crossover,
|
|
1518
|
-
mutate,
|
|
1519
|
-
{
|
|
1520
|
-
generations: 40,
|
|
1521
|
-
populationSize: 30,
|
|
1522
|
-
}
|
|
1523
|
-
);
|
|
1524
|
-
|
|
1525
|
-
// Pour runMultiple, on doit retourner un objet avec une propriété "fitness" ou "energy".
|
|
1526
|
-
// Pour un front de Pareto, il n'y a pas de score unique. On choisit la meilleure solution
|
|
1527
|
-
// en fonction du score de suspicion et on lui assigne un score de 0 pour que runMultiple la sélectionne.
|
|
1528
|
-
if (!paretoFront || paretoFront.length === 0) {
|
|
1529
|
-
return { solution: null, fitness: Infinity };
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
// Stratégie de sélection :
|
|
1533
|
-
// Pour un score faible (< 50), on privilégie la solution avec le plus grand TTL (minimise la friction).
|
|
1534
|
-
// Pour un score élevé (>= 50), on privilégie la solution avec le plus petit TTL (minimise le risque).
|
|
1535
|
-
let bestSolutionInFront;
|
|
1536
|
-
if (suspicionScore < 50) {
|
|
1537
|
-
bestSolutionInFront = paretoFront.reduce((max, p) => Math.max(max, p.solution), 0);
|
|
1538
|
-
} else {
|
|
1539
|
-
bestSolutionInFront = paretoFront.reduce((min, p) => Math.min(min, p.solution), Infinity);
|
|
1540
|
-
}
|
|
1541
|
-
return { solution: bestSolutionInFront, fitness: 0 }; // fitness=0 car on a déjà la meilleure solution du cycle.
|
|
1542
|
-
};
|
|
1543
|
-
|
|
1544
|
-
// On exécute le solveur 20 fois pour trouver une solution plus stable et robuste.
|
|
1545
|
-
const { bestResult } = Optimization.runMultiple(solverFunction, 20);
|
|
1546
|
-
|
|
1547
|
-
if (!bestResult || !bestResult.solution || bestResult.solution === Infinity) {
|
|
1548
|
-
// Fallback : si l'algo ne retourne rien, on applique une règle simple et sûre.
|
|
1549
|
-
return Math.max(MIN_TTL, MAX_TTL - (suspicionScore / 100) * MAX_TTL);
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
// runMultiple choisit le meilleur résultat sur la base du score (ici, 0).
|
|
1553
|
-
// La "meilleure" solution dépendra du cycle qui a trouvé le meilleur compromis.
|
|
1554
|
-
return Math.round(bestResult.solution);
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
/**
|
|
1558
|
-
* Vérifie si une chaîne de caractères contient des patterns d'injection connus.
|
|
1559
|
-
* @param {string} str - La chaîne à vérifier.
|
|
1560
|
-
* @returns {boolean} - True si un pattern malveillant est détecté.
|
|
1561
|
-
* @private
|
|
1562
|
-
*/
|
|
1563
|
-
function isMalicious(str) {
|
|
1564
|
-
if (typeof str !== 'string') return false;
|
|
1565
|
-
// Regex pour les injections SQL et NoSQL de base
|
|
1566
|
-
// Ajout de la détection des injections basées sur le temps (SLEEP, BENCHMARK, WAITFOR) et d'autres commandes dangereuses.
|
|
1567
|
-
const injectionRegex = /(\$ne|' *OR *'1'='1|['";]\s*--|; ?(DROP|TRUNCATE|DELETE)|UNION SELECT|SLEEP\(|BENCHMARK\(|WAITFOR DELAY)/i;
|
|
1568
|
-
// Regex pour les injections plus avancées
|
|
1569
|
-
const log4ShellRegex = /\$\{jndi:(ldap|rmi|dns):/i;
|
|
1570
|
-
const sstiRegex = /\{\{.*\}\}|\{%.*%\}/; // Détecte les syntaxes de type Jinja2, Twig, etc.
|
|
1571
|
-
const xxeRegex = /<!ENTITY\s+.*SYSTEM/i;
|
|
1572
|
-
const pathTraversalRegex = /(\.\.\/|\.\.\\)/;
|
|
1573
|
-
// Regex pour les injections de commandes.
|
|
1574
|
-
// Elle détecte :
|
|
1575
|
-
// 1. L'utilisation de backticks ``.
|
|
1576
|
-
// 2. Des commandes dangereuses (rm, whoami...) qui sont soit au début de la chaîne,
|
|
1577
|
-
// soit précédées par un séparateur de commande (;, &&, ||, |) suivi d'espaces.
|
|
1578
|
-
const commandInjectionRegex = /`.*`|(^|[\n;&|]\s*)(ping|ls|whoami|cat|rm|ncat|nc|bash|sh|powershell|cmd)\b/i;
|
|
1579
|
-
|
|
1580
|
-
return injectionRegex.test(str) || log4ShellRegex.test(str) || sstiRegex.test(str) || xxeRegex.test(str) || pathTraversalRegex.test(str) || commandInjectionRegex.test(str);
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
// --- Middleware Proof-of-Work (Le péage) ---
|
|
1584
|
-
export { isMalicious };
|
|
1585
|
-
|
|
1586
1535
|
export class FingerprintEngine {
|
|
1587
1536
|
constructor(securityConfig) {
|
|
1588
1537
|
const isProduction = process.env.NODE_ENV === 'production';
|
|
@@ -1609,7 +1558,8 @@ export class FingerprintEngine {
|
|
|
1609
1558
|
(suspicionVector.inconsistencyScore || 0) * (weights.inconsistencyScore || 0) +
|
|
1610
1559
|
(suspicionVector.honeypotScore || 0) * (weights.honeypotScore || 0) +
|
|
1611
1560
|
(suspicionVector.behaviorScore || 0) * (weights.behaviorScore || 0) +
|
|
1612
|
-
(suspicionVector.crossLayerInconsistencyScore || 0) * (weights.crossLayerInconsistencyScore || 0)
|
|
1561
|
+
(suspicionVector.crossLayerInconsistencyScore || 0) * (weights.crossLayerInconsistencyScore || 0) +
|
|
1562
|
+
(suspicionVector.timeInconsistencyScore || 0) * (weights.timeInconsistencyScore || 0);
|
|
1613
1563
|
|
|
1614
1564
|
return Math.min(100, score);
|
|
1615
1565
|
}
|
|
@@ -1746,7 +1696,7 @@ export class FingerprintEngine {
|
|
|
1746
1696
|
this._log('Whitelisted bot verified - allowing request', { clientIp });
|
|
1747
1697
|
return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'bot' } };
|
|
1748
1698
|
}
|
|
1749
|
-
|
|
1699
|
+
|
|
1750
1700
|
// Resolve identity and check for persisted "condemned" status early.
|
|
1751
1701
|
const { deviceId, deviceData, newCookie } = await resolveRequestIdentity(requestContext, this.securityConfig);
|
|
1752
1702
|
const isNewDevice = !!newCookie;
|
|
@@ -1792,6 +1742,7 @@ export class FingerprintEngine {
|
|
|
1792
1742
|
const isSuspiciousHigh = finalScore >= thresholds.high && !isBlocked;
|
|
1793
1743
|
const isSuspiciousMedium = finalScore >= thresholds.medium;
|
|
1794
1744
|
const isSuspicious = finalScore >= thresholds.low;
|
|
1745
|
+
const isVerySuspicious = finalScore >= thresholds.medium; // Seuil pour le challenge d'optimisation
|
|
1795
1746
|
|
|
1796
1747
|
// Calculate an analog "suspicion factor" (0 to 1+) for progressive difficulty
|
|
1797
1748
|
const suspicionFactor = isSuspicious
|
|
@@ -1816,10 +1767,10 @@ export class FingerprintEngine {
|
|
|
1816
1767
|
// --- NOUVELLE LOGIQUE DE PRIORITÉ ---
|
|
1817
1768
|
// Si une solution de challenge est soumise, on la traite en priorité absolue,
|
|
1818
1769
|
// avant même de recalculer le score de suspicion.
|
|
1819
|
-
const { pow_type, pow_solution, pow_solution_cpu, pow_solution_mem } = query;
|
|
1770
|
+
const { pow_type, pow_solution, pow_solution_cpu, pow_solution_mem, pow_fp, pow_solution_population, pow_solution_work_result, pow_problem_id } = query;
|
|
1820
1771
|
if (pow_nonce && (pow_solution || (pow_solution_cpu && pow_solution_mem))) {
|
|
1821
1772
|
this._log('Challenge solution submitted', { pow_type, pow_nonce });
|
|
1822
|
-
|
|
1773
|
+
|
|
1823
1774
|
// On doit calculer le score de suspicion *avant* de valider le ticket,
|
|
1824
1775
|
// car le TTL optimal en dépend.
|
|
1825
1776
|
const preliminaryVector = suspicionVector; // Use the already calculated vector
|
|
@@ -1842,27 +1793,38 @@ export class FingerprintEngine {
|
|
|
1842
1793
|
const probationaryTtl = 30000; // 30 secondes
|
|
1843
1794
|
|
|
1844
1795
|
if (challengeContext) {
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
this._log('CPU target challenge verification', { isValid });
|
|
1856
|
-
} else if (pow_type === "cpu_mem" && pow_solution_cpu && pow_solution_mem) {
|
|
1857
|
-
const cpuTicket = verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, pow_solution_cpu, challengeContext.clientSecret, challengeContext.cpuTarget);
|
|
1858
|
-
const isMemValid = verifyMemoryPoW(pow_nonce, pow_solution_mem, challengeContext.memDifficulty, challengeContext.clientSecret);
|
|
1859
|
-
isValid = cpuTicket !== null && isMemValid;
|
|
1860
|
-
if (isValid) ticket = cpuTicket; // Le ticket est le même, on le réutilise
|
|
1861
|
-
this._log('Combined CPU+Memory challenge verification', {
|
|
1862
|
-
cpuValid: cpuTicket !== null,
|
|
1863
|
-
memValid: isMemValid,
|
|
1864
|
-
isValid
|
|
1796
|
+
// *** NOUVELLE VÉRIFICATION CRUCIALE ***
|
|
1797
|
+
// On compare le fingerprint soumis par le solver (`pow_fp`) avec celui stocké
|
|
1798
|
+
// lors de l'émission du challenge (`challengeContext.fingerprint`).
|
|
1799
|
+
const solverFingerprint = pow_fp;
|
|
1800
|
+
const originalFingerprint = challengeContext.fingerprint;
|
|
1801
|
+
|
|
1802
|
+
if (solverFingerprint !== originalFingerprint) {
|
|
1803
|
+
this._log('Fingerprint mismatch - challenge solved on a different machine!', {
|
|
1804
|
+
original: originalFingerprint,
|
|
1805
|
+
solver: solverFingerprint,
|
|
1865
1806
|
});
|
|
1807
|
+
isValid = false;
|
|
1808
|
+
} else {
|
|
1809
|
+
optimalTtl = determineOptimalTicketTtl(preliminaryScore);
|
|
1810
|
+
finalTtl = isProbationary ? probationaryTtl : optimalTtl;
|
|
1811
|
+
this._log('Challenge context found, verifying solution', { optimalTtl, finalTtl });
|
|
1812
|
+
|
|
1813
|
+
if (pow_type === "cpu_target" && pow_solution) {
|
|
1814
|
+
ticket = verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, pow_solution, challengeContext.clientSecret, challengeContext.cpuTarget, solverFingerprint);
|
|
1815
|
+
isValid = ticket !== null;
|
|
1816
|
+
this._log('CPU target challenge verification', { isValid });
|
|
1817
|
+
} else if (pow_type === "cpu_mem" && pow_solution_cpu && pow_solution_mem) {
|
|
1818
|
+
const cpuTicket = verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, pow_solution_cpu, challengeContext.clientSecret, challengeContext.cpuTarget, solverFingerprint);
|
|
1819
|
+
const isMemValid = verifyMemoryPoW(pow_nonce, pow_solution_mem, challengeContext.memDifficulty, challengeContext.clientSecret);
|
|
1820
|
+
isValid = cpuTicket !== null && isMemValid;
|
|
1821
|
+
if (isValid) ticket = cpuTicket; // Le ticket est le même, on le réutilise
|
|
1822
|
+
this._log('Combined CPU+Memory challenge verification', {
|
|
1823
|
+
cpuValid: cpuTicket !== null,
|
|
1824
|
+
memValid: isMemValid,
|
|
1825
|
+
isValid
|
|
1826
|
+
});
|
|
1827
|
+
}
|
|
1866
1828
|
}
|
|
1867
1829
|
} else {
|
|
1868
1830
|
this._log('Challenge context not found or expired', { pow_nonce });
|
|
@@ -1888,11 +1850,12 @@ export class FingerprintEngine {
|
|
|
1888
1850
|
finalSearchParams.delete('pow_solution');
|
|
1889
1851
|
finalSearchParams.delete('pow_solution_cpu');
|
|
1890
1852
|
finalSearchParams.delete('pow_solution_mem');
|
|
1853
|
+
finalSearchParams.delete('pow_fp'); // Ne pas oublier de nettoyer le fingerprint
|
|
1891
1854
|
|
|
1892
1855
|
// 4. On reconstruit le chemin final.
|
|
1893
1856
|
const finalQueryString = finalSearchParams.toString();
|
|
1894
1857
|
const finalRedirectPath = finalQueryString ? `${originalUrl.pathname}?${finalQueryString}` : originalUrl.pathname;
|
|
1895
|
-
this._log('Redirecting to clean path', { finalRedirectPath });
|
|
1858
|
+
this._log('Redirecting to clean path', { finalRedirectPath, cookieMaxAge: finalTtl });
|
|
1896
1859
|
return {
|
|
1897
1860
|
action: 'redirect',
|
|
1898
1861
|
path: finalRedirectPath,
|
|
@@ -1900,8 +1863,8 @@ export class FingerprintEngine {
|
|
|
1900
1863
|
vector: { challenge_solved: 100 },
|
|
1901
1864
|
cookie: {
|
|
1902
1865
|
name: 'pow_clearance',
|
|
1903
|
-
value: ticket,
|
|
1904
|
-
options: { httpOnly: true, secure: this.isProduction, maxAge: finalTtl }
|
|
1866
|
+
value: ticket, // The ticket itself
|
|
1867
|
+
options: { httpOnly: true, secure: this.isProduction, maxAge: finalTtl } // Options for setting the cookie
|
|
1905
1868
|
}
|
|
1906
1869
|
};
|
|
1907
1870
|
} else {
|
|
@@ -1911,6 +1874,60 @@ export class FingerprintEngine {
|
|
|
1911
1874
|
suspicionVector.honeypotScore = 100; // Invalid solution is a strong bot signal.
|
|
1912
1875
|
finalScore = this.calculateFinalScore(suspicionVector);
|
|
1913
1876
|
}
|
|
1877
|
+
} else if (pow_nonce && pow_type === 'optimization_task' && pow_solution_population) {
|
|
1878
|
+
this._log('Optimization task solution submitted', { pow_nonce });
|
|
1879
|
+
const challengeContext = await store.get(`secret:${pow_nonce}`);
|
|
1880
|
+
let isValid = false;
|
|
1881
|
+
|
|
1882
|
+
if (challengeContext?.optimizationProblem) {
|
|
1883
|
+
try {
|
|
1884
|
+
const submittedChromosomes = JSON.parse(pow_solution_population);
|
|
1885
|
+
// Vérification simple : le client a-t-il renvoyé le bon nombre de solutions ?
|
|
1886
|
+
if (Array.isArray(submittedChromosomes) && submittedChromosomes.length === challengeContext.optimizationProblem.population.length) {
|
|
1887
|
+
// Le serveur recalcule la fitness pour la nouvelle population.
|
|
1888
|
+
const fitnessFunction = Optimization.Operators.createFullSecurityConfigEvaluator({ trafficData: challengeContext.optimizationProblem.trafficData });
|
|
1889
|
+
const newPopulation = submittedChromosomes.map(chromosome => ({ chromosome, fitness: fitnessFunction(chromosome) }));
|
|
1890
|
+
|
|
1891
|
+
// On met à jour le problème principal avec la nouvelle population.
|
|
1892
|
+
challengeContext.optimizationProblem.population = newPopulation;
|
|
1893
|
+
await store.set(`device:${deviceId}`, deviceData); // Sauvegarde l'état mis à jour
|
|
1894
|
+
isValid = true;
|
|
1895
|
+
}
|
|
1896
|
+
} catch (e) {
|
|
1897
|
+
this._log('Error parsing optimization solution', { error: e.message });
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1901
|
+
if (isValid) {
|
|
1902
|
+
await store.delete(`secret:${pow_nonce}`);
|
|
1903
|
+
// La solution est valide, on accorde un ticket et on redirige.
|
|
1904
|
+
const ticket = "valid_ticket_placeholder"; // Générer un vrai ticket ici
|
|
1905
|
+
return { action: 'redirect', path: path, score: 0, vector: { challenge_solved: 100 }, cookie: { name: 'pow_clearance', value: ticket, options: { httpOnly: true, secure: this.isProduction, maxAge: 60000 } } };
|
|
1906
|
+
} else {
|
|
1907
|
+
this._log('Optimization task solution invalid', { pow_nonce });
|
|
1908
|
+
suspicionVector.honeypotScore = 100;
|
|
1909
|
+
finalScore = this.calculateFinalScore(suspicionVector);
|
|
1910
|
+
}
|
|
1911
|
+
} else if (pow_nonce && pow_type === 'useful_work_task' && pow_solution_work_result && pow_problem_id) {
|
|
1912
|
+
this._log('Useful work solution submitted', { problemId: pow_problem_id });
|
|
1913
|
+
const challengeContext = await store.get(`secret:${pow_nonce}`);
|
|
1914
|
+
if (challengeContext) {
|
|
1915
|
+
try {
|
|
1916
|
+
const workResult = JSON.parse(pow_solution_work_result);
|
|
1917
|
+
problemManager.integrateSolution(pow_problem_id, workResult);
|
|
1918
|
+
|
|
1919
|
+
await store.delete(`secret:${pow_nonce}`);
|
|
1920
|
+
// Accorder un ticket de passage comme pour un PoW normal
|
|
1921
|
+
const ticket = "valid_ticket_placeholder"; // Générer un vrai ticket ici
|
|
1922
|
+
return { action: 'redirect', path: path, score: 0, vector: { challenge_solved: 100 }, cookie: { name: 'pow_clearance', value: ticket, options: { httpOnly: true, secure: this.isProduction, maxAge: 60000 } } };
|
|
1923
|
+
|
|
1924
|
+
} catch (e) {
|
|
1925
|
+
this._log('Error parsing useful work solution', { error: e.message });
|
|
1926
|
+
}
|
|
1927
|
+
}
|
|
1928
|
+
// Si la validation échoue, on pénalise fortement
|
|
1929
|
+
suspicionVector.honeypotScore = 100;
|
|
1930
|
+
finalScore = this.calculateFinalScore(suspicionVector);
|
|
1914
1931
|
}
|
|
1915
1932
|
// --- FIN DE LA LOGIQUE DE PRIORITÉ ---
|
|
1916
1933
|
|
|
@@ -1961,13 +1978,25 @@ export class FingerprintEngine {
|
|
|
1961
1978
|
const nonce = crypto.randomBytes(16).toString("hex");
|
|
1962
1979
|
const clientSecret = crypto.randomBytes(16).toString("hex");
|
|
1963
1980
|
|
|
1964
|
-
//
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1981
|
+
// Pour les scores élevés, on choisit aléatoirement entre un challenge de travail utile et un PoW classique.
|
|
1982
|
+
// Cela rend l'automatisation plus difficile pour un attaquant.
|
|
1983
|
+
if (isSuspicious && this.securityConfig.enableUsefulWork && Math.random() > 0.5) {
|
|
1984
|
+
this._log('Issuing a useful work challenge', { finalScore });
|
|
1985
|
+
|
|
1986
|
+
const { problemId, task } = problemManager.dispatchWork(suspicionFactor);
|
|
1968
1987
|
|
|
1969
|
-
|
|
1970
|
-
|
|
1988
|
+
await store.set(`secret:${nonce}`, { clientSecret, originalPath: path }, 300);
|
|
1989
|
+
|
|
1990
|
+
const challengePayload = {
|
|
1991
|
+
challenge: {
|
|
1992
|
+
type: 'useful_work_task',
|
|
1993
|
+
nonce: nonce,
|
|
1994
|
+
clientSecret: clientSecret,
|
|
1995
|
+
usefulWorkTask: { problemId, task }
|
|
1996
|
+
}
|
|
1997
|
+
};
|
|
1998
|
+
return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 404, body: challengePayload };
|
|
1999
|
+
} else if (isSuspicious) { // Pour les scores bas/moyens ou si le travail utile n'est pas choisi
|
|
1971
2000
|
// Generate some trap URLs to embed in the challenge page.
|
|
1972
2001
|
// These links are visually hidden but present in the DOM to trap bots.
|
|
1973
2002
|
const trapUrls = Array.from({ length: 3 }, () => generateTrapUrl(nonce));
|
|
@@ -1990,12 +2019,15 @@ export class FingerprintEngine {
|
|
|
1990
2019
|
memDifficulty,
|
|
1991
2020
|
cpuTarget: cpuChallengeDetails.target
|
|
1992
2021
|
});
|
|
2022
|
+
// (NOUVEAU) On stocke le fingerprint de la requête qui a déclenché le challenge.
|
|
2023
|
+
const originalFingerprint = requestContext.headers['x-device-fingerprint'] || getCompositeDeviceHash(requestContext);
|
|
1993
2024
|
|
|
1994
2025
|
// Store the entire challenge context with a short TTL (e.g., 5 minutes)
|
|
1995
2026
|
await store.set(`secret:${nonce}`, {
|
|
1996
2027
|
clientSecret,
|
|
1997
2028
|
cpuTarget: cpuChallengeDetails.target,
|
|
1998
2029
|
suspicionScore: finalScore, // *** FIX: Store the score that triggered the challenge ***
|
|
2030
|
+
fingerprint: originalFingerprint, // *** NOUVEAU ***
|
|
1999
2031
|
memDifficulty: memDifficulty,
|
|
2000
2032
|
originalPath: path, // *** FIX: Store the original path ***
|
|
2001
2033
|
}, this.securityConfig.challengeTtl || 300); // NOUVEAU: TTL configurable (5min par défaut)
|
|
@@ -2017,7 +2049,7 @@ export class FingerprintEngine {
|
|
|
2017
2049
|
}
|
|
2018
2050
|
|
|
2019
2051
|
// Check if the request is an API request to return a JSON challenge
|
|
2020
|
-
const isApi = requestContext.rawReq && this.securityConfig
|
|
2052
|
+
const isApi = requestContext.rawReq && this.securityConfig?.isApiRequest?.(requestContext.rawReq);
|
|
2021
2053
|
|
|
2022
2054
|
if (isApi) {
|
|
2023
2055
|
// For API clients, send a JSON response with challenge details.
|
|
@@ -2110,6 +2142,176 @@ export class FingerprintEngine {
|
|
|
2110
2142
|
}
|
|
2111
2143
|
}
|
|
2112
2144
|
|
|
2145
|
+
const staticExtensions = new RegExp(
|
|
2146
|
+
"\\.(js|css|png|jpg|jpeg|gif|svg|mp3|webp|ico|woff|woff2|ttf|otf|map|json|manifest|webmanifest)$",
|
|
2147
|
+
"i",
|
|
2148
|
+
);
|
|
2149
|
+
const isStaticResource = (path) => staticExtensions.test(path);
|
|
2150
|
+
|
|
2151
|
+
|
|
2152
|
+
/**
|
|
2153
|
+
* Détermine le TTL optimal pour un ticket en utilisant un algorithme génétique multi-objectifs.
|
|
2154
|
+
* @param {number} suspicionScore - Le score de suspicion de la requête.
|
|
2155
|
+
* @returns {number} Le TTL optimal calculé en millisecondes.
|
|
2156
|
+
*/
|
|
2157
|
+
function determineOptimalTicketTtl(suspicionScore) {
|
|
2158
|
+
// Définir les bornes pour la durée de vie du ticket (5 minutes à 24 heures)
|
|
2159
|
+
const MIN_TTL = 300000;
|
|
2160
|
+
const MAX_TTL = 86400000;
|
|
2161
|
+
|
|
2162
|
+
const solverFunction = () => {
|
|
2163
|
+
const fitnessFunction = Optimization.Operators.createOptimalTtlEvaluator({ suspicionScore });
|
|
2164
|
+
|
|
2165
|
+
// Un "individu" est simplement une valeur de TTL en millisecondes.
|
|
2166
|
+
const createIndividual = () => MIN_TTL + Math.random() * (MAX_TTL - MIN_TTL);
|
|
2167
|
+
const crossover = (ttl1, ttl2) => (ttl1 + ttl2) / 2;
|
|
2168
|
+
const mutate = (ttl) => {
|
|
2169
|
+
const newTtl = ttl + (Math.random() - 0.5) * (MAX_TTL - MIN_TTL) * 0.1; // Mutation de +/- 10% max
|
|
2170
|
+
return Math.max(MIN_TTL, Math.min(MAX_TTL, newTtl));
|
|
2171
|
+
};
|
|
2172
|
+
|
|
2173
|
+
const paretoFront = Optimization.geneticAlgorithmMultiObjective(
|
|
2174
|
+
createIndividual,
|
|
2175
|
+
fitnessFunction,
|
|
2176
|
+
crossover,
|
|
2177
|
+
mutate,
|
|
2178
|
+
{
|
|
2179
|
+
generations: 40,
|
|
2180
|
+
populationSize: 30,
|
|
2181
|
+
}
|
|
2182
|
+
);
|
|
2183
|
+
|
|
2184
|
+
// Pour runMultiple, on doit retourner un objet avec une propriété "fitness" ou "energy".
|
|
2185
|
+
// Pour un front de Pareto, il n'y a pas de score unique. On choisit la meilleure solution
|
|
2186
|
+
// en fonction du score de suspicion et on lui assigne un score de 0 pour que runMultiple la sélectionne.
|
|
2187
|
+
if (!paretoFront || paretoFront.length === 0) {
|
|
2188
|
+
return { solution: null, fitness: Infinity };
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
// Stratégie de sélection :
|
|
2192
|
+
// Pour un score faible (< 50), on privilégie la solution avec le plus grand TTL (minimise la friction).
|
|
2193
|
+
// Pour un score élevé (>= 50), on privilégie la solution avec le plus petit TTL (minimise le risque).
|
|
2194
|
+
let bestSolutionInFront;
|
|
2195
|
+
if (suspicionScore < 50) {
|
|
2196
|
+
bestSolutionInFront = paretoFront.reduce((max, p) => Math.max(max, p.solution), 0);
|
|
2197
|
+
} else {
|
|
2198
|
+
bestSolutionInFront = paretoFront.reduce((min, p) => Math.min(min, p.solution), Infinity);
|
|
2199
|
+
}
|
|
2200
|
+
return { solution: bestSolutionInFront, fitness: 0 }; // fitness=0 car on a déjà la meilleure solution du cycle.
|
|
2201
|
+
};
|
|
2202
|
+
|
|
2203
|
+
// On exécute le solveur 20 fois pour trouver une solution plus stable et robuste.
|
|
2204
|
+
const { bestResult } = Optimization.runMultiple(solverFunction, 20);
|
|
2205
|
+
|
|
2206
|
+
if (!bestResult || !bestResult.solution || bestResult.solution === Infinity) {
|
|
2207
|
+
// Fallback : si l'algo ne retourne rien, on applique une règle simple et sûre.
|
|
2208
|
+
return Math.max(MIN_TTL, MAX_TTL - (suspicionScore / 100) * MAX_TTL);
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
// runMultiple choisit le meilleur résultat sur la base du score (ici, 0).
|
|
2212
|
+
// La "meilleure" solution dépendra du cycle qui a trouvé le meilleur compromis.
|
|
2213
|
+
return Math.round(bestResult.solution);
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
/**
|
|
2217
|
+
* Vérifie si une chaîne de caractères contient des patterns d'injection connus.
|
|
2218
|
+
* @private
|
|
2219
|
+
* @param {string} str - La chaîne à vérifier.
|
|
2220
|
+
* @param {string[]} [typesToDetect=['sql', 'log4shell', 'ssti', 'xxe', 'traversal', 'rce']] - Les types d'injections à détecter.
|
|
2221
|
+
* @returns {boolean} - True si un pattern malveillant est détecté.
|
|
2222
|
+
*/
|
|
2223
|
+
function isMalicious(str, typesToDetect = Object.keys(injectionPatterns)) {
|
|
2224
|
+
if (typeof str !== 'string') return false;
|
|
2225
|
+
|
|
2226
|
+
for (const type of typesToDetect) {
|
|
2227
|
+
const regex = injectionPatterns[type];
|
|
2228
|
+
if (regex && regex.test(str)) {
|
|
2229
|
+
return true;
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
return false;
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
// --- Middleware Proof-of-Work (Le péage) ---
|
|
2237
|
+
export { isMalicious };
|
|
2238
|
+
|
|
2239
|
+
/**
|
|
2240
|
+
* Returns a default list of security analyzers for honeypot detection.
|
|
2241
|
+
* This list can be used as a base and extended with custom rules.
|
|
2242
|
+
* Currently includes an XSS detection analyzer.
|
|
2243
|
+
* @returns {Array<Function>}
|
|
2244
|
+
*/
|
|
2245
|
+
export const default_analyzers = () => [
|
|
2246
|
+
// Analyzer for Cross-Site Scripting (XSS) detection.
|
|
2247
|
+
// It uses the 'xss' library, which should be installed by the user (`npm install xss`).
|
|
2248
|
+
// If 'xss' is not available, this analyzer will be safely ignored.
|
|
2249
|
+
xss_analyzer
|
|
2250
|
+
];
|
|
2251
|
+
|
|
2252
|
+
export const xss_analyzer = async (data) => {
|
|
2253
|
+
try {
|
|
2254
|
+
// Dynamically import the 'xss' library.
|
|
2255
|
+
// The module is loaded only once by Node's cache.
|
|
2256
|
+
const xss = (await import('xss')).default;
|
|
2257
|
+
const originalData = JSON.stringify(data);
|
|
2258
|
+
// If the sanitized string is different, it means malicious HTML/JS was found and removed.
|
|
2259
|
+
return xss(originalData) !== originalData;
|
|
2260
|
+
} catch (error) {
|
|
2261
|
+
// This catch block handles the case where the 'xss' module is not installed.
|
|
2262
|
+
if (error.code === 'ERR_MODULE_NOT_FOUND') {
|
|
2263
|
+
console.warn('[Fingerprint] Warning: The "xss" package is not installed. The default XSS analyzer is disabled. Run "npm install xss" to enable it.');
|
|
2264
|
+
// To avoid repeated warnings, we can replace this function with a no-op.
|
|
2265
|
+
this.isXssAnalyzerAvailable = false; // A flag to prevent future attempts.
|
|
2266
|
+
}
|
|
2267
|
+
return false; // In case of any error, we assume the data is not malicious.
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
2270
|
+
/**
|
|
2271
|
+
* Returns a powerful WAF (Web Application Firewall) analyzer based on ModSecurity.
|
|
2272
|
+
* This analyzer is highly effective against a wide range of attacks (SQLi, XSS, RCE, etc.)
|
|
2273
|
+
* by using the OWASP Core Rule Set.
|
|
2274
|
+
*
|
|
2275
|
+
* **Note:** This is an optional and advanced feature.
|
|
2276
|
+
* 1. The user must install the package: `npm install modsecurity-nodejs`
|
|
2277
|
+
* 2. ModSecurity rules (like the OWASP CRS) must be available on the server.
|
|
2278
|
+
*
|
|
2279
|
+
* If the package is not installed, the analyzer will be safely ignored.
|
|
2280
|
+
*
|
|
2281
|
+
* @param {string} rulesPath - The path to the ModSecurity rules configuration file (e.g., `crs-setup.conf`).
|
|
2282
|
+
* @returns {Function} An analyzer function to be used in the `honeypot.analyzers` array.
|
|
2283
|
+
*/
|
|
2284
|
+
export const modsecurity_analyzer = (rulesPath) => {
|
|
2285
|
+
let wafInstance = null; // Singleton instance for the WAF
|
|
2286
|
+
|
|
2287
|
+
return async (data) => {
|
|
2288
|
+
if (!rulesPath) {
|
|
2289
|
+
console.warn('[Fingerprint] ModSecurity analyzer disabled: `rulesPath` is not provided.');
|
|
2290
|
+
return false;
|
|
2291
|
+
}
|
|
2292
|
+
|
|
2293
|
+
try {
|
|
2294
|
+
if (!wafInstance) {
|
|
2295
|
+
// Dynamically import the library only when needed.
|
|
2296
|
+
const { ModSecurity } = await import('modsecurity-nodejs');
|
|
2297
|
+
wafInstance = new ModSecurity();
|
|
2298
|
+
wafInstance.init();
|
|
2299
|
+
wafInstance.addRules(rulesPath);
|
|
2300
|
+
console.log('[Fingerprint] ModSecurity WAF analyzer initialized successfully.');
|
|
2301
|
+
}
|
|
2302
|
+
|
|
2303
|
+
// The `transaction` method checks the data against the loaded rules.
|
|
2304
|
+
// It returns `null` if no rules are matched, or an object with intervention details if a threat is found.
|
|
2305
|
+
const result = wafInstance.transaction(data);
|
|
2306
|
+
return result !== null; // A non-null result means a threat was detected.
|
|
2307
|
+
} catch (error) {
|
|
2308
|
+
if (error.code === 'ERR_MODULE_NOT_FOUND') {
|
|
2309
|
+
console.warn('[Fingerprint] Warning: "modsecurity-nodejs" is not installed. The WAF analyzer is disabled. Run "npm install modsecurity-nodejs" to enable it.');
|
|
2310
|
+
}
|
|
2311
|
+
return false; // Assume data is safe if any error occurs.
|
|
2312
|
+
}
|
|
2313
|
+
};
|
|
2314
|
+
};
|
|
2113
2315
|
/**
|
|
2114
2316
|
* Returns a default list of whitelisting rules for common and legitimate web crawlers.
|
|
2115
2317
|
* This list can be used as a base and extended with custom rules.
|
|
@@ -2228,9 +2430,8 @@ export const powMiddleware = (securityConfig) => {
|
|
|
2228
2430
|
|
|
2229
2431
|
// Provide a default for isApiRequest if not specified by the user.
|
|
2230
2432
|
// This makes API challenge handling work more seamlessly out-of-the-box.
|
|
2231
|
-
if (!securityConfig
|
|
2232
|
-
|
|
2233
|
-
securityConfig.thresholds.isApiRequest = (req) =>
|
|
2433
|
+
if (!securityConfig?.isApiRequest) {
|
|
2434
|
+
securityConfig.isApiRequest = (req) =>
|
|
2234
2435
|
req.headers?.accept?.includes('application/json');
|
|
2235
2436
|
}
|
|
2236
2437
|
|
|
@@ -2242,9 +2443,10 @@ export const powMiddleware = (securityConfig) => {
|
|
|
2242
2443
|
query: req.query,
|
|
2243
2444
|
body: req.body,
|
|
2244
2445
|
headers: req.headers,
|
|
2245
|
-
isStatic: isStaticResource(req.path),
|
|
2446
|
+
isStatic: securityConfig?.isStaticResource?.(req.path) || isStaticResource(req.path),
|
|
2246
2447
|
// Pass the original request object for the isApiRequest function
|
|
2247
2448
|
rawReq: req,
|
|
2449
|
+
requestTimestamp: Date.now(), // Timestamp de début de requête
|
|
2248
2450
|
// Add the newly required properties for full decoupling
|
|
2249
2451
|
rawHeaders: req.rawHeaders,
|
|
2250
2452
|
// Pass the raw request object for advanced inspection (e.g., JA3)
|
|
@@ -2306,6 +2508,7 @@ export const __internal = {
|
|
|
2306
2508
|
getBehaviorScore, // Expose for testing
|
|
2307
2509
|
getCrossLayerInconsistency, // Expose for testing
|
|
2308
2510
|
// Expose page generators for security testing
|
|
2511
|
+
getTimeInconsistencyScore,
|
|
2309
2512
|
generateCpuTargetChallengePage,
|
|
2310
2513
|
generateCombinedPoWChallengePage,
|
|
2311
2514
|
};
|
package/library.js
CHANGED
|
@@ -289,20 +289,23 @@ const Optimization = {
|
|
|
289
289
|
* @param {number} numCycles - Le nombre total de cycles à exécuter.
|
|
290
290
|
* @param {boolean} [logProgress=false] - Si true, affiche la progression dans la console.
|
|
291
291
|
* @param {object} [options={}] - Options pour la parallélisation.
|
|
292
|
-
* @param {number} [options.concurrency] - Le nombre de workers à utiliser en parallèle. Par défaut, le nombre de cœurs CPU.
|
|
292
|
+
* @param {number} [options.concurrency] - Le nombre de workers à utiliser en parallèle. Par défaut, le nombre de cœurs CPU.
|
|
293
|
+
* @param {function(number): Array<any>} [options.workerDataGenerator] - Une fonction qui, pour chaque cycle (index), génère les arguments spécifiques à passer au solveur. Si non fournie, `baseSolverArgs` est utilisé tel quel.
|
|
293
294
|
* @returns {Promise<{bestResult: object, stats: {scores: Array<number>, average: number, stdDev: number}}>} Le meilleur résultat et des statistiques.
|
|
294
295
|
*/
|
|
295
296
|
async runMultipleParallel(
|
|
296
297
|
solverName,
|
|
297
|
-
baseSolverArgs,
|
|
298
|
+
baseSolverArgs = [], // Default to empty array if not provided
|
|
298
299
|
numCycles,
|
|
299
300
|
logProgress = false,
|
|
300
301
|
options = {},
|
|
301
302
|
) {
|
|
302
303
|
const concurrency = options.concurrency || os.cpus().length;
|
|
304
|
+
const workerDataGenerator = options.workerDataGenerator;
|
|
305
|
+
|
|
303
306
|
if (logProgress) {
|
|
304
307
|
console.log(
|
|
305
|
-
` (Utilisation d'un pool de ${concurrency} workers pour ${numCycles} cycles)`,
|
|
308
|
+
` (Utilisation d'un pool de ${concurrency} workers pour ${numCycles} cycles avec le solveur ${solverName})`,
|
|
306
309
|
);
|
|
307
310
|
}
|
|
308
311
|
|
|
@@ -317,18 +320,15 @@ const Optimization = {
|
|
|
317
320
|
|
|
318
321
|
const workerData = {
|
|
319
322
|
solverName,
|
|
320
|
-
solverArgs:
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
x: Math.random() * 100,
|
|
324
|
-
y: Math.random() * 100,
|
|
325
|
-
})),
|
|
326
|
-
...baseSolverArgs,
|
|
327
|
-
],
|
|
323
|
+
solverArgs: workerDataGenerator
|
|
324
|
+
? workerDataGenerator(taskIndex)
|
|
325
|
+
: baseSolverArgs,
|
|
328
326
|
};
|
|
329
327
|
|
|
330
|
-
const result = await new Promise((resolve, reject) => {
|
|
331
|
-
|
|
328
|
+
const result = await new Promise(async (resolve, reject) => {
|
|
329
|
+
// Le chemin du worker doit être absolu ou relatif au fichier appelant.
|
|
330
|
+
// On utilise import.meta.url pour résoudre le chemin de manière fiable.
|
|
331
|
+
const worker = new Worker(new URL('./optimization.worker.js', import.meta.url), { workerData });
|
|
332
332
|
worker.on("message", resolve);
|
|
333
333
|
worker.on("error", reject);
|
|
334
334
|
worker.on("exit", (code) => {
|
|
@@ -1313,15 +1313,21 @@ Optimization.Operators.createOptimalTtlEvaluator = ({ suspicionScore }) => {
|
|
|
1313
1313
|
* @param {string} numberString - Une chaîne de chiffres (ex: "123456789").
|
|
1314
1314
|
* @returns {number} Un score de déviation (0 = parfait, > 0.15 = suspect).
|
|
1315
1315
|
*/
|
|
1316
|
-
Optimization.Operators.benfordTest = (
|
|
1317
|
-
|
|
1318
|
-
|
|
1316
|
+
Optimization.Operators.benfordTest = (numbers) => {
|
|
1317
|
+
if (!Array.isArray(numbers)) {
|
|
1318
|
+
// Si l'entrée n'est pas un tableau, on ne peut pas l'analyser.
|
|
1319
|
+
return 0;
|
|
1320
|
+
}
|
|
1321
|
+
const leadingDigits = numbers.map(n => String(n).trim().charAt(0))
|
|
1322
|
+
.filter(d => d >= '1' && d <= '9'); // On ne garde que les chiffres de 1 à 9.
|
|
1323
|
+
|
|
1324
|
+
if (leadingDigits.length < 10) {
|
|
1319
1325
|
return 0; // Pas assez de données pour un test fiable
|
|
1320
1326
|
}
|
|
1321
1327
|
|
|
1322
1328
|
const counts = Array(10).fill(0);
|
|
1323
|
-
for (let i = 0; i <
|
|
1324
|
-
counts[parseInt(
|
|
1329
|
+
for (let i = 0; i < leadingDigits.length; i++) {
|
|
1330
|
+
counts[parseInt(leadingDigits[i], 10)]++;
|
|
1325
1331
|
}
|
|
1326
1332
|
|
|
1327
1333
|
// Distribution attendue selon la loi de Benford pour le premier chiffre
|
|
@@ -1332,7 +1338,7 @@ Optimization.Operators.benfordTest = (numberString) => {
|
|
|
1332
1338
|
|
|
1333
1339
|
let totalDeviation = 0;
|
|
1334
1340
|
for (let i = 1; i <= 9; i++) {
|
|
1335
|
-
const observedFrequency = (counts[i] /
|
|
1341
|
+
const observedFrequency = (counts[i] / leadingDigits.length) * 100;
|
|
1336
1342
|
const expectedFrequency = benfordDistribution[i];
|
|
1337
1343
|
totalDeviation += Math.pow(observedFrequency - expectedFrequency, 2);
|
|
1338
1344
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anonympins/fingerprint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "An HTTP(S) client mitigation and anti-bot protection library for Node.js/Express, based on digital fingerprinting and dynamic Proof-of-Work (PoW) challenges.",
|
|
5
5
|
"main": "fingerprint.js",
|
|
6
6
|
"type": "module",
|
package/pow.solver.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file @/pow.solver.js
|
|
3
3
|
* @description Contient les fonctions côté client pour résoudre les différents types de challenges Proof-of-Work.
|
|
4
|
+
* IMPORTANT: Pour les tâches d'optimisation, ce fichier a besoin d'accéder aux algorithmes de `library.js`.
|
|
5
|
+
* Dans un vrai projet, il faudrait bundler une version client de `library.js` et l'importer ici.
|
|
6
|
+
* Pour cet exemple, nous allons copier/coller les fonctions nécessaires.
|
|
4
7
|
* Fichier compatible à la fois avec l'import de modules ES6 et l'injection directe dans un script HTML.
|
|
5
8
|
*/
|
|
6
9
|
|
|
@@ -169,13 +172,164 @@ export async function solveTsp(cities, targetMaxDistance) {
|
|
|
169
172
|
return { path: solutionPath, distance: solutionDistance };
|
|
170
173
|
}
|
|
171
174
|
|
|
175
|
+
// --- Fonctions d'optimisation copiées/adaptées de library.js pour le client ---
|
|
176
|
+
|
|
177
|
+
const ClientOptimizers = {
|
|
178
|
+
simulatedAnnealing(initialSolution, evaluator, neighbor, iterations, temp, cooling) {
|
|
179
|
+
let currentSolution = initialSolution;
|
|
180
|
+
let currentEnergy = evaluator(currentSolution);
|
|
181
|
+
let temperature = temp;
|
|
182
|
+
|
|
183
|
+
for (let i = 0; i < iterations; i++) {
|
|
184
|
+
const newSolution = neighbor(currentSolution);
|
|
185
|
+
const newEnergy = evaluator(newSolution);
|
|
186
|
+
if (newEnergy < currentEnergy || Math.random() < Math.exp((currentEnergy - newEnergy) / temperature)) {
|
|
187
|
+
currentSolution = newSolution;
|
|
188
|
+
currentEnergy = newEnergy;
|
|
189
|
+
}
|
|
190
|
+
temperature *= cooling;
|
|
191
|
+
}
|
|
192
|
+
return { solution: currentSolution, energy: currentEnergy };
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
geneticAlgorithm(createIndividual, fitness, crossover, mutate, generations, popSize) {
|
|
196
|
+
let population = Array.from({ length: popSize }, () => {
|
|
197
|
+
const chromosome = createIndividual();
|
|
198
|
+
return { chromosome, fitness: fitness(chromosome) };
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
for (let gen = 0; gen < generations; gen++) {
|
|
202
|
+
population.sort((a, b) => a.fitness - b.fitness);
|
|
203
|
+
const newPopulation = [population[0]]; // Elitism
|
|
204
|
+
while (newPopulation.length < popSize) {
|
|
205
|
+
const p1 = population[Math.floor(Math.random() * (popSize / 2))];
|
|
206
|
+
const p2 = population[Math.floor(Math.random() * (popSize / 2))];
|
|
207
|
+
let offspring = crossover(p1.chromosome, p2.chromosome);
|
|
208
|
+
if (Math.random() < 0.1) offspring = mutate(offspring);
|
|
209
|
+
newPopulation.push({ chromosome: offspring, fitness: fitness(offspring) });
|
|
210
|
+
}
|
|
211
|
+
population = newPopulation;
|
|
212
|
+
}
|
|
213
|
+
return population;
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Résout une unité de travail utile (Useful Work Unit).
|
|
219
|
+
* @param {object} task - La tâche envoyée par le serveur.
|
|
220
|
+
* @returns {Promise<object>} Le résultat du calcul.
|
|
221
|
+
*/
|
|
222
|
+
async function solveUsefulWorkTask(task) {
|
|
223
|
+
await new Promise(r => setTimeout(r, 10)); // Yield thread
|
|
224
|
+
|
|
225
|
+
switch (task.type) {
|
|
226
|
+
case 'simulated_annealing_iterations': {
|
|
227
|
+
const { cities } = task.payload;
|
|
228
|
+
const distance = (c1, c2) => Math.sqrt(Math.pow(c1.x - c2.x, 2) + Math.pow(c1.y - c2.y, 2));
|
|
229
|
+
const evaluator = (path) => {
|
|
230
|
+
let total = 0;
|
|
231
|
+
for (let i = 0; i < path.length - 1; i++) total += distance(cities[path[i]], cities[path[i + 1]]);
|
|
232
|
+
total += distance(cities[path[path.length - 1]], cities[path[0]]);
|
|
233
|
+
return total;
|
|
234
|
+
};
|
|
235
|
+
const neighbor = (path) => {
|
|
236
|
+
const newPath = [...path];
|
|
237
|
+
const [i, j] = [Math.floor(Math.random() * path.length), Math.floor(Math.random() * path.length)];
|
|
238
|
+
[newPath[i], newPath[j]] = [newPath[j], newPath[i]];
|
|
239
|
+
return newPath;
|
|
240
|
+
};
|
|
241
|
+
const initialSolution = task.initialSolution || Array.from({ length: cities.length }, (_, i) => i).sort(() => 0.5 - Math.random());
|
|
242
|
+
|
|
243
|
+
return ClientOptimizers.simulatedAnnealing(initialSolution, evaluator, neighbor, task.iterations, task.payload.options.initialTemperature, task.payload.options.coolingRate);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
case 'genetic_algorithm_generations': {
|
|
247
|
+
const { assets, maxVolatility } = task.payload;
|
|
248
|
+
const fitness = (weights) => {
|
|
249
|
+
const total = weights.reduce((s, w) => s + w, 0);
|
|
250
|
+
if (total === 0) return Infinity;
|
|
251
|
+
const normW = weights.map(w => w / total);
|
|
252
|
+
const ret = normW.reduce((s, w, i) => s + w * assets[i].expectedReturn, 0);
|
|
253
|
+
const vol = normW.reduce((s, w, i) => s + w * assets[i].volatility, 0);
|
|
254
|
+
if (vol > maxVolatility) return 1000 + (vol - maxVolatility);
|
|
255
|
+
return -ret;
|
|
256
|
+
};
|
|
257
|
+
const createIndividual = () => Array.from({ length: assets.length }, Math.random);
|
|
258
|
+
const crossover = (p1, p2) => p1.map((w, i) => (w + p2[i]) / 2);
|
|
259
|
+
const mutate = p => { const n = [...p], i = Math.floor(Math.random() * n.length); n[i] += (Math.random() - 0.5) * 0.2; return n.map(v => Math.max(0, v)); };
|
|
260
|
+
|
|
261
|
+
// Le client doit recréer la population si elle n'est pas fournie
|
|
262
|
+
const initialPopulation = task.initialPopulation || Array.from({ length: task.payload.options.populationSize }, () => ({ chromosome: createIndividual(), fitness: 0 }));
|
|
263
|
+
initialPopulation.forEach(p => p.fitness = fitness(p.chromosome));
|
|
264
|
+
|
|
265
|
+
const finalPopulation = ClientOptimizers.geneticAlgorithm(createIndividual, fitness, crossover, mutate, task.generations, task.payload.options.populationSize);
|
|
266
|
+
return { population: finalPopulation };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
case 'run_multiple_parallel':
|
|
270
|
+
// Côté client, on ne peut pas utiliser de vrais workers pour `runMultipleParallel`.
|
|
271
|
+
// On exécute donc une version simplifiée : un seul cycle du solveur demandé.
|
|
272
|
+
// Cela reste un travail coûteux et valide le principe du "Useful Work".
|
|
273
|
+
const { solverName, baseSolverArgs } = task;
|
|
274
|
+
const clientSolver = ClientOptimizers[solverName];
|
|
275
|
+
if (!clientSolver) throw new Error(`Solver ${solverName} not found on client.`);
|
|
276
|
+
|
|
277
|
+
// On simule l'appel avec les arguments de base.
|
|
278
|
+
// Note: `baseSolverArgs` peut contenir des options.
|
|
279
|
+
return clientSolver(...baseSolverArgs);
|
|
280
|
+
|
|
281
|
+
default:
|
|
282
|
+
throw new Error(`Unknown useful work type: ${task.type}`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Résout une tâche d'optimisation basée sur un algorithme génétique.
|
|
288
|
+
* Reçoit une population et la fait évoluer pendant un certain nombre de générations.
|
|
289
|
+
* NOTE: Cette fonction est une version simplifiée de l'AG de `library.js` adaptée au client.
|
|
290
|
+
* @param {Array<object>} initialPopulation - La population de départ.
|
|
291
|
+
* @param {number} generations - Le nombre de générations à exécuter.
|
|
292
|
+
* @returns {Promise<Array<object>>} La population finale après évolution.
|
|
293
|
+
*/
|
|
294
|
+
export async function solveOptimizationTask(initialPopulation, generations) {
|
|
295
|
+
// Fonctions AG simplifiées (croisement, mutation)
|
|
296
|
+
const crossover = (p1, p2) => p1.map((w, i) => (w + p2[i]) / 2);
|
|
297
|
+
const mutate = (p) => {
|
|
298
|
+
const newP = [...p];
|
|
299
|
+
const i = Math.floor(Math.random() * newP.length);
|
|
300
|
+
newP[i] += (Math.random() - 0.5) * 0.2;
|
|
301
|
+
return newP;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
let population = initialPopulation;
|
|
305
|
+
|
|
306
|
+
for (let gen = 0; gen < generations; gen++) {
|
|
307
|
+
// Sélection simple : on garde les 50% meilleurs
|
|
308
|
+
const parents = population.sort((a, b) => a.fitness - b.fitness).slice(0, Math.ceil(population.length / 2));
|
|
309
|
+
const newPopulation = [...parents]; // Élitisme
|
|
310
|
+
|
|
311
|
+
while (newPopulation.length < population.length) {
|
|
312
|
+
const parent1 = parents[Math.floor(Math.random() * parents.length)];
|
|
313
|
+
const parent2 = parents[Math.floor(Math.random() * parents.length)];
|
|
314
|
+
let offspring = crossover(parent1.chromosome, parent2.chromosome);
|
|
315
|
+
if (Math.random() < 0.1) offspring = mutate(offspring);
|
|
316
|
+
// La fitness sera recalculée côté serveur pour la vérification.
|
|
317
|
+
newPopulation.push({ chromosome: offspring, fitness: -1 });
|
|
318
|
+
}
|
|
319
|
+
population = newPopulation;
|
|
320
|
+
// Pause pour ne pas geler l'UI sur les longues tâches
|
|
321
|
+
if (gen % 10 === 0) await new Promise(r => setTimeout(r, 0));
|
|
322
|
+
}
|
|
323
|
+
return population;
|
|
324
|
+
}
|
|
325
|
+
|
|
172
326
|
/**
|
|
173
327
|
* Fonction principale qui reçoit un objet challenge et le résout.
|
|
174
328
|
* @param {object} challenge - L'objet challenge reçu du serveur.
|
|
175
329
|
* @returns {Promise<object>} Un objet contenant la ou les solutions.
|
|
176
330
|
*/
|
|
177
331
|
export async function solveChallenge(challenge) {
|
|
178
|
-
const { type, nonce, clientSecret, cpuTarget, memDifficulty, cities, clientIp, targetMaxDistance } = challenge;
|
|
332
|
+
const { type, nonce, clientSecret, cpuTarget, memDifficulty, cities, clientIp, targetMaxDistance, optimizationTask, usefulWorkTask } = challenge;
|
|
179
333
|
const solutions = {};
|
|
180
334
|
|
|
181
335
|
switch (type) {
|
|
@@ -219,6 +373,15 @@ export async function solveChallenge(challenge) {
|
|
|
219
373
|
solutions.tsp = tspResult.path;
|
|
220
374
|
solutions.distance = tspResult.distance;
|
|
221
375
|
break;
|
|
376
|
+
case 'optimization_task':
|
|
377
|
+
const finalPopulation = await solveOptimizationTask(optimizationTask.population, optimizationTask.generations);
|
|
378
|
+
solutions.population = finalPopulation.map(p => p.chromosome); // On ne renvoie que les chromosomes
|
|
379
|
+
break;
|
|
380
|
+
case 'useful_work_task':
|
|
381
|
+
const workResult = await solveUsefulWorkTask(usefulWorkTask.task);
|
|
382
|
+
solutions.work_result = workResult;
|
|
383
|
+
solutions.problem_id = usefulWorkTask.problemId;
|
|
384
|
+
break;
|
|
222
385
|
default:
|
|
223
386
|
throw new Error(`Unknown challenge type: ${type}`);
|
|
224
387
|
}
|