@anonympins/fingerprint 0.7.1 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4472,6 +4472,17 @@ export class FingerprintEngine {
4472
4472
  }
4473
4473
  }
4474
4474
  }
4475
+
4476
+ // If no security config is provided at all, create a base one.
4477
+ if (!finalConfig) {
4478
+ finalConfig = {};
4479
+ }
4480
+ // If the whitelist is not explicitly provided in the configuration,
4481
+ // apply the default whitelist which includes common search engine bots.
4482
+ if (!finalConfig.whitelist) {
4483
+ finalConfig.whitelist = default_whitelist();
4484
+ }
4485
+
4475
4486
  this.securityConfig = finalConfig;
4476
4487
  this.isProduction = isProduction;
4477
4488
  this._allowlist = this._buildAllowlist();
@@ -4670,7 +4681,8 @@ export class FingerprintEngine {
4670
4681
  return false;
4671
4682
  }
4672
4683
  _isIpInAllowlist(clientIp) {
4673
- return this._allowlist.check(clientIp);
4684
+ const family = isIPv6(clientIp) ? 'ipv6' : 'ipv4';
4685
+ return this._allowlist.check(clientIp, family);
4674
4686
  }
4675
4687
  /**
4676
4688
  * Checks if the request's host and path match an entry in the host+path allowlist.
@@ -4809,7 +4821,10 @@ export class FingerprintEngine {
4809
4821
  return false;
4810
4822
  }
4811
4823
 
4824
+ const botName = matchedRule.userAgent; // e.g., 'Googlebot'
4825
+
4812
4826
  if (!canAttemptDns()) {
4827
+ this._log(`[Bot Verification] DNS circuit breaker is open. Skipping check for ${botName}.`, { clientIp });
4813
4828
  return false;
4814
4829
  }
4815
4830
 
@@ -4817,9 +4832,11 @@ export class FingerprintEngine {
4817
4832
  const cachedStatus = await store.get(cacheKey);
4818
4833
 
4819
4834
  if (cachedStatus === 'verified') {
4835
+ this._log(`[Bot Verification] PASSED (cached): ${botName}`, { clientIp });
4820
4836
  return true;
4821
4837
  }
4822
4838
  if (cachedStatus === 'failed') {
4839
+ this._log(`[Bot Verification] FAILED (cached): ${botName}`, { clientIp });
4823
4840
  return false;
4824
4841
  }
4825
4842
 
@@ -4829,25 +4846,35 @@ export class FingerprintEngine {
4829
4846
  const validHostname = hostnames.find(h => h.endsWith(matchedRule.hostnameSuffix));
4830
4847
 
4831
4848
  if (!validHostname) {
4849
+ this._log(`[Bot Verification] FAILED: Reverse DNS lookup for ${clientIp} did not yield a valid hostname ending in '${matchedRule.hostnameSuffix}'.`, { clientIp, resolvedHostnames: hostnames });
4832
4850
  await store.set(cacheKey, 'failed', 300); // Temporary negative caching (5 minutes)
4833
4851
  return false;
4834
4852
  }
4835
4853
 
4854
+ this._log(`[Bot Verification] Reverse DNS OK for ${botName}.`, { clientIp, validHostname });
4855
+
4836
4856
  // 2. Forward DNS lookup
4837
4857
  let addresses = [];
4838
4858
  try {
4839
4859
  addresses = await withTimeout(dns.resolve(validHostname), 500);
4840
- } catch (e) {}
4860
+ } catch (e) {
4861
+ this._log(`[Bot Verification] Forward DNS (A) lookup FAILED for ${validHostname}.`, { error: e.message });
4862
+ }
4841
4863
  try {
4842
4864
  const ipv6 = await withTimeout(dns.resolve(validHostname, 'AAAA'), 500);
4843
4865
  addresses = addresses.concat(ipv6);
4844
- } catch (e) {}
4866
+ } catch (e) {
4867
+ this._log(`[Bot Verification] Forward DNS (AAAA) lookup FAILED for ${validHostname}.`, { error: e.message });
4868
+ }
4845
4869
  if (addresses.includes(clientIp)) {
4870
+ this._log(`[Bot Verification] PASSED: Forward DNS IP matches original IP for ${botName}.`, { clientIp, validHostname, resolvedIps: addresses });
4846
4871
  recordDnsSuccess();
4847
4872
  await store.set(cacheKey, 'verified', 86400); // Cache success for 24h (TTL in seconds)
4848
4873
  return true;
4849
4874
  }
4875
+ this._log(`[Bot Verification] FAILED: IP mismatch for ${botName}. Original IP not in resolved addresses.`, { clientIp, validHostname, resolvedIps: addresses });
4850
4876
  } catch (error) {
4877
+ this._log(`[Bot Verification] FAILED: DNS lookup error for ${botName}.`, { clientIp, error: error.message });
4851
4878
  recordDnsFailure();
4852
4879
  await store.set(cacheKey, 'failed', 300); // Temporary negative caching (5 minutes)
4853
4880
  return false;
@@ -4965,15 +4992,15 @@ export class FingerprintEngine {
4965
4992
  }
4966
4993
  }
4967
4994
 
4968
- // Check if the request is from a verified, whitelisted bot (e.g., Googlebot)
4969
- if (await this._verifyWhitelistedBot(requestContext)) {
4995
+ const botVerificationResult = await this._verifyWhitelistedBot(requestContext);
4996
+ if (botVerificationResult === true) {
4970
4997
  const filterWhitelist = this.securityConfig.filterWhitelist || false;
4971
4998
  let bypassWhitelist = false;
4972
4999
  if (filterWhitelist === true) {
4973
5000
  bypassWhitelist = this._hasCertainAttack(requestContext);
4974
5001
  } else if (typeof filterWhitelist === 'number') {
4975
5002
  const res = await getScoreAndVector();
4976
- if (res.score > filterWhitelist) {
5003
+ if (res.score > filterWhitelist) { // NOSONAR
4977
5004
  bypassWhitelist = true;
4978
5005
  }
4979
5006
  }
@@ -5013,6 +5040,11 @@ export class FingerprintEngine {
5013
5040
 
5014
5041
  // The engine now works with the context directly, no more rawReq dependency here.
5015
5042
  const suspicionVector = preCalculatedVector || await __internal.getSuspicionVector(requestContext, this.securityConfig);
5043
+ if (botVerificationResult === false) {
5044
+ // This means it claimed to be a bot but failed verification.
5045
+ // This is a strong signal of spoofing.
5046
+ suspicionVector.tlsSpoofingScore = Math.max(suspicionVector.tlsSpoofingScore || 0, 95);
5047
+ }
5016
5048
  let finalScore = preCalculatedScore !== null ? preCalculatedScore : this.calculateFinalScore(suspicionVector);
5017
5049
 
5018
5050
  this._log('Suspicion vector calculated', {
@@ -10,6 +10,8 @@ import * as fingerprint from '../fingerprint.js';
10
10
  vi.mock('import-meta-env', () => ({
11
11
  env: { NODE_ENV: 'test', POW_SECRET: 'fallback-dev-secret-32-chars-minimum' },
12
12
  }));
13
+ // Mock the entire dns module
14
+ vi.mock('node:dns/promises');
13
15
  // Mock readFileSync for custom challenge page tests
14
16
  vi.mock('node:fs', async () => {
15
17
  const actualFs = await vi.importActual('node:fs');
@@ -35,9 +37,8 @@ const {
35
37
  } = fingerprint;
36
38
  const { store, getRequestPatternScore, getDeviceHash } = __internal;
37
39
  const { getRenderingAnomalyScore } = __internal;
38
- let { getBehaviorScore, getClickVarianceScore } = __internal;
39
- // Mock the entire dns module
40
- vi.mock('node:dns/promises');
40
+ let { getBehaviorScore, getClickVarianceScore } = __internal; // eslint-disable-line prefer-const
41
+
41
42
 
42
43
  describe('Fingerprint & PoW Security Suite', () => {
43
44
  test('cyrb53 should be deterministic', () => {
@@ -1764,6 +1765,92 @@ describe('Fingerprint & PoW Security Suite', () => {
1764
1765
  });
1765
1766
  });
1766
1767
  });
1768
+ describe('Googlebot Whitelisting (Node.js)', () => {
1769
+ const inMemoryStore = {
1770
+ _map: new Map(),
1771
+ async get(key) { return this._map.get(key); },
1772
+ async set(key, value) { this._map.set(key, value); },
1773
+ async has(key) { return this._map.has(key); },
1774
+ async delete(key) { this._map.delete(key); },
1775
+ };
1776
+
1777
+ const securityConfig = {
1778
+ weights: { headerAnomalyScore: 1.0, tlsSpoofingScore: 1.0 }, // Simple config for testing
1779
+ thresholds: { low: 20, medium: 45, high: 75, block: 95 },
1780
+ whitelist: [
1781
+ { userAgent: 'Googlebot', hostnameSuffix: '.googlebot.com' }
1782
+ ]
1783
+ };
1784
+
1785
+ let engine;
1786
+
1787
+ beforeEach(() => {
1788
+ inMemoryStore._map.clear();
1789
+ configureStore(inMemoryStore);
1790
+ vi.clearAllMocks(); // Clear mocks before each test
1791
+ engine = new FingerprintEngine(securityConfig);
1792
+ });
1793
+
1794
+ it('should correctly identify and allow a legitimate Googlebot request', async () => {
1795
+ const googleIp = '::ffff:66.249.66.39'; // An IP from the official list
1796
+ const googleHostname = 'crawl-66-249-66-39.googlebot.com';
1797
+ const googleUa = 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)';
1798
+
1799
+ // Mock the DNS verification process
1800
+ vi.mocked(dns.reverse).mockResolvedValue([googleHostname]);
1801
+ vi.mocked(dns.resolve).mockResolvedValue([googleIp]);
1802
+
1803
+ const requestContext = {
1804
+ clientIp: googleIp,
1805
+ path: '/',
1806
+ headers: { 'user-agent': googleUa },
1807
+ isStatic: false,
1808
+ cookies: {},
1809
+ query: {},
1810
+ body: null,
1811
+ httpVersion: '1.1',
1812
+ rawHeaders: ['user-agent', googleUa]
1813
+ };
1814
+
1815
+ const decision = await engine.processRequest(requestContext);
1816
+
1817
+ expect(decision.action).toBe('next');
1818
+ expect(decision.score).toBe(0);
1819
+ expect(decision.vector.whitelisted).toBe(100);
1820
+ expect(decision.vector.type).toBe('bot');
1821
+
1822
+ expect(dns.reverse).toHaveBeenCalledWith(googleIp);
1823
+ expect(dns.resolve).toHaveBeenCalledWith(googleHostname);
1824
+ });
1825
+
1826
+ it('should challenge or block a request spoofing a Googlebot User-Agent from a non-Google IP', async () => {
1827
+ const fakeGoogleIp = '1.2.3.4';
1828
+ const googleUa = 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)';
1829
+
1830
+ vi.mocked(dns.reverse).mockRejectedValue(new Error('Host not found'));
1831
+
1832
+ const requestContext = {
1833
+ clientIp: fakeGoogleIp,
1834
+ path: '/',
1835
+ headers: { 'user-agent': googleUa, 'accept-language': 'en-US' },
1836
+ isStatic: false,
1837
+ cookies: {},
1838
+ query: {},
1839
+ body: null,
1840
+ httpVersion: '1.1',
1841
+ rawHeaders: ['user-agent', googleUa, 'accept-language', 'en-US']
1842
+ };
1843
+
1844
+ const decision = await engine.processRequest(requestContext);
1845
+
1846
+ expect(decision.action).toBe('block');
1847
+ expect(decision.score).toBeGreaterThan(0);
1848
+ expect(decision.vector.whitelisted).toBeUndefined();
1849
+
1850
+ expect(dns.reverse).toHaveBeenCalledWith(fakeGoogleIp);
1851
+ expect(dns.resolve).not.toHaveBeenCalled();
1852
+ });
1853
+ });
1767
1854
 
1768
1855
  describe('TLS Session Resumption (Cookieless Identity)', () => {
1769
1856
  const inMemoryStore = {
@@ -892,7 +892,7 @@ class ChallengeUtils
892
892
  return false;
893
893
  }
894
894
 
895
- if (empty($solution)) {
895
+ if ($solution === '') {
896
896
  return false;
897
897
  }
898
898
 
@@ -179,6 +179,12 @@
179
179
  }
180
180
  }
181
181
  }
182
+
183
+ // Si aucune liste blanche n'est explicitement fournie, appliquer la liste par défaut.
184
+ if (!isset($securityConfig['whitelist'])) {
185
+ $securityConfig['whitelist'] = self::default_whitelist();
186
+ }
187
+
182
188
  $this->securityConfig = $securityConfig;
183
189
  $this->verbose = $securityConfig['verbose'] ?? false;
184
190
  $this->allowlist = $this->buildAllowlist();
@@ -551,10 +557,8 @@
551
557
 
552
558
  $deviceId = bin2hex(random_bytes(16)); // UUID-like
553
559
 
554
- $isHttps = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
555
- (isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443) ||
556
- ($context->getHeader('x-forwarded-proto') === 'https');
557
- $secureOption = $isHttps || $this->isProduction;
560
+ $isHttps = !empty($context->isHttps);
561
+ $secureOption = $isHttps;
558
562
 
559
563
  // Préparer le cookie à envoyer
560
564
  $newCookie = [
@@ -985,13 +989,36 @@
985
989
  // Le fingerprint est cohérent, on peut valider la solution
986
990
  if ($powType === 'cpu_target' || $powType === 'cpu_mem') {
987
991
  $cpuSolution = $context->query['pow_solution_cpu'] ?? $context->query['pow_solution'] ?? null;
988
- if ($cpuSolution) {
989
- $ticket = ChallengeUtils::verifyCpuTargetPoWAndGenerateTicket($context->clientIp, 3600000, $powNonce, $cpuSolution, $challengeContext);
992
+ if ($cpuSolution !== null && $cpuSolution !== '') {
993
+ $identity = $this->resolveRequestIdentity($context, $suspicionVector);
994
+ $deviceId = (string)($identity['deviceId'] ?? '');
995
+ $currentDeviceHash = (string)($identity['currentDeviceHash'] ?? RequestUtils::getCompositeDeviceHash($context));
996
+
997
+ $ticket = ChallengeUtils::verifyCpuTargetPoWAndGenerateTicket(
998
+ $context->clientIp,
999
+ 3600000,
1000
+ $powNonce,
1001
+ (string)$cpuSolution,
1002
+ $challengeContext,
1003
+ $deviceId,
1004
+ $currentDeviceHash
1005
+ );
990
1006
  $isValid = $ticket !== null;
991
1007
 
992
1008
  if ($powType === 'cpu_mem') {
993
- $memSolution = $context->query['pow_solution_mem'] ?? null;
994
- $isMemValid = $memSolution ? ChallengeUtils::verifyMemoryPoW($powNonce, $memSolution, $challengeContext['memDifficulty'] ?? 0, $challengeContext['clientSecret'] ?? '') : false;
1009
+ if (!empty($challengeContext['isHttp'])) {
1010
+ $isMemValid = true;
1011
+ } else {
1012
+ $memSolution = $context->query['pow_solution_mem'] ?? null;
1013
+ $isMemValid = ($memSolution !== null && $memSolution !== '')
1014
+ ? ChallengeUtils::verifyMemoryPoW(
1015
+ $powNonce,
1016
+ (string)$memSolution,
1017
+ $challengeContext['memDifficulty'] ?? 0,
1018
+ $challengeContext['clientSecret'] ?? ''
1019
+ )
1020
+ : false;
1021
+ }
995
1022
  $isValid = $isValid && $isMemValid;
996
1023
  }
997
1024
  }
@@ -1073,10 +1100,8 @@
1073
1100
  MetricsManager::incrementCounter('challenges_solved_total');
1074
1101
  $this->log('Challenge solution valid - issuing ticket', ['ticketMaxAge' => $ticketTtl]);
1075
1102
 
1076
- $isHttps = (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ||
1077
- (isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443) ||
1078
- ($context->getHeader('x-forwarded-proto') === 'https');
1079
- $secureOption = $isHttps || $this->isProduction;
1103
+ $isHttps = !empty($context->isHttps);
1104
+ $secureOption = $isHttps;
1080
1105
 
1081
1106
  return [
1082
1107
  'action' => 'redirect',