@anonympins/fingerprint 0.7.3 → 0.7.4
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/CHANGELOG.md +14 -0
- package/README.md +1 -1
- package/package.json +109 -107
- package/public/fingerprint-anti-bot.zip +0 -0
- package/src/js/fingerprint.client.js +252 -45
- package/src/js/fingerprint.client.obfuscated.js +1 -0
- package/src/js/fingerprint.js +198 -163
- package/src/js/fingerprint.worker.js +81 -0
- package/src/js/library.js +6 -2
- package/src/js/pow.solver.js +2 -3
- package/src/js/problem-manager.js +113 -113
- package/src/js/tests/fingerprint.client.init.test.js +30 -0
- package/src/js/tests/fingerprint.test.js +135 -3
- package/src/php/Challenge/ChallengeUtils.php +15 -16
- package/src/php/Tests/ChallengeUtilsTest.php +469 -453
- package/src/php/Tests/RequestUtilsTest.php +1 -1
- package/src/php/Utils/RequestUtils.php +209 -127
- package/src/php/WordPress/fingerprint-anti-bot.php +1 -1
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +1 -1
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +1 -1
- package/src/php/WordPress/readme.txt +1 -1
|
@@ -906,8 +906,76 @@ describe('Fingerprint & PoW Security Suite', () => {
|
|
|
906
906
|
expect(req.fingerprint.score).toBeGreaterThanOrEqual(blockThreshold);
|
|
907
907
|
});
|
|
908
908
|
|
|
909
|
-
test('should redirect after a valid COMBINED (CPU+Mem) PoW solution is provided', async (
|
|
909
|
+
test('should redirect after a valid COMBINED (CPU+Mem) PoW solution is provided', async () => {
|
|
910
|
+
const ip = '127.0.0.1';
|
|
911
|
+
const originalPath = '/protected/combined';
|
|
912
|
+
const solverFingerprint = 'fp-combined-solution';
|
|
913
|
+
const userAgent = 'test-ua-combined';
|
|
914
|
+
|
|
915
|
+
vi.spyOn(__internal, 'getSuspicionVector').mockResolvedValue({
|
|
916
|
+
historyScore: 35,
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
const securityConfigWithLowDiff = {
|
|
920
|
+
...securityConfig,
|
|
921
|
+
cpu: { minDifficultyBits: 4, ...securityConfig.cpu },
|
|
922
|
+
};
|
|
923
|
+
const middleware = powMiddleware(securityConfigWithLowDiff);
|
|
924
|
+
|
|
925
|
+
// 1. Déclencher le challenge
|
|
926
|
+
const req1 = {
|
|
927
|
+
path: originalPath, ip, cookies: {}, query: {},
|
|
928
|
+
headers: { 'user-agent': userAgent, 'x-device-fingerprint': solverFingerprint },
|
|
929
|
+
rawHeaders: ['User-Agent', userAgent], httpVersion: '1.1'
|
|
930
|
+
};
|
|
931
|
+
let challengeBody;
|
|
932
|
+
const res1 = {
|
|
933
|
+
status: () => res1,
|
|
934
|
+
send: (body) => { challengeBody = body; },
|
|
935
|
+
cookie: vi.fn()
|
|
936
|
+
};
|
|
937
|
+
req1.fingerprint = {};
|
|
938
|
+
await middleware(req1, res1, vi.fn());
|
|
939
|
+
|
|
940
|
+
expect(challengeBody).toContain('Enhanced Verification');
|
|
910
941
|
|
|
942
|
+
// 2. Résoudre le challenge
|
|
943
|
+
const nonce = challengeBody.match(/const nonce = "([^"]+)"/)[1];
|
|
944
|
+
const clientSecret = challengeBody.match(/const clientSecret = "([^"]+)"/)[1];
|
|
945
|
+
const cpuTargetHex = challengeBody.match(/const cpuTarget = BigInt\("0x" \+ "([^"]+)"\);/)[1];
|
|
946
|
+
const memDifficulty = parseInt(challengeBody.match(/const memDifficulty = (\d+)/)[1], 10);
|
|
947
|
+
|
|
948
|
+
const baseBlock = new TextEncoder().encode(`${nonce}:${clientSecret}:${solverFingerprint}:${ip}::`);
|
|
949
|
+
const cpuSolution = await solveCpuTargetInline(baseBlock, cpuTargetHex, () => {});
|
|
950
|
+
const memSolution = await solveMemory(`:${nonce}:${clientSecret}`, memDifficulty);
|
|
951
|
+
|
|
952
|
+
// 3. Soumettre la solution
|
|
953
|
+
const req2 = {
|
|
954
|
+
path: originalPath, ip, cookies: {},
|
|
955
|
+
query: {
|
|
956
|
+
pow_type: 'cpu_mem',
|
|
957
|
+
pow_nonce: nonce,
|
|
958
|
+
pow_solution_cpu: String(cpuSolution),
|
|
959
|
+
pow_solution_mem: JSON.stringify(memSolution),
|
|
960
|
+
pow_fp: solverFingerprint
|
|
961
|
+
},
|
|
962
|
+
headers: { 'user-agent': userAgent, 'x-device-fingerprint': solverFingerprint },
|
|
963
|
+
rawHeaders: ['User-Agent', userAgent], httpVersion: '1.1'
|
|
964
|
+
};
|
|
965
|
+
let capturedCookie;
|
|
966
|
+
const res2 = {
|
|
967
|
+
cookie: (name, value, options) => { capturedCookie = { name, value, options }; },
|
|
968
|
+
redirect: vi.fn(),
|
|
969
|
+
status: vi.fn(() => res2),
|
|
970
|
+
send: vi.fn(),
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
req2.fingerprint = {};
|
|
974
|
+
await middleware(req2, res2, vi.fn());
|
|
975
|
+
|
|
976
|
+
expect(res2.redirect).toHaveBeenCalledWith(originalPath);
|
|
977
|
+
expect(capturedCookie).toBeDefined();
|
|
978
|
+
expect(capturedCookie.name).toBe('pow_clearance');
|
|
911
979
|
});
|
|
912
980
|
});
|
|
913
981
|
|
|
@@ -1553,6 +1621,18 @@ describe('Fingerprint & PoW Security Suite', () => {
|
|
|
1553
1621
|
expect(behaviorScore).toBe(0);
|
|
1554
1622
|
});
|
|
1555
1623
|
|
|
1624
|
+
it('should return a score of 0 when x-behavior-metrics is 1 (human verified client-side)', () => {
|
|
1625
|
+
const context = { headers: { 'x-behavior-metrics': '1' } };
|
|
1626
|
+
const { behaviorScore } = getBehaviorScore(context);
|
|
1627
|
+
expect(behaviorScore).toBe(0);
|
|
1628
|
+
});
|
|
1629
|
+
|
|
1630
|
+
it('should return a score of 100 when x-behavior-metrics is 0 (bot detected client-side)', () => {
|
|
1631
|
+
const context = { headers: { 'x-behavior-metrics': '0' } };
|
|
1632
|
+
const { behaviorScore } = getBehaviorScore(context);
|
|
1633
|
+
expect(behaviorScore).toBe(100);
|
|
1634
|
+
});
|
|
1635
|
+
|
|
1556
1636
|
it('should return a score of 100 for honeypot interaction', () => {
|
|
1557
1637
|
const metrics = { honeypotInteraction: true, mouseEntropy: 50, keystrokeLatency: 120 };
|
|
1558
1638
|
const context = { headers: { 'x-behavior-metrics': JSON.stringify(metrics) } };
|
|
@@ -1560,11 +1640,11 @@ describe('Fingerprint & PoW Security Suite', () => {
|
|
|
1560
1640
|
expect(behaviorScore).toBe(100);
|
|
1561
1641
|
});
|
|
1562
1642
|
|
|
1563
|
-
it('should return a score of
|
|
1643
|
+
it('should return a score of 5 for no mouse or keyboard activity', () => {
|
|
1564
1644
|
const metrics = { honeypotInteraction: false, mouseMovementsHistory: [], keystrokeLatency: 0 };
|
|
1565
1645
|
const context = { headers: { 'x-behavior-metrics': JSON.stringify(metrics) } };
|
|
1566
1646
|
const { behaviorScore } = getBehaviorScore(context);
|
|
1567
|
-
expect(behaviorScore).toBe(
|
|
1647
|
+
expect(behaviorScore).toBe(5);
|
|
1568
1648
|
});
|
|
1569
1649
|
|
|
1570
1650
|
it('should return a score of 0 for normal user activity', () => {
|
|
@@ -2394,6 +2474,58 @@ describe('getClientHintsInconsistencyScore', () => {
|
|
|
2394
2474
|
});
|
|
2395
2475
|
});
|
|
2396
2476
|
|
|
2477
|
+
describe('getProtocolAnomalyScore (HTTP/2 Frame Analysis)', () => {
|
|
2478
|
+
const { getProtocolAnomalyScore } = __internal;
|
|
2479
|
+
|
|
2480
|
+
test('should penalize spoofed Chromium with no PRIORITY frames', () => {
|
|
2481
|
+
const context = {
|
|
2482
|
+
headers: {
|
|
2483
|
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
2484
|
+
'x-http2-fingerprint': 's:1:65536,2:0,3:1000,4:6291456,6:262144|15663105|1:0:0:256|m,a,s,p|p:0,w:4,c:1'
|
|
2485
|
+
},
|
|
2486
|
+
httpVersion: '2.0'
|
|
2487
|
+
};
|
|
2488
|
+
const { protocolAnomalyScore } = getProtocolAnomalyScore(context);
|
|
2489
|
+
expect(protocolAnomalyScore).toBe(25.0);
|
|
2490
|
+
});
|
|
2491
|
+
|
|
2492
|
+
test('should penalize spoofed Chromium with too few WINDOW_UPDATE frames', () => {
|
|
2493
|
+
const context = {
|
|
2494
|
+
headers: {
|
|
2495
|
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
2496
|
+
'x-http2-fingerprint': 's:1:65536,2:0,3:1000,4:6291456,6:262144|15663105|1:0:0:256|m,a,s,p|p:3,w:1,c:1'
|
|
2497
|
+
},
|
|
2498
|
+
httpVersion: '2.0'
|
|
2499
|
+
};
|
|
2500
|
+
const { protocolAnomalyScore } = getProtocolAnomalyScore(context);
|
|
2501
|
+
expect(protocolAnomalyScore).toBe(20.0);
|
|
2502
|
+
});
|
|
2503
|
+
|
|
2504
|
+
test('should penalize spoofed Firefox with too many PRIORITY frames', () => {
|
|
2505
|
+
const context = {
|
|
2506
|
+
headers: {
|
|
2507
|
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0',
|
|
2508
|
+
'x-http2-fingerprint': 's:1:65536,2:0,3:1000,4:6291456,6:262144|15663105|1:0:0:256|m,s,p,a|p:3,w:2,c:0'
|
|
2509
|
+
},
|
|
2510
|
+
httpVersion: '2.0'
|
|
2511
|
+
};
|
|
2512
|
+
const { protocolAnomalyScore } = getProtocolAnomalyScore(context);
|
|
2513
|
+
expect(protocolAnomalyScore).toBe(20.0);
|
|
2514
|
+
});
|
|
2515
|
+
|
|
2516
|
+
test('should not penalize a legitimate Chromium fingerprint', () => {
|
|
2517
|
+
const context = {
|
|
2518
|
+
headers: {
|
|
2519
|
+
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
2520
|
+
'x-http2-fingerprint': 's:1:65536,2:0,3:1000,4:6291456,6:262144|15663105|1:0:0:256|m,a,s,p|p:3,w:4,c:1'
|
|
2521
|
+
},
|
|
2522
|
+
httpVersion: '2.0'
|
|
2523
|
+
};
|
|
2524
|
+
const { protocolAnomalyScore } = getProtocolAnomalyScore(context);
|
|
2525
|
+
expect(protocolAnomalyScore).toBe(0.0);
|
|
2526
|
+
});
|
|
2527
|
+
});
|
|
2528
|
+
|
|
2397
2529
|
describe('Subnet Scoring (Node.js)', () => {
|
|
2398
2530
|
const inMemoryStore = {
|
|
2399
2531
|
_map: new Map(),
|
|
@@ -789,39 +789,38 @@ class ChallengeUtils
|
|
|
789
789
|
/**
|
|
790
790
|
* Vérifie le limiteur de débit Token Bucket pour les demandes de challenge d'un sous-réseau.
|
|
791
791
|
*/
|
|
792
|
-
public static function checkChallengeRateLimit(string $clientIp): bool
|
|
792
|
+
public static function checkChallengeRateLimit(string $clientIp, float $capacity = 5.0, float $refillRate = 0.1): bool
|
|
793
793
|
{
|
|
794
|
-
$subnet = RequestUtils::getIpSubnet($clientIp);
|
|
795
|
-
if ($subnet === null) {
|
|
796
|
-
return false;
|
|
797
|
-
}
|
|
794
|
+
$subnet = RequestUtils::getIpSubnet($clientIp) ?? $clientIp;
|
|
798
795
|
|
|
799
796
|
$store = StoreManager::getStore();
|
|
800
797
|
$key = "rate-limit:{$subnet}";
|
|
801
|
-
$rateLimitData = $store->get($key) ?? [
|
|
802
|
-
'tokens' => 5.0,
|
|
803
|
-
'lastRefill' => microtime(true)
|
|
804
|
-
];
|
|
805
|
-
|
|
806
|
-
$capacity = 5.0;
|
|
807
|
-
$refillRate = 0.1; // 1 token toutes les 10 secondes
|
|
808
798
|
$now = microtime(true);
|
|
809
799
|
|
|
810
|
-
$
|
|
811
|
-
$
|
|
800
|
+
$rateLimitData = $store->get($key);
|
|
801
|
+
if (!is_array($rateLimitData) || !isset($rateLimitData['tokens'], $rateLimitData['lastRefill'])) {
|
|
802
|
+
$rateLimitData = [
|
|
803
|
+
'tokens' => $capacity,
|
|
804
|
+
'lastRefill' => $now
|
|
805
|
+
];
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
$elapsed = max(0.0, $now - (float)$rateLimitData['lastRefill']);
|
|
809
|
+
$tokens = min($capacity, (float)$rateLimitData['tokens'] + $elapsed * $refillRate);
|
|
810
|
+
$ttl = (int)max(60, ceil($capacity / max(0.1, $refillRate)));
|
|
812
811
|
|
|
813
812
|
if ($tokens < 1.0) {
|
|
814
813
|
$store->set($key, [
|
|
815
814
|
'tokens' => $tokens,
|
|
816
815
|
'lastRefill' => $now
|
|
817
|
-
],
|
|
816
|
+
], $ttl);
|
|
818
817
|
return false;
|
|
819
818
|
}
|
|
820
819
|
|
|
821
820
|
$store->set($key, [
|
|
822
821
|
'tokens' => $tokens - 1.0,
|
|
823
822
|
'lastRefill' => $now
|
|
824
|
-
],
|
|
823
|
+
], $ttl);
|
|
825
824
|
|
|
826
825
|
return true;
|
|
827
826
|
}
|