@anonympins/fingerprint 0.0.4 → 0.0.5
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 +17 -2
- package/fingerprint.js +189 -17
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ The process unfolds in three steps:
|
|
|
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
21
|
* **Inconsistency**: A low similarity score between the current fingerprint and the initial one associated with the `device_id` (cookie theft detection).
|
|
22
|
+
* **Honeypot Trap**: Detection of bots that automatically fill hidden form fields or probe for common but unused URL parameters (e.g., `?debug=true`).
|
|
22
23
|
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:
|
|
23
24
|
* **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.
|
|
24
25
|
* **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.
|
|
@@ -40,7 +41,7 @@ This module is designed for a Node.js environment.
|
|
|
40
41
|
|
|
41
42
|
### Prerequisites
|
|
42
43
|
|
|
43
|
-
Ensure you have
|
|
44
|
+
Ensure you have middleware for parsing cookies (like `cookie-parser`) and request bodies (like `express.json` and `express.urlencoded`) set up in your Express application *before* the `powMiddleware`.
|
|
44
45
|
|
|
45
46
|
### Configuration
|
|
46
47
|
|
|
@@ -56,11 +57,14 @@ The `powMiddleware` requires a configuration object defining the weights of susp
|
|
|
56
57
|
|
|
57
58
|
```javascript
|
|
58
59
|
import express from 'express';
|
|
60
|
+
import bodyParser from 'body-parser';
|
|
59
61
|
import cookieParser from 'cookie-parser';
|
|
60
62
|
import { powMiddleware /*, configurePow */ } from './fingerprint.js'; // Adjust the path
|
|
61
63
|
|
|
62
64
|
const app = express();
|
|
63
65
|
app.use(cookieParser());
|
|
66
|
+
app.use(bodyParser.json()); // For parsing application/json
|
|
67
|
+
app.use(bodyParser.urlencoded({ extended: true })); // For parsing application/x-www-form-urlencoded
|
|
64
68
|
|
|
65
69
|
// Configuration of weights and thresholds for calculating the suspicion score.
|
|
66
70
|
// These values should be adjusted based on traffic and expected user behavior.
|
|
@@ -69,7 +73,8 @@ const securityConfig = {
|
|
|
69
73
|
historyScore: 0.3, // Penalizes IP rotation (proxy)
|
|
70
74
|
rotationScore: 0.5, // Penalizes rapid fingerprint changes (user-agent, etc.)
|
|
71
75
|
headerAnomalyScore: 0.1, // Penalizes abnormal headers (missing UA, etc.)
|
|
72
|
-
inconsistencyScore: 0.8
|
|
76
|
+
inconsistencyScore: 0.8, // Strongly penalizes inconsistency between the current and initial fingerprint (stolen cookie)
|
|
77
|
+
honeypotScore: 1.0 // Strongly penalizes bots filling hidden form fields
|
|
73
78
|
},
|
|
74
79
|
thresholds: {
|
|
75
80
|
low: 20, // Score from which a CPU challenge is issued
|
|
@@ -77,6 +82,16 @@ const securityConfig = {
|
|
|
77
82
|
high: 75, // Score for a very difficult challenge
|
|
78
83
|
block: 95, // Score above which the request is blocked outright (HTTP 403)
|
|
79
84
|
isStaticResource: (req) => req.path.startsWith('/static/') // Optional: Custom function to identify static resources
|
|
85
|
+
},
|
|
86
|
+
honeypot: {
|
|
87
|
+
// List of field names that are traps for bots.
|
|
88
|
+
// These should be hidden in forms for humans, or be URL parameters your app never uses.
|
|
89
|
+
fields: ['email_confirm', 'user_nickname', 'debug', 'test_mode', 'admin'], // (Optional)
|
|
90
|
+
// List of URL paths that should never be accessed by a legitimate user.
|
|
91
|
+
// A request to one of these paths will immediately flag the device as malicious.
|
|
92
|
+
trapUrls: ['/wp-admin', '/.env', '/admin.php', '/phpmyadmin'], // (Optional)
|
|
93
|
+
// Automatically detect common SQL/NoSQL injection and RCE patterns in request values. (Optional, default: true)
|
|
94
|
+
detectInjections: true
|
|
80
95
|
}
|
|
81
96
|
};
|
|
82
97
|
|
package/fingerprint.js
CHANGED
|
@@ -398,6 +398,122 @@ function getHeaderAnomalies(context) {
|
|
|
398
398
|
};
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
+
/**
|
|
402
|
+
* Checks for submitted honeypot fields to detect bots.
|
|
403
|
+
* @param {object} context - The request context.
|
|
404
|
+
* @param {object} honeypotConfig - The honeypot configuration.
|
|
405
|
+
* @returns {{honeypotScore: number}}
|
|
406
|
+
*/
|
|
407
|
+
function getHoneypotScore(context, honeypotConfig = {}) {
|
|
408
|
+
const { fields = [], trapUrls = [], detectInjections = true } = honeypotConfig;
|
|
409
|
+
|
|
410
|
+
// 1. Check for trap URL access
|
|
411
|
+
if (trapUrls.some(trap => context.path.startsWith(trap))) {
|
|
412
|
+
return { honeypotScore: 100 };
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (fields.length === 0 && !detectInjections) {
|
|
416
|
+
return { honeypotScore: 0 };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Check both query parameters (for URL probing) and the request body (for hidden form fields).
|
|
420
|
+
const queryData =
|
|
421
|
+
context.query instanceof URLSearchParams
|
|
422
|
+
? Object.fromEntries(context.query.entries())
|
|
423
|
+
: context.query || {};
|
|
424
|
+
const bodyData = context.body || {};
|
|
425
|
+
|
|
426
|
+
// 2. Check for honeypot field names
|
|
427
|
+
for (const field of fields) {
|
|
428
|
+
// A bot is trapped if the field exists in either the query OR the body.
|
|
429
|
+
if (
|
|
430
|
+
Object.prototype.hasOwnProperty.call(queryData, field) ||
|
|
431
|
+
Object.prototype.hasOwnProperty.call(bodyData, field)
|
|
432
|
+
) {
|
|
433
|
+
return { honeypotScore: 100 }; // A bot fell into the trap, maximum score.
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// 3. Check for injection attempts in values
|
|
438
|
+
if (detectInjections) {
|
|
439
|
+
// Regex for common SQL injection patterns
|
|
440
|
+
const sqlRegex = new RegExp(
|
|
441
|
+
"('|\"|;|--|#|/\\*.*\\*/)|\\b(union|select|insert|update|delete|drop|truncate)\\b",
|
|
442
|
+
"i"
|
|
443
|
+
);
|
|
444
|
+
// Regex for common NoSQL (MongoDB) injection patterns (e.g., keys starting with '$')
|
|
445
|
+
const nosqlKeyRegex = /"\$[^"]*":/;
|
|
446
|
+
// Regex for common Remote Code Execution (RCE) patterns
|
|
447
|
+
const rceRegex = new RegExp(
|
|
448
|
+
// File traversal, command execution functions, and shell commands
|
|
449
|
+
"(\\.\\./|\\.\\.\\\\)|\\b(exec|system|shell_exec|passthru|popen|proc_open|eval|assert|require|include)(_once)?\\s*\\(|\\b(wget|curl|bash|sh|powershell)\\b",
|
|
450
|
+
"i"
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
const inspect = (obj) => {
|
|
454
|
+
for (const key in obj) {
|
|
455
|
+
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
456
|
+
const value = obj[key];
|
|
457
|
+
if (typeof value === 'string') {
|
|
458
|
+
if (rceRegex.test(value) || sqlRegex.test(value)) return true;
|
|
459
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
460
|
+
// For NoSQL, we check the stringified version of the object to find keys like "$gt"
|
|
461
|
+
// This is more accurate when done on the object itself.
|
|
462
|
+
if (nosqlKeyRegex.test(JSON.stringify(value))) return true;
|
|
463
|
+
if (inspect(value)) return true;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return false;
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
if (inspect(queryData)) {
|
|
471
|
+
return { honeypotScore: 100 };
|
|
472
|
+
}
|
|
473
|
+
if (inspect(bodyData)) {
|
|
474
|
+
return { honeypotScore: 100 };
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
return { honeypotScore: 0 };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const trapUrlTemplates = [
|
|
482
|
+
'/includes/config-{RANDOM}.php', // Classic PHP config file
|
|
483
|
+
'/.env.{RANDOM}', // Environment file
|
|
484
|
+
'/backups/db_backup_{RANDOM}.sql.gz', // Database backup
|
|
485
|
+
'/api/v1/internal/status?trace={RANDOM}', // Internal API endpoint
|
|
486
|
+
'/_private/deploy_key_{RANDOM}.pem', // Private key file
|
|
487
|
+
'/logs/app_error_{RANDOM}.log', // Log file
|
|
488
|
+
'/.git/config_{RANDOM}' // Exposed git config variant
|
|
489
|
+
];
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Generates a signed trap URL.
|
|
493
|
+
* @param {string} nonce - The nonce to sign the URL with.
|
|
494
|
+
* @returns {string} The trap URL.
|
|
495
|
+
*/
|
|
496
|
+
function generateTrapUrl(nonce) {
|
|
497
|
+
// Pick a random template to diversify the traps
|
|
498
|
+
const template = trapUrlTemplates[Math.floor(Math.random() * trapUrlTemplates.length)];
|
|
499
|
+
const randomPart = crypto.randomBytes(8).toString('hex');
|
|
500
|
+
const path = template.replace('{RANDOM}', randomPart);
|
|
501
|
+
|
|
502
|
+
const signature = crypto.createHmac('sha256', getPowSecret()).update(nonce + path).digest('hex').substring(0, 16);
|
|
503
|
+
return `${path}?sig=${signature}`;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Verifies if a given path is a valid trap URL for a given nonce.
|
|
508
|
+
* @param {string} path - The request path.
|
|
509
|
+
* @param {string} signature - The signature from the query.
|
|
510
|
+
* @param {string} nonce - The nonce to verify against.
|
|
511
|
+
* @returns {boolean}
|
|
512
|
+
*/
|
|
513
|
+
function verifyTrapUrl(path, signature, nonce) {
|
|
514
|
+
const expectedSignature = crypto.createHmac('sha256', getPowSecret()).update(nonce + path).digest('hex').substring(0, 16);
|
|
515
|
+
return signature === expectedSignature;
|
|
516
|
+
}
|
|
401
517
|
/**
|
|
402
518
|
* @typedef {object} IStore
|
|
403
519
|
* @property {(key: string) => Promise<any>} get
|
|
@@ -443,7 +559,6 @@ async function resolveRequestIdentity(context) {
|
|
|
443
559
|
let consistencyScore = 1.0; // 1.0 = perfectly consistent
|
|
444
560
|
let deviceData = null;
|
|
445
561
|
let newCookie = null;
|
|
446
|
-
|
|
447
562
|
if (deviceId) {
|
|
448
563
|
deviceData = await store.get(`device:${deviceId}`);
|
|
449
564
|
}
|
|
@@ -545,10 +660,10 @@ async function getBehavioralIndicators(context, deviceData) {
|
|
|
545
660
|
/**
|
|
546
661
|
* Returns a vector of raw (unweighted) suspicion scores.
|
|
547
662
|
* @param {object} context - The request context object.
|
|
548
|
-
* @returns {Promise<{historyScore: number, rotationScore: number, headerAnomalyScore: number, inconsistencyScore: number}>}
|
|
663
|
+
* @returns {Promise<{historyScore: number, rotationScore: number, headerAnomalyScore: number, inconsistencyScore: number, honeypotScore: number}>}
|
|
549
664
|
*/
|
|
550
|
-
export const getSuspicionVector = async (context) => {
|
|
551
|
-
|
|
665
|
+
export const getSuspicionVector = async (context, securityConfig) => {
|
|
666
|
+
const { deviceId, deviceData, consistencyScore, newCookie } = await resolveRequestIdentity(context);
|
|
552
667
|
|
|
553
668
|
const clientIp = context.clientIp;
|
|
554
669
|
|
|
@@ -570,10 +685,11 @@ export const getSuspicionVector = async (context) => {
|
|
|
570
685
|
const behavioral = await getBehavioralIndicators(context, deviceData);
|
|
571
686
|
const { headerAnomalyScore } = getHeaderAnomalies(context);
|
|
572
687
|
// Calculate the inconsistency score here, separately.
|
|
573
|
-
const inconsistencyScore = Math.min(100, Math.max(0, (1 - consistencyScore) * 200));
|
|
688
|
+
const inconsistencyScore = Math.min(100, Math.max(0, (1 - consistencyScore) * 200)); // Amplified score
|
|
574
689
|
|
|
575
690
|
|
|
576
691
|
// Save the updated device state to the store
|
|
692
|
+
// Note: deviceData.ips is a Set, which may not serialize correctly in all stores (e.g., JSON). A Redis store should handle this via custom serialization or by converting to an array.
|
|
577
693
|
await store.set(`device:${deviceId}`, deviceData);
|
|
578
694
|
|
|
579
695
|
return { ...behavioral, headerAnomalyScore, inconsistencyScore };
|
|
@@ -595,17 +711,20 @@ const MAX_RAPID_CHANGES_PER_DEVICE = 3; // Number of rapid fingerprint changes a
|
|
|
595
711
|
* Uses FingerprintBuilder to create a fingerprint based on headers
|
|
596
712
|
* and IP, making spoofing more complex (requires changing the entire stack).
|
|
597
713
|
*/
|
|
598
|
-
export const identifyRequest = async (req, res) => {
|
|
714
|
+
export const identifyRequest = (securityConfig) => async (req, res) => {
|
|
599
715
|
// This function now acts as a lightweight wrapper around the engine's identifyRequest method.
|
|
600
716
|
// It requires a default configuration to work.
|
|
601
|
-
const
|
|
602
|
-
weights: { historyScore: 0.3, rotationScore: 0.5, headerAnomalyScore: 0.1, inconsistencyScore: 0.8 },
|
|
603
|
-
thresholds: { low: 20, medium: 40, high: 75 }
|
|
717
|
+
const config = securityConfig || {
|
|
718
|
+
weights: { historyScore: 0.3, rotationScore: 0.5, headerAnomalyScore: 0.1, inconsistencyScore: 0.8, honeypotScore: 1.0 },
|
|
719
|
+
thresholds: { low: 20, medium: 40, high: 75 },
|
|
720
|
+
honeypot: { fields: [] } // Ensure honeypot config exists to prevent errors
|
|
604
721
|
};
|
|
605
|
-
const engine = new FingerprintEngine(
|
|
722
|
+
const engine = new FingerprintEngine(config);
|
|
606
723
|
|
|
607
724
|
const requestContext = {
|
|
608
725
|
clientIp: req.ip || req.socket?.remoteAddress || "unknown",
|
|
726
|
+
query: req.query,
|
|
727
|
+
body: req.body,
|
|
609
728
|
cookies: req.cookies,
|
|
610
729
|
headers: req.headers,
|
|
611
730
|
rawHeaders: req.rawHeaders,
|
|
@@ -744,9 +863,9 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
744
863
|
await new Promise(r => setTimeout(r, 10)); // Yield to update UI
|
|
745
864
|
|
|
746
865
|
let memSolution = 0;
|
|
747
|
-
const iterations = size / 16;
|
|
748
866
|
try {
|
|
749
867
|
const size = ${memoryDifficulty} * 1024 * 1024;
|
|
868
|
+
const iterations = size / 16;
|
|
750
869
|
const buffer = new Uint32Array(size / 4);
|
|
751
870
|
const seed = nonce + ":" + clientSecret;
|
|
752
871
|
let h = new TextEncoder().encode(seed).reduce((acc, v) => acc + v, 0);
|
|
@@ -822,16 +941,27 @@ class FingerprintEngine {
|
|
|
822
941
|
return { action: 'next', score: 0, vector: {} };
|
|
823
942
|
}
|
|
824
943
|
|
|
944
|
+
// Check for persisted "condemned" status early.
|
|
945
|
+
const { deviceData } = await resolveRequestIdentity(requestContext);
|
|
946
|
+
if (deviceData?.condemned) {
|
|
947
|
+
return { action: 'block', status: 403, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
|
|
948
|
+
}
|
|
949
|
+
|
|
825
950
|
// The engine now works with the context directly, no more rawReq dependency here.
|
|
826
|
-
const suspicionVector = await __internal.getSuspicionVector(requestContext);
|
|
951
|
+
const suspicionVector = await __internal.getSuspicionVector(requestContext, this.securityConfig);
|
|
952
|
+
const { honeypotScore } = getHoneypotScore(requestContext, this.securityConfig.honeypot);
|
|
953
|
+
suspicionVector.honeypotScore = honeypotScore;
|
|
827
954
|
|
|
828
955
|
const finalScore =
|
|
829
956
|
suspicionVector.historyScore * (weights.historyScore || 0) +
|
|
830
957
|
suspicionVector.rotationScore * (weights.rotationScore || 0) +
|
|
831
958
|
suspicionVector.headerAnomalyScore * (weights.headerAnomalyScore || 0) +
|
|
832
|
-
suspicionVector.inconsistencyScore * (weights.inconsistencyScore || 0)
|
|
959
|
+
suspicionVector.inconsistencyScore * (weights.inconsistencyScore || 0) +
|
|
960
|
+
honeypotScore * (weights.honeypotScore || 0);
|
|
833
961
|
|
|
834
|
-
const
|
|
962
|
+
const isBlocked = finalScore >= (thresholds.block || 95);
|
|
963
|
+
|
|
964
|
+
const isSuspiciousHigh = finalScore >= thresholds.high && !isBlocked;
|
|
835
965
|
const isSuspiciousMedium = finalScore >= thresholds.medium;
|
|
836
966
|
const isSuspicious = finalScore >= thresholds.low;
|
|
837
967
|
|
|
@@ -845,6 +975,32 @@ class FingerprintEngine {
|
|
|
845
975
|
const powCookie = cookies?.pow_clearance;
|
|
846
976
|
const { pow_type, pow_nonce, pow_solution, pow_solution_cpu, pow_solution_mem } = query;
|
|
847
977
|
|
|
978
|
+
// Honeypot: Direct probing of challenge endpoints is highly suspicious.
|
|
979
|
+
// A legitimate user only hits these endpoints via the challenge page itself.
|
|
980
|
+
if (pow_nonce && !isSuspicious) {
|
|
981
|
+
if (logger) {
|
|
982
|
+
logger({ type: 'honeypot_probe', deviceId: cookies?.device_id, score: finalScore, path: path, timestamp: Date.now() });
|
|
983
|
+
}
|
|
984
|
+
suspicionVector.honeypotScore = 100; // Bot is probing. Max penalty.
|
|
985
|
+
// Recalculate score and block immediately.
|
|
986
|
+
const newFinalScore = finalScore + (100 * (weights.honeypotScore || 0));
|
|
987
|
+
return { action: 'block', status: 403, body: 'Forbidden', score: newFinalScore, vector: suspicionVector };
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// If the action is to block, we should still include the score and vector for logging/testing.
|
|
991
|
+
if (isBlocked) {
|
|
992
|
+
return { action: 'block', status: 403, body: 'Forbidden', score: finalScore, vector: suspicionVector };
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Honeypot: Check if the request is for a trap URL generated in a previous challenge.
|
|
996
|
+
// This requires a nonce from a *previous* challenge, which we can look up via the device ID.
|
|
997
|
+
const lastNonce = deviceData?.lastChallengeNonce;
|
|
998
|
+
if (lastNonce && query.sig && verifyTrapUrl(path, query.sig, lastNonce)) {
|
|
999
|
+
deviceData.condemned = true; // This device is a bot. Condemn it.
|
|
1000
|
+
await store.set(`device:${cookies.device_id}`, deviceData);
|
|
1001
|
+
return { action: 'block', status: 403, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
|
|
1002
|
+
}
|
|
1003
|
+
|
|
848
1004
|
if (isSuspicious && !isTicketValid(clientIp, powCookie)) {
|
|
849
1005
|
// --- CHALLENGE SOLUTION HANDLING ---
|
|
850
1006
|
if (pow_nonce && (pow_solution || (pow_solution_cpu && pow_solution_mem))) {
|
|
@@ -925,6 +1081,12 @@ class FingerprintEngine {
|
|
|
925
1081
|
// Store the secret with a short TTL (e.g., 5 minutes)
|
|
926
1082
|
await store.set(`secret:${nonce}`, clientSecret, 300);
|
|
927
1083
|
|
|
1084
|
+
// Associate the current challenge nonce with the device for trap URL verification later.
|
|
1085
|
+
if (deviceData) {
|
|
1086
|
+
deviceData.lastChallengeNonce = nonce;
|
|
1087
|
+
await store.set(`device:${cookies.device_id}`, deviceData);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
928
1090
|
if (logger) {
|
|
929
1091
|
logger({ type: 'challenge_issued', deviceId: cookies?.device_id, score: finalScore, timestamp: Date.now() });
|
|
930
1092
|
}
|
|
@@ -936,6 +1098,12 @@ class FingerprintEngine {
|
|
|
936
1098
|
|
|
937
1099
|
// UNIFIED CHALLENGE LOGIC for all suspicion levels (low, medium, high)
|
|
938
1100
|
if (isSuspicious) { // Couvre à la fois low et medium
|
|
1101
|
+
// Generate some trap URLs to embed in the challenge page.
|
|
1102
|
+
// These links are visually hidden but present in the DOM to trap bots.
|
|
1103
|
+
const trapUrls = Array.from({ length: 3 }, () => generateTrapUrl(nonce));
|
|
1104
|
+
const trapLinksHtml = trapUrls.map(url => `<a href="${url}" tabindex="-1">config</a>`).join(' ');
|
|
1105
|
+
|
|
1106
|
+
|
|
939
1107
|
const cpuChallengeDetails = generateCpuTargetChallenge(clientIp, nonce, suspicionFactor, path);
|
|
940
1108
|
|
|
941
1109
|
// La difficulté mémoire démarre à 0 et augmente seulement après un certain seuil de suspicion.
|
|
@@ -947,7 +1115,8 @@ class FingerprintEngine {
|
|
|
947
1115
|
const memDifficulty = Math.round(minMemDifficulty + memActivationFactor * (maxMemDifficulty - minMemDifficulty));
|
|
948
1116
|
|
|
949
1117
|
// Always use the combined page, even if memory difficulty is 0 (it will be almost instant).
|
|
950
|
-
const
|
|
1118
|
+
const trapContainer = `<div style="position:absolute;left:-9999px;top:-9999px;" aria-hidden="true">${trapLinksHtml}</div>`;
|
|
1119
|
+
const page = generateCombinedPoWChallengePage(cpuChallengeDetails, memDifficulty, clientIp, clientSecret).replace('</body>', `${trapContainer}</body>`);
|
|
951
1120
|
return { action: 'challenge', score: finalScore, vector: suspicionVector, status: 429, body: page };
|
|
952
1121
|
}
|
|
953
1122
|
}
|
|
@@ -992,12 +1161,14 @@ class FingerprintEngine {
|
|
|
992
1161
|
}
|
|
993
1162
|
await store.set(`ip:${clientIp}`, ipProfile);
|
|
994
1163
|
|
|
995
|
-
const vector = await __internal.getSuspicionVector(requestContext);
|
|
1164
|
+
const vector = await __internal.getSuspicionVector(requestContext, this.securityConfig); // Pass the config
|
|
1165
|
+
const { honeypotScore } = getHoneypotScore(requestContext, this.securityConfig.honeypot);
|
|
996
1166
|
const score =
|
|
997
1167
|
vector.historyScore * (this.securityConfig.weights.historyScore || 0.3) +
|
|
998
1168
|
vector.rotationScore * (this.securityConfig.weights.rotationScore || 0.5) +
|
|
999
1169
|
vector.headerAnomalyScore * (this.securityConfig.weights.headerAnomalyScore || 0.1) +
|
|
1000
|
-
vector.inconsistencyScore * (this.securityConfig.weights.inconsistencyScore || 0.8)
|
|
1170
|
+
vector.inconsistencyScore * (this.securityConfig.weights.inconsistencyScore || 0.8) +
|
|
1171
|
+
honeypotScore * (this.securityConfig.weights.honeypotScore || 0);
|
|
1001
1172
|
|
|
1002
1173
|
if (score >= this.securityConfig.thresholds.high) return `suspicious_high:${clientIp}`;
|
|
1003
1174
|
if (score >= this.securityConfig.thresholds.low) return `suspicious_medium:${clientIp}`; // Use medium for any suspicion
|
|
@@ -1028,6 +1199,7 @@ export const powMiddleware = (securityConfig) => {
|
|
|
1028
1199
|
path: req.path,
|
|
1029
1200
|
cookies: req.cookies,
|
|
1030
1201
|
query: req.query,
|
|
1202
|
+
body: req.body,
|
|
1031
1203
|
headers: req.headers,
|
|
1032
1204
|
isStatic: isStaticResource(req.path),
|
|
1033
1205
|
// Add the newly required properties for full decoupling
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anonympins/fingerprint",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
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",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
},
|
|
41
41
|
"homepage": "https://github.com/anonympins/fingerprint#readme",
|
|
42
42
|
"devDependencies": {
|
|
43
|
+
"body-parser": "^1.20.2",
|
|
43
44
|
"cookie-parser": "^1.4.6",
|
|
44
45
|
"express": "^4.18.2",
|
|
45
46
|
"vitest": "^4.1.11"
|