@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.
@@ -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 (context) => {
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 40 for no mouse or keyboard activity', () => {
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(40);
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
- $elapsed = $now - $rateLimitData['lastRefill'];
811
- $tokens = min($capacity, $rateLimitData['tokens'] + $elapsed * $refillRate);
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
- ], 60);
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
- ], 60);
823
+ ], $ttl);
825
824
 
826
825
  return true;
827
826
  }