engagelab-captcha-sdk 1.0.0
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/dist/captcha-sdk.esm.js +1 -0
- package/dist/captcha-sdk.umd.js +1 -0
- package/dist/types/api.d.ts +12 -0
- package/dist/types/challenges/dragsort.d.ts +24 -0
- package/dist/types/challenges/grid.d.ts +20 -0
- package/dist/types/challenges/icons.d.ts +18 -0
- package/dist/types/challenges/invisible.d.ts +8 -0
- package/dist/types/challenges/physics.d.ts +40 -0
- package/dist/types/challenges/rotate.d.ts +14 -0
- package/dist/types/challenges/slider.d.ts +17 -0
- package/dist/types/challenges/spatial.d.ts +12 -0
- package/dist/types/compat/hcaptcha.d.ts +1 -0
- package/dist/types/compat/recaptcha.d.ts +1 -0
- package/dist/types/crypto.d.ts +11 -0
- package/dist/types/deep_probes.d.ts +13 -0
- package/dist/types/degradation.d.ts +27 -0
- package/dist/types/device_attestation.d.ts +19 -0
- package/dist/types/fingerprint.d.ts +2 -0
- package/dist/types/headless.d.ts +2 -0
- package/dist/types/i18n.d.ts +7 -0
- package/dist/types/index.d.ts +36 -0
- package/dist/types/integrity.d.ts +3 -0
- package/dist/types/logger.d.ts +49 -0
- package/dist/types/native_monitor.d.ts +21 -0
- package/dist/types/passive_signals.d.ts +119 -0
- package/dist/types/pow.d.ts +2 -0
- package/dist/types/reporter.d.ts +8 -0
- package/dist/types/session_persist.d.ts +18 -0
- package/dist/types/theme.d.ts +11 -0
- package/dist/types/timing.d.ts +2 -0
- package/dist/types/trajectory.d.ts +18 -0
- package/dist/types/types.d.ts +182 -0
- package/dist/types/ui/container.d.ts +20 -0
- package/dist/types/ui/styles.d.ts +1 -0
- package/dist/types/wasm_core.d.ts +44 -0
- package/dist/types/web-component.d.ts +11 -0
- package/package.json +22 -0
- package/rollup.config.mjs +62 -0
- package/src/api.ts +180 -0
- package/src/challenges/dragsort.ts +168 -0
- package/src/challenges/grid.ts +147 -0
- package/src/challenges/icons.ts +106 -0
- package/src/challenges/invisible.ts +57 -0
- package/src/challenges/physics.ts +437 -0
- package/src/challenges/rotate.ts +81 -0
- package/src/challenges/slider.ts +168 -0
- package/src/challenges/spatial.ts +91 -0
- package/src/compat/hcaptcha.ts +112 -0
- package/src/compat/recaptcha.ts +108 -0
- package/src/crypto.ts +69 -0
- package/src/deep_probes.ts +690 -0
- package/src/degradation.ts +113 -0
- package/src/device_attestation.ts +109 -0
- package/src/fingerprint.ts +247 -0
- package/src/headless.ts +233 -0
- package/src/i18n.ts +455 -0
- package/src/index.ts +527 -0
- package/src/integrity.ts +100 -0
- package/src/logger.ts +170 -0
- package/src/native_monitor.ts +100 -0
- package/src/passive_signals.ts +544 -0
- package/src/pow.ts +120 -0
- package/src/reporter.ts +75 -0
- package/src/session_persist.ts +90 -0
- package/src/theme.ts +41 -0
- package/src/timing.ts +79 -0
- package/src/trajectory.ts +110 -0
- package/src/types.ts +199 -0
- package/src/ui/container.ts +161 -0
- package/src/ui/styles.ts +153 -0
- package/src/wasm_core.ts +189 -0
- package/src/web-component.ts +103 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deep environment probes — counter Camoufox/Nodriver/Stealth attacks.
|
|
3
|
+
* These tests exploit behavioral and timing differences that are extremely
|
|
4
|
+
* difficult to fake even with C++-level browser patching.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { sdkLogger } from './logger';
|
|
8
|
+
|
|
9
|
+
export interface DeepProbeResult {
|
|
10
|
+
name: string;
|
|
11
|
+
detected: boolean;
|
|
12
|
+
value: string;
|
|
13
|
+
weight: number;
|
|
14
|
+
confidence: number; // 0-1, how confident we are this is a true signal
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function runDeepProbes(): Promise<DeepProbeResult[]> {
|
|
18
|
+
sdkLogger.debug('DeepProbes', 'runDeepProbes start');
|
|
19
|
+
const results: DeepProbeResult[] = [];
|
|
20
|
+
|
|
21
|
+
const push = (r: DeepProbeResult) => {
|
|
22
|
+
results.push(r);
|
|
23
|
+
sdkLogger.debug('DeepProbes', `probe ${r.name}`, { detected: r.detected, weight: r.weight });
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
push(probePropertyDescriptors());
|
|
27
|
+
push(probeErrorStackDepth());
|
|
28
|
+
push(probeCanvasConsistency());
|
|
29
|
+
push(probeWebGLParamDepth());
|
|
30
|
+
push(probeIntlConsistency());
|
|
31
|
+
push(probeTimerCoherence());
|
|
32
|
+
push(probeFontRendering());
|
|
33
|
+
push(probeObjectStringify());
|
|
34
|
+
push(probeWorkerScope());
|
|
35
|
+
push(probeSharedArrayBuffer());
|
|
36
|
+
push(probeAudioContextFingerprint());
|
|
37
|
+
push(probeEventTrustChain());
|
|
38
|
+
push(probeNavigatorInconsistency());
|
|
39
|
+
push(await probeNetworkTiming());
|
|
40
|
+
|
|
41
|
+
push(probeFingerprintBrowser());
|
|
42
|
+
push(probeMultiLoginBrowserArtifacts());
|
|
43
|
+
|
|
44
|
+
const detectedCount = results.filter(p => p.detected).length;
|
|
45
|
+
sdkLogger.info('DeepProbes', `Completed ${results.length} probes, ${detectedCount} detected`);
|
|
46
|
+
|
|
47
|
+
return results;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if native property descriptors have been tampered.
|
|
52
|
+
* Camoufox/Stealth modify getters but rarely fix the descriptor metadata.
|
|
53
|
+
*/
|
|
54
|
+
function probePropertyDescriptors(): DeepProbeResult {
|
|
55
|
+
let tampered = 0;
|
|
56
|
+
const checks = [
|
|
57
|
+
{ obj: navigator, prop: 'webdriver' },
|
|
58
|
+
{ obj: navigator, prop: 'languages' },
|
|
59
|
+
{ obj: navigator, prop: 'plugins' },
|
|
60
|
+
{ obj: navigator, prop: 'hardwareConcurrency' },
|
|
61
|
+
{ obj: screen, prop: 'width' },
|
|
62
|
+
{ obj: screen, prop: 'height' },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
for (const { obj, prop } of checks) {
|
|
66
|
+
try {
|
|
67
|
+
const desc = Object.getOwnPropertyDescriptor(obj, prop);
|
|
68
|
+
// Native properties should NOT have own descriptors on the instance
|
|
69
|
+
// They should be on the prototype. If found on instance = patched
|
|
70
|
+
if (desc && desc.get && !Function.prototype.toString.call(desc.get).includes('[native code]')) {
|
|
71
|
+
tampered++;
|
|
72
|
+
}
|
|
73
|
+
// Also check: own property on instance instead of prototype
|
|
74
|
+
if (desc && Object.prototype.hasOwnProperty.call(obj, prop)) {
|
|
75
|
+
const protoDesc = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(obj), prop);
|
|
76
|
+
if (protoDesc && protoDesc.get) {
|
|
77
|
+
// Property exists on both instance AND prototype = suspicious override
|
|
78
|
+
tampered++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
} catch { /* skip */ }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
name: 'property_descriptor_tampering',
|
|
86
|
+
detected: tampered > 1,
|
|
87
|
+
value: String(tampered),
|
|
88
|
+
weight: tampered > 2 ? 15 : tampered > 0 ? 8 : 0,
|
|
89
|
+
confidence: Math.min(1, tampered / 3),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Analyze Error stack trace depth and format.
|
|
95
|
+
* Automated browsers often have different stack patterns.
|
|
96
|
+
*/
|
|
97
|
+
function probeErrorStackDepth(): DeepProbeResult {
|
|
98
|
+
let anomalies = 0;
|
|
99
|
+
try {
|
|
100
|
+
const e = new Error('probe');
|
|
101
|
+
const stack = e.stack || '';
|
|
102
|
+
const lines = stack.split('\n').filter(l => l.trim().length > 0);
|
|
103
|
+
|
|
104
|
+
// Check for automation-framework frames in stack
|
|
105
|
+
const suspiciousFrames = ['puppeteer', 'playwright', 'selenium', 'webdriver', 'cdp', 'devtools', '__puppeteer'];
|
|
106
|
+
for (const frame of lines) {
|
|
107
|
+
const lower = frame.toLowerCase();
|
|
108
|
+
for (const sus of suspiciousFrames) {
|
|
109
|
+
if (lower.includes(sus)) anomalies++;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Very shallow stack in non-minimal context = suspicious
|
|
114
|
+
if (lines.length < 2) anomalies++;
|
|
115
|
+
|
|
116
|
+
// Check Error.captureStackTrace behavior (Chrome-specific)
|
|
117
|
+
if (typeof Error.captureStackTrace === 'function') {
|
|
118
|
+
const obj: Record<string, unknown> = {};
|
|
119
|
+
Error.captureStackTrace(obj);
|
|
120
|
+
const st = typeof obj.stack === 'string' ? obj.stack : '';
|
|
121
|
+
if (!st || st.length < 10) anomalies++;
|
|
122
|
+
}
|
|
123
|
+
} catch {
|
|
124
|
+
anomalies++;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
name: 'error_stack_anomaly',
|
|
129
|
+
detected: anomalies > 0,
|
|
130
|
+
value: String(anomalies),
|
|
131
|
+
weight: anomalies > 1 ? 10 : anomalies > 0 ? 5 : 0,
|
|
132
|
+
confidence: Math.min(1, anomalies / 2),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Canvas rendering consistency check.
|
|
138
|
+
* Draw the same thing twice with slight timing — result should be identical.
|
|
139
|
+
* Randomized fingerprint spoofing will produce different results.
|
|
140
|
+
*/
|
|
141
|
+
function probeCanvasConsistency(): DeepProbeResult {
|
|
142
|
+
try {
|
|
143
|
+
const draw = () => {
|
|
144
|
+
const c = document.createElement('canvas');
|
|
145
|
+
c.width = 100; c.height = 30;
|
|
146
|
+
const ctx = c.getContext('2d')!;
|
|
147
|
+
ctx.textBaseline = 'alphabetic';
|
|
148
|
+
ctx.fillStyle = '#f60';
|
|
149
|
+
ctx.fillRect(10, 1, 62, 20);
|
|
150
|
+
ctx.fillStyle = '#069';
|
|
151
|
+
ctx.font = '11px Arial';
|
|
152
|
+
ctx.fillText('Cwm fjordbank', 2, 15);
|
|
153
|
+
ctx.fillStyle = 'rgba(102,204,0,0.7)';
|
|
154
|
+
ctx.font = '18px Arial';
|
|
155
|
+
ctx.fillText('glyphs vext', 4, 25);
|
|
156
|
+
return c.toDataURL();
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const hash1 = draw();
|
|
160
|
+
const hash2 = draw();
|
|
161
|
+
const consistent = hash1 === hash2;
|
|
162
|
+
|
|
163
|
+
// Inconsistency = fingerprint randomization (Camoufox does this)
|
|
164
|
+
return {
|
|
165
|
+
name: 'canvas_consistency',
|
|
166
|
+
detected: !consistent,
|
|
167
|
+
value: consistent ? 'consistent' : 'randomized',
|
|
168
|
+
weight: !consistent ? 20 : 0,
|
|
169
|
+
confidence: !consistent ? 0.9 : 0,
|
|
170
|
+
};
|
|
171
|
+
} catch {
|
|
172
|
+
return { name: 'canvas_consistency', detected: false, value: 'error', weight: 0, confidence: 0 };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Deep WebGL parameter check.
|
|
178
|
+
* Query many obscure WebGL parameters; spoofed renderers often miss some.
|
|
179
|
+
*/
|
|
180
|
+
function probeWebGLParamDepth(): DeepProbeResult {
|
|
181
|
+
try {
|
|
182
|
+
const canvas = document.createElement('canvas');
|
|
183
|
+
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
|
184
|
+
if (!gl) return { name: 'webgl_param_depth', detected: false, value: 'no_webgl', weight: 0, confidence: 0 };
|
|
185
|
+
|
|
186
|
+
const g = gl as WebGLRenderingContext;
|
|
187
|
+
let inconsistencies = 0;
|
|
188
|
+
|
|
189
|
+
const ext = g.getExtension('WEBGL_debug_renderer_info');
|
|
190
|
+
const renderer = ext ? String(g.getParameter(ext.UNMASKED_RENDERER_WEBGL) ?? '') : '';
|
|
191
|
+
const vendor = ext ? String(g.getParameter(ext.UNMASKED_VENDOR_WEBGL) ?? '') : '';
|
|
192
|
+
|
|
193
|
+
// Check max values consistency with renderer
|
|
194
|
+
const maxTextureSize = g.getParameter(g.MAX_TEXTURE_SIZE) as number;
|
|
195
|
+
|
|
196
|
+
// Software renderers have specific limits
|
|
197
|
+
if (renderer.includes('SwiftShader') || renderer.includes('llvmpipe')) {
|
|
198
|
+
// Already caught by headless detection
|
|
199
|
+
} else if (renderer.includes('ANGLE')) {
|
|
200
|
+
// ANGLE on real GPU: maxTextureSize is usually 16384+
|
|
201
|
+
if (maxTextureSize < 4096) inconsistencies++;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Check: if vendor is "Google Inc." but not ANGLE = suspicious
|
|
205
|
+
if (vendor === 'Google Inc.' && !renderer.includes('ANGLE') && !renderer.includes('SwiftShader')) {
|
|
206
|
+
inconsistencies++;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Check aliased line width range (often not spoofed)
|
|
210
|
+
const aliasedLineRange = g.getParameter(g.ALIASED_LINE_WIDTH_RANGE) as Float32Array | null;
|
|
211
|
+
if (aliasedLineRange && aliasedLineRange[0] === aliasedLineRange[1]) {
|
|
212
|
+
inconsistencies++;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
name: 'webgl_param_depth',
|
|
217
|
+
detected: inconsistencies > 0,
|
|
218
|
+
value: `renderer=${renderer.substring(0, 30)},issues=${inconsistencies}`,
|
|
219
|
+
weight: inconsistencies > 1 ? 12 : inconsistencies > 0 ? 5 : 0,
|
|
220
|
+
confidence: Math.min(1, inconsistencies / 2),
|
|
221
|
+
};
|
|
222
|
+
} catch {
|
|
223
|
+
return { name: 'webgl_param_depth', detected: false, value: 'error', weight: 0, confidence: 0 };
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Intl API consistency check.
|
|
229
|
+
* Timezone, locale, and Intl formatting should all be consistent.
|
|
230
|
+
* Fingerprint spoofers often miss Intl.
|
|
231
|
+
*/
|
|
232
|
+
function probeIntlConsistency(): DeepProbeResult {
|
|
233
|
+
let inconsistencies = 0;
|
|
234
|
+
try {
|
|
235
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
236
|
+
const locale = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
237
|
+
const tzOffset = new Date().getTimezoneOffset();
|
|
238
|
+
|
|
239
|
+
// Check: navigator.language vs Intl locale
|
|
240
|
+
const navLang = navigator.language;
|
|
241
|
+
if (navLang && locale && navLang.split('-')[0] !== locale.split('-')[0]) {
|
|
242
|
+
// Language mismatch between navigator and Intl
|
|
243
|
+
inconsistencies++;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Check: timezone offset should be plausible for the Intl timezone
|
|
247
|
+
// This is hard to fake consistently
|
|
248
|
+
if (tz && tzOffset !== undefined) {
|
|
249
|
+
const d = new Date();
|
|
250
|
+
const fmt = new Intl.DateTimeFormat('en-US', { timeZone: tz, hour: 'numeric', hour12: false });
|
|
251
|
+
// If this throws, tz is invalid
|
|
252
|
+
try { fmt.format(d); } catch { inconsistencies++; }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Check NumberFormat consistency
|
|
256
|
+
const nf = new Intl.NumberFormat(navLang);
|
|
257
|
+
nf.format(1234.5);
|
|
258
|
+
// formatted should use the grouping/decimal separators of navLang
|
|
259
|
+
// If navigator.language was spoofed but Intl wasn't, this may reveal inconsistency
|
|
260
|
+
} catch {
|
|
261
|
+
inconsistencies++;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
name: 'intl_consistency',
|
|
266
|
+
detected: inconsistencies > 0,
|
|
267
|
+
value: String(inconsistencies),
|
|
268
|
+
weight: inconsistencies > 0 ? 8 : 0,
|
|
269
|
+
confidence: Math.min(1, inconsistencies / 2),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Timer coherence test.
|
|
275
|
+
* Compare performance.now() vs Date.now() drift.
|
|
276
|
+
* Automated environments sometimes have desynchronized timers.
|
|
277
|
+
*/
|
|
278
|
+
function probeTimerCoherence(): DeepProbeResult {
|
|
279
|
+
const perfStart = performance.now();
|
|
280
|
+
const dateStart = Date.now();
|
|
281
|
+
|
|
282
|
+
// Busy wait ~5ms
|
|
283
|
+
while (performance.now() - perfStart < 5) { /* spin */ }
|
|
284
|
+
|
|
285
|
+
const perfElapsed = performance.now() - perfStart;
|
|
286
|
+
const dateElapsed = Date.now() - dateStart;
|
|
287
|
+
|
|
288
|
+
const drift = Math.abs(perfElapsed - dateElapsed);
|
|
289
|
+
// Normally these should agree within ~2ms
|
|
290
|
+
// Virtualized or hooked timers may show larger drift
|
|
291
|
+
const detected = drift > 10;
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
name: 'timer_coherence',
|
|
295
|
+
detected,
|
|
296
|
+
value: `perf=${perfElapsed.toFixed(2)},date=${dateElapsed},drift=${drift.toFixed(2)}`,
|
|
297
|
+
weight: detected ? 8 : 0,
|
|
298
|
+
confidence: detected ? 0.6 : 0,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Font rendering measurement.
|
|
304
|
+
* Measure specific text rendering dimensions; spoofed font lists create inconsistencies.
|
|
305
|
+
*/
|
|
306
|
+
function probeFontRendering(): DeepProbeResult {
|
|
307
|
+
try {
|
|
308
|
+
const testFonts = ['monospace', 'serif', 'sans-serif'];
|
|
309
|
+
const span = document.createElement('span');
|
|
310
|
+
span.style.position = 'absolute';
|
|
311
|
+
span.style.left = '-9999px';
|
|
312
|
+
span.style.fontSize = '72px';
|
|
313
|
+
span.textContent = 'mmmmmmmmmmlli';
|
|
314
|
+
document.body.appendChild(span);
|
|
315
|
+
|
|
316
|
+
const widths: number[] = [];
|
|
317
|
+
for (const font of testFonts) {
|
|
318
|
+
span.style.fontFamily = font;
|
|
319
|
+
widths.push(span.offsetWidth);
|
|
320
|
+
}
|
|
321
|
+
document.body.removeChild(span);
|
|
322
|
+
|
|
323
|
+
// All three should produce different widths on real systems
|
|
324
|
+
const allSame = widths[0] === widths[1] && widths[1] === widths[2];
|
|
325
|
+
const detected = allSame && widths[0] > 0;
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
name: 'font_rendering',
|
|
329
|
+
detected,
|
|
330
|
+
value: widths.join(','),
|
|
331
|
+
weight: detected ? 10 : 0,
|
|
332
|
+
confidence: detected ? 0.7 : 0,
|
|
333
|
+
};
|
|
334
|
+
} catch {
|
|
335
|
+
return { name: 'font_rendering', detected: false, value: 'error', weight: 0, confidence: 0 };
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Check if toString() on native objects has been overridden.
|
|
341
|
+
* Stealth plugins often override toString to hide modifications.
|
|
342
|
+
*/
|
|
343
|
+
function probeObjectStringify(): DeepProbeResult {
|
|
344
|
+
let tampered = 0;
|
|
345
|
+
try {
|
|
346
|
+
// Check if Function.prototype.toString itself was modified
|
|
347
|
+
const toStringStr = Function.prototype.toString.toString();
|
|
348
|
+
if (!toStringStr.includes('[native code]')) tampered += 2;
|
|
349
|
+
|
|
350
|
+
// Check Object.getOwnPropertyDescriptor hasn't been wrapped
|
|
351
|
+
const gopdStr = Object.getOwnPropertyDescriptor.toString();
|
|
352
|
+
if (!gopdStr.includes('[native code]')) tampered++;
|
|
353
|
+
|
|
354
|
+
// Check Proxy detection: new Proxy wrapping often leaks
|
|
355
|
+
const test = { a: 1 };
|
|
356
|
+
const keys = Object.keys(test);
|
|
357
|
+
if (keys.length !== 1) tampered++;
|
|
358
|
+
} catch {
|
|
359
|
+
tampered++;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
name: 'object_stringify_tampering',
|
|
364
|
+
detected: tampered > 0,
|
|
365
|
+
value: String(tampered),
|
|
366
|
+
weight: tampered >= 2 ? 15 : tampered > 0 ? 8 : 0,
|
|
367
|
+
confidence: Math.min(1, tampered / 2),
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Check Worker scope availability.
|
|
373
|
+
* Some automation tools have inconsistent Worker support.
|
|
374
|
+
*/
|
|
375
|
+
function probeWorkerScope(): DeepProbeResult {
|
|
376
|
+
try {
|
|
377
|
+
const hasServiceWorker = 'serviceWorker' in navigator;
|
|
378
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
379
|
+
|
|
380
|
+
// Chrome should have all three
|
|
381
|
+
if (ua.includes('chrome') && !ua.includes('edge')) {
|
|
382
|
+
if (!hasServiceWorker) return { name: 'worker_scope', detected: true, value: 'no_sw', weight: 10, confidence: 0.7 };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return { name: 'worker_scope', detected: false, value: 'ok', weight: 0, confidence: 0 };
|
|
386
|
+
} catch {
|
|
387
|
+
return { name: 'worker_scope', detected: false, value: 'error', weight: 0, confidence: 0 };
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* SharedArrayBuffer availability check.
|
|
393
|
+
* Requires COOP/COEP headers; automated environments often don't have this.
|
|
394
|
+
*/
|
|
395
|
+
function probeSharedArrayBuffer(): DeepProbeResult {
|
|
396
|
+
const hasSAB = typeof SharedArrayBuffer !== 'undefined';
|
|
397
|
+
// Not having SAB is common and not suspicious by itself
|
|
398
|
+
// But having it when COOP headers aren't set IS suspicious
|
|
399
|
+
return { name: 'shared_array_buffer', detected: false, value: String(hasSAB), weight: 0, confidence: 0 };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* AudioContext fingerprint timing.
|
|
404
|
+
* Real audio processing takes measurable time; mocked contexts return instantly.
|
|
405
|
+
*/
|
|
406
|
+
function probeAudioContextFingerprint(): DeepProbeResult {
|
|
407
|
+
try {
|
|
408
|
+
const w = window as typeof window & { webkitAudioContext?: typeof AudioContext };
|
|
409
|
+
const AudioCtx = w.AudioContext || w.webkitAudioContext;
|
|
410
|
+
if (!AudioCtx) return { name: 'audio_context', detected: false, value: 'unsupported', weight: 0, confidence: 0 };
|
|
411
|
+
|
|
412
|
+
const ctx = new AudioCtx();
|
|
413
|
+
const oscillator = ctx.createOscillator();
|
|
414
|
+
const analyser = ctx.createAnalyser();
|
|
415
|
+
const gain = ctx.createGain();
|
|
416
|
+
|
|
417
|
+
oscillator.type = 'triangle';
|
|
418
|
+
oscillator.connect(analyser);
|
|
419
|
+
analyser.connect(gain);
|
|
420
|
+
gain.connect(ctx.destination);
|
|
421
|
+
|
|
422
|
+
gain.gain.value = 0; // mute
|
|
423
|
+
oscillator.start(0);
|
|
424
|
+
oscillator.stop(ctx.currentTime + 0.01);
|
|
425
|
+
|
|
426
|
+
const data = new Float32Array(analyser.fftSize);
|
|
427
|
+
analyser.getFloatTimeDomainData(data);
|
|
428
|
+
|
|
429
|
+
ctx.close();
|
|
430
|
+
|
|
431
|
+
// All zeros = mocked audio
|
|
432
|
+
const allZero = data.every(v => v === 0);
|
|
433
|
+
return {
|
|
434
|
+
name: 'audio_context',
|
|
435
|
+
detected: allZero,
|
|
436
|
+
value: allZero ? 'mocked' : 'real',
|
|
437
|
+
weight: allZero ? 8 : 0,
|
|
438
|
+
confidence: allZero ? 0.6 : 0,
|
|
439
|
+
};
|
|
440
|
+
} catch {
|
|
441
|
+
return { name: 'audio_context', detected: false, value: 'error', weight: 0, confidence: 0 };
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Event trust chain verification.
|
|
447
|
+
* Dispatch a custom event and verify isTrusted behavior.
|
|
448
|
+
*/
|
|
449
|
+
function probeEventTrustChain(): DeepProbeResult {
|
|
450
|
+
let tampered = false;
|
|
451
|
+
try {
|
|
452
|
+
const div = document.createElement('div');
|
|
453
|
+
let receivedTrusted: boolean | null = null;
|
|
454
|
+
|
|
455
|
+
div.addEventListener('click', (e: MouseEvent) => {
|
|
456
|
+
receivedTrusted = e.isTrusted;
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// Programmatically dispatched events should have isTrusted=false
|
|
460
|
+
div.dispatchEvent(new MouseEvent('click'));
|
|
461
|
+
|
|
462
|
+
// If isTrusted is true for programmatic event = hook detected
|
|
463
|
+
if (receivedTrusted === true) {
|
|
464
|
+
tampered = true;
|
|
465
|
+
}
|
|
466
|
+
} catch {
|
|
467
|
+
// Ignore
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return {
|
|
471
|
+
name: 'event_trust_chain',
|
|
472
|
+
detected: tampered,
|
|
473
|
+
value: String(tampered),
|
|
474
|
+
weight: tampered ? 20 : 0,
|
|
475
|
+
confidence: tampered ? 0.95 : 0,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Navigator property enumeration inconsistency.
|
|
481
|
+
* Count enumerable properties; automated browsers often have extra or missing ones.
|
|
482
|
+
*/
|
|
483
|
+
function probeNavigatorInconsistency(): DeepProbeResult {
|
|
484
|
+
let score = 0;
|
|
485
|
+
try {
|
|
486
|
+
// Check for extra properties injected by automation
|
|
487
|
+
const navKeys = Object.keys(navigator);
|
|
488
|
+
const suspiciousKeys = ['__driver', '__webdriver', '__selenium', '__nightmare', 'domAutomation', 'domAutomationController'];
|
|
489
|
+
for (const k of navKeys) {
|
|
490
|
+
if (suspiciousKeys.some(s => k.toLowerCase().includes(s))) score += 2;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// In normal Chrome, navigator shouldn't have many own enumerable properties
|
|
494
|
+
if (navKeys.length > 5) score++;
|
|
495
|
+
|
|
496
|
+
// Check if navigator.permissions.query exists and works correctly
|
|
497
|
+
if (!navigator.permissions) score++;
|
|
498
|
+
} catch {
|
|
499
|
+
score++;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
return {
|
|
503
|
+
name: 'navigator_inconsistency',
|
|
504
|
+
detected: score > 1,
|
|
505
|
+
value: String(score),
|
|
506
|
+
weight: score > 2 ? 12 : score > 0 ? 5 : 0,
|
|
507
|
+
confidence: Math.min(1, score / 3),
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Detect fingerprint browsers (AdsPower, HubStudio, Multilogin, GoLogin, etc.)
|
|
513
|
+
* These tools create isolated browser profiles with spoofed fingerprints.
|
|
514
|
+
* They leave detectable artifacts in window/navigator/DOM.
|
|
515
|
+
*/
|
|
516
|
+
function probeFingerprintBrowser(): DeepProbeResult {
|
|
517
|
+
let score = 0;
|
|
518
|
+
const artifacts: string[] = [];
|
|
519
|
+
|
|
520
|
+
try {
|
|
521
|
+
const w = window as any;
|
|
522
|
+
const n = navigator as any;
|
|
523
|
+
|
|
524
|
+
// AdsPower specific artifacts
|
|
525
|
+
if (w.__adspower || w.ADS_POWER || w._adspower_state) { score += 3; artifacts.push('adspower_global'); }
|
|
526
|
+
if (n.adspower || n.__adspower_id) { score += 3; artifacts.push('adspower_nav'); }
|
|
527
|
+
|
|
528
|
+
// HubStudio / Hubstudio artifacts
|
|
529
|
+
if (w.__hubstudio || w.HubStudio || w._hubstudio) { score += 3; artifacts.push('hubstudio_global'); }
|
|
530
|
+
|
|
531
|
+
// Multilogin artifacts
|
|
532
|
+
if (w.__multilogin || w.Multilogin || w._mla_data) { score += 3; artifacts.push('multilogin_global'); }
|
|
533
|
+
if (n.multiloginVersion || n.__multilogin_profile_id) { score += 3; artifacts.push('multilogin_nav'); }
|
|
534
|
+
|
|
535
|
+
// GoLogin artifacts
|
|
536
|
+
if (w.__gologin || w.GoLogin || w._gologin) { score += 3; artifacts.push('gologin_global'); }
|
|
537
|
+
|
|
538
|
+
// VMLogin artifacts
|
|
539
|
+
if (w.__vmlogin || w.VmLogin || w._vmlogin) { score += 3; artifacts.push('vmlogin_global'); }
|
|
540
|
+
|
|
541
|
+
// Generic fingerprint browser detection heuristics
|
|
542
|
+
|
|
543
|
+
// Check for profile-id or session-id cookies that fingerprint browsers often inject
|
|
544
|
+
const cookies = document.cookie;
|
|
545
|
+
const fpBrowserCookies = ['_adspower_', '_multilogin_', '_gologin_', '_vmlogin_', '_hubstudio_'];
|
|
546
|
+
for (const prefix of fpBrowserCookies) {
|
|
547
|
+
if (cookies.includes(prefix)) { score += 2; artifacts.push(`cookie_${prefix}`); }
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// Many fingerprint browsers inject custom extensions with predictable IDs
|
|
551
|
+
if ('chrome' in window && (window as any).chrome?.runtime) {
|
|
552
|
+
const knownExtIds = [
|
|
553
|
+
'fhbjgbiflinjbdggehcddcbncdddomop', // AdsPower
|
|
554
|
+
'nkbihfbeogaeaoehlefnkodbefgpgknn', // Some multilogin
|
|
555
|
+
];
|
|
556
|
+
for (const id of knownExtIds) {
|
|
557
|
+
try {
|
|
558
|
+
const img = new Image();
|
|
559
|
+
img.src = `chrome-extension://${id}/icon.png`;
|
|
560
|
+
// Can't fully check, but attempt is recorded
|
|
561
|
+
} catch { /* expected */ }
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Detect Chromium kernel modifications:
|
|
566
|
+
// Fingerprint browsers modify Chrome version string inconsistently
|
|
567
|
+
const ua = navigator.userAgent;
|
|
568
|
+
const chromeMatch = ua.match(/Chrome\/(\d+)/);
|
|
569
|
+
if (chromeMatch) {
|
|
570
|
+
const majorVersion = parseInt(chromeMatch[1]);
|
|
571
|
+
// Check if claimed Chrome version is consistent with feature support
|
|
572
|
+
if (majorVersion > 100 && !('cookieStore' in window)) {
|
|
573
|
+
score += 1; // Mild signal: cookie Store API missing in claimed modern Chrome
|
|
574
|
+
}
|
|
575
|
+
if (majorVersion > 90 && typeof (navigator as any).userAgentData === 'undefined') {
|
|
576
|
+
score += 1; // Modern Chrome should have userAgentData
|
|
577
|
+
artifacts.push('missing_ua_data');
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Timezone spoofing detection via Performance API timing
|
|
582
|
+
const perfTiming = performance.timing;
|
|
583
|
+
if (perfTiming && perfTiming.navigationStart > 0) {
|
|
584
|
+
const now = Date.now();
|
|
585
|
+
const drift = Math.abs(now - perfTiming.navigationStart - performance.now());
|
|
586
|
+
if (drift > 100) {
|
|
587
|
+
score += 1;
|
|
588
|
+
artifacts.push('timing_drift');
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
} catch { /* ignore detection errors */ }
|
|
593
|
+
|
|
594
|
+
return {
|
|
595
|
+
name: 'fingerprint_browser',
|
|
596
|
+
detected: score >= 3,
|
|
597
|
+
value: artifacts.length > 0 ? artifacts.join(',') : 'none',
|
|
598
|
+
weight: score >= 6 ? 25 : score >= 3 ? 15 : 0,
|
|
599
|
+
confidence: Math.min(1, score / 6),
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Detect multi-login browser profile artifacts via DOM/API inconsistencies.
|
|
605
|
+
* Fingerprint browsers create profiles that reuse the same underlying Chromium
|
|
606
|
+
* but spoof device parameters — resulting in subtle inconsistencies.
|
|
607
|
+
*/
|
|
608
|
+
function probeMultiLoginBrowserArtifacts(): DeepProbeResult {
|
|
609
|
+
let inconsistencies = 0;
|
|
610
|
+
|
|
611
|
+
try {
|
|
612
|
+
// Check: deviceMemory + hardwareConcurrency vs WebGL renderer
|
|
613
|
+
const mem = (navigator as any).deviceMemory;
|
|
614
|
+
const cores = navigator.hardwareConcurrency;
|
|
615
|
+
|
|
616
|
+
const canvas = document.createElement('canvas');
|
|
617
|
+
const gl = canvas.getContext('webgl');
|
|
618
|
+
if (gl) {
|
|
619
|
+
const ext = gl.getExtension('WEBGL_debug_renderer_info');
|
|
620
|
+
const renderer = ext ? String(gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) ?? '') : '';
|
|
621
|
+
|
|
622
|
+
// Low-end spoofed: claims 2GB RAM but has high-end GPU name
|
|
623
|
+
if (mem && mem <= 2 && renderer.match(/RTX|RX 6|RX 7|Arc A/i)) {
|
|
624
|
+
inconsistencies++;
|
|
625
|
+
}
|
|
626
|
+
// Claims many cores but software renderer
|
|
627
|
+
if (cores && cores >= 8 && renderer.match(/SwiftShader|llvmpipe|Software/i)) {
|
|
628
|
+
inconsistencies++;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Check: screen resolution vs device pixel ratio mismatch
|
|
633
|
+
const screenW = screen.width;
|
|
634
|
+
const screenH = screen.height;
|
|
635
|
+
const dpr = window.devicePixelRatio;
|
|
636
|
+
// Common spoofed combo: mobile resolution with desktop DPR
|
|
637
|
+
if (screenW <= 414 && screenH <= 896 && dpr === 1 && !('ontouchstart' in window)) {
|
|
638
|
+
inconsistencies++;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Check: platform vs userAgent consistency
|
|
642
|
+
const platform = navigator.platform;
|
|
643
|
+
const ua = navigator.userAgent;
|
|
644
|
+
if (platform === 'Win32' && ua.includes('Macintosh')) inconsistencies += 2;
|
|
645
|
+
if (platform === 'MacIntel' && ua.includes('Windows')) inconsistencies += 2;
|
|
646
|
+
if (platform === 'Linux x86_64' && ua.includes('Windows NT')) inconsistencies++;
|
|
647
|
+
|
|
648
|
+
// Check: language list consistency with timezone
|
|
649
|
+
const lang = navigator.language;
|
|
650
|
+
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
651
|
+
// Chinese language with US timezone or vice versa (mild signal)
|
|
652
|
+
if (lang.startsWith('zh') && tz.startsWith('America/')) inconsistencies++;
|
|
653
|
+
if (lang.startsWith('en-US') && tz.startsWith('Asia/Shanghai')) inconsistencies++;
|
|
654
|
+
|
|
655
|
+
} catch { /* ignore */ }
|
|
656
|
+
|
|
657
|
+
return {
|
|
658
|
+
name: 'multilogin_artifacts',
|
|
659
|
+
detected: inconsistencies >= 2,
|
|
660
|
+
value: String(inconsistencies),
|
|
661
|
+
weight: inconsistencies >= 3 ? 18 : inconsistencies >= 2 ? 10 : 0,
|
|
662
|
+
confidence: Math.min(1, inconsistencies / 4),
|
|
663
|
+
};
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Network request timing test.
|
|
668
|
+
* Measure time to fetch a tiny resource; automated environments in VMs
|
|
669
|
+
* sometimes have different network characteristics.
|
|
670
|
+
*/
|
|
671
|
+
async function probeNetworkTiming(): Promise<DeepProbeResult> {
|
|
672
|
+
try {
|
|
673
|
+
const start = performance.now();
|
|
674
|
+
// Fetch the same page/a small known resource
|
|
675
|
+
await fetch(window.location.href, { method: 'HEAD', cache: 'no-store' }).catch(() => {});
|
|
676
|
+
const elapsed = performance.now() - start;
|
|
677
|
+
|
|
678
|
+
// Very fast response (< 1ms) = mocked fetch
|
|
679
|
+
const detected = elapsed < 0.5;
|
|
680
|
+
return {
|
|
681
|
+
name: 'network_timing',
|
|
682
|
+
detected,
|
|
683
|
+
value: `${elapsed.toFixed(2)}ms`,
|
|
684
|
+
weight: detected ? 10 : 0,
|
|
685
|
+
confidence: detected ? 0.7 : 0,
|
|
686
|
+
};
|
|
687
|
+
} catch {
|
|
688
|
+
return { name: 'network_timing', detected: false, value: 'error', weight: 0, confidence: 0 };
|
|
689
|
+
}
|
|
690
|
+
}
|