@niksbanna/bot-detector 1.0.0 → 1.0.3

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.
@@ -35,30 +35,40 @@ class PageLoadSignal extends Signal {
35
35
 
36
36
  const timing = performance.timing;
37
37
 
38
- // Calculate key timings
38
+ // Calculate key timings.
39
+ // A 0 entry means the event hasn't fired; subtracting from a large epoch
40
+ // timestamp produces a huge negative number that was incorrectly flagged
41
+ // as 'negative-timing' (timestamp manipulation).
39
42
  const navigationStart = timing.navigationStart;
40
- const domContentLoaded = timing.domContentLoadedEventEnd - navigationStart;
41
- const domComplete = timing.domComplete - navigationStart;
42
- const loadComplete = timing.loadEventEnd - navigationStart;
43
- const dnsLookup = timing.domainLookupEnd - timing.domainLookupStart;
44
- const tcpConnection = timing.connectEnd - timing.connectStart;
45
- const serverResponse = timing.responseEnd - timing.requestStart;
46
- const domProcessing = timing.domComplete - timing.domLoading;
43
+ const safeTimingDiff = (end, start) => {
44
+ if (end === 0 || start === 0) return null;
45
+ return end - start;
46
+ };
47
+
48
+ const domContentLoaded = safeTimingDiff(timing.domContentLoadedEventEnd, navigationStart);
49
+ const domComplete = safeTimingDiff(timing.domComplete, navigationStart);
50
+ const loadComplete = safeTimingDiff(timing.loadEventEnd, navigationStart);
51
+ const dnsLookup = safeTimingDiff(timing.domainLookupEnd, timing.domainLookupStart);
52
+ const tcpConnection = safeTimingDiff(timing.connectEnd, timing.connectStart);
53
+ const serverResponse = safeTimingDiff(timing.responseEnd, timing.requestStart);
54
+ const domProcessing = safeTimingDiff(timing.domComplete, timing.domLoading);
47
55
 
48
56
  // Check for impossibly fast load times
49
- if (domContentLoaded > 0 && domContentLoaded < 10) {
57
+ if (domContentLoaded !== null && domContentLoaded > 0 && domContentLoaded < 10) {
50
58
  anomalies.push('instant-dom-content-loaded');
51
59
  confidence = Math.max(confidence, 0.7);
52
60
  }
53
61
 
54
62
  // Check for zero DNS lookup (could indicate local file or caching, but suspicious in combination)
55
- if (dnsLookup === 0 && tcpConnection === 0 && serverResponse < 5) {
63
+ if (dnsLookup === 0 && tcpConnection === 0 && serverResponse !== null && serverResponse < 5) {
56
64
  anomalies.push('zero-network-timing');
57
65
  confidence = Math.max(confidence, 0.4);
58
66
  }
59
67
 
60
- // Check for negative timings (timestamp manipulation)
61
- if (domContentLoaded < 0 || domComplete < 0 || loadComplete < 0) {
68
+ // Check for negative timings (timestamp manipulation).
69
+ if ((domContentLoaded !== null && domContentLoaded < 0) ||
70
+ (domComplete !== null && domComplete < 0) ||
71
+ (loadComplete !== null && loadComplete < 0)) {
62
72
  anomalies.push('negative-timing');
63
73
  confidence = Math.max(confidence, 0.8);
64
74
  }
@@ -72,7 +82,7 @@ class PageLoadSignal extends Signal {
72
82
  }
73
83
 
74
84
  // Check for very long processing times (could indicate headless waiting)
75
- if (domProcessing > 30000) { // 30 seconds
85
+ if (domProcessing !== null && domProcessing > 30000) { // 30 seconds
76
86
  anomalies.push('excessive-dom-processing');
77
87
  confidence = Math.max(confidence, 0.3);
78
88
  }
@@ -80,17 +90,25 @@ class PageLoadSignal extends Signal {
80
90
  // Check for script injection timing pattern
81
91
  // Bots often inject scripts immediately after load
82
92
  const scriptsLoadedTime = timing.domContentLoadedEventStart - timing.responseEnd;
83
- if (scriptsLoadedTime > 0 && scriptsLoadedTime < 5) {
93
+ if (timing.responseEnd > 0 && timing.domContentLoadedEventStart > 0 &&
94
+ scriptsLoadedTime > 0 && scriptsLoadedTime < 5) {
84
95
  anomalies.push('instant-script-execution');
85
96
  confidence = Math.max(confidence, 0.4);
86
97
  }
87
98
 
88
- // Check for performance.now() manipulation
89
- const perfNow1 = performance.now();
90
- const perfNow2 = performance.now();
91
-
92
- // If two consecutive calls return the same value (shouldn't happen)
93
- if (perfNow1 === perfNow2 && perfNow1 > 0) {
99
+ // Check for performance.now() manipulation.
100
+ // performance.now() to 1-2ms for Spectre protection. Two consecutive calls
101
+ // can legitimately return the same value, so comparing only two is unreliable.
102
+ // Instead, use a busy-wait loop to advance time, then check if the clock moved.
103
+ const perfBefore = performance.now();
104
+ // Spin for ~1ms to force the clock to advance past quantization granularity
105
+ const spinEnd = perfBefore + 2;
106
+ // eslint-disable-next-line no-empty
107
+ while (performance.now() < spinEnd) {}
108
+ const perfAfter = performance.now();
109
+
110
+ if (perfAfter === perfBefore) {
111
+ // Clock truly didn't advance over 2ms — something is wrong
94
112
  anomalies.push('frozen-performance-now');
95
113
  confidence = Math.max(confidence, 0.6);
96
114
  }