@anonympins/fingerprint 0.7.3 → 0.7.5
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 +41 -0
- package/README.md +1 -1
- package/package.json +109 -107
- package/public/fingerprint-anti-bot.zip +0 -0
- package/src/js/dynamic-wasm.js +218 -218
- package/src/js/fingerprint.client.js +329 -88
- package/src/js/fingerprint.client.obfuscated.js +1 -0
- package/src/js/fingerprint.js +486 -231
- 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/dynamic-wasm.test.js +77 -77
- package/src/js/tests/fingerprint.client.init.test.js +108 -0
- package/src/js/tests/fingerprint.test.js +153 -3
- package/src/js/tests/threatIntel.test.js +40 -1
- package/src/php/Challenge/ChallengeUtils.php +25 -16
- package/src/php/Config/SecurityProfiles.php +5 -0
- package/src/php/FingerprintEngine.php +186 -37
- package/src/php/Tests/ChallengeUtilsTest.php +469 -453
- package/src/php/Tests/RequestUtilsTest.php +1 -1
- package/src/php/Tests/ThreatIntelTest.php +34 -0
- package/src/php/Utils/RequestUtils.php +257 -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
|
@@ -1,78 +1,78 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { DynamicWasmGenerator } from '../dynamic-wasm.js';
|
|
3
|
-
|
|
4
|
-
// Helper function to decode ULEB128 (for verification)
|
|
5
|
-
function decodeULEB128(bytes) {
|
|
6
|
-
let result = 0;
|
|
7
|
-
let shift = 0;
|
|
8
|
-
for (const byte of bytes) {
|
|
9
|
-
result |= (byte & 0x7f) << shift;
|
|
10
|
-
if (!(byte & 0x80)) {
|
|
11
|
-
break;
|
|
12
|
-
}
|
|
13
|
-
shift += 7;
|
|
14
|
-
}
|
|
15
|
-
return result;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
describe('DynamicWasmGenerator', () => {
|
|
19
|
-
it('should generate a valid WASM module buffer', () => {
|
|
20
|
-
const constants = { seed: 123, multiplier: 33, adder: 7 };
|
|
21
|
-
const wasmBuffer = DynamicWasmGenerator.generate(constants);
|
|
22
|
-
|
|
23
|
-
// Basic check: ensure it's a Buffer and starts with WASM magic number
|
|
24
|
-
expect(wasmBuffer).toBeInstanceOf(Buffer);
|
|
25
|
-
expect(wasmBuffer.slice(0, 4).toString('hex')).toBe('0061736d'); // \0asm
|
|
26
|
-
expect(wasmBuffer.slice(4, 8).toString('hex')).toBe('01000000'); // Version 1
|
|
27
|
-
|
|
28
|
-
// Attempt to compile and instantiate the module to ensure validity
|
|
29
|
-
let instance;
|
|
30
|
-
try {
|
|
31
|
-
const module = new WebAssembly.Module(wasmBuffer);
|
|
32
|
-
instance = new WebAssembly.Instance(module, {});
|
|
33
|
-
} catch (e) {
|
|
34
|
-
// If compilation/instantiation fails, it's an invalid WASM module
|
|
35
|
-
expect.fail(`Generated WASM module is invalid: ${e.message}`);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// Verify the exported hash function exists
|
|
39
|
-
expect(instance.exports.hash).toBeInstanceOf(Function);
|
|
40
|
-
expect(instance.exports.memory).toBeInstanceOf(WebAssembly.Memory);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('should produce a consistent hash result with the JS fallback', () => {
|
|
44
|
-
const constants = { seed: 42, multiplier: 1597334677, adder: 12345 };
|
|
45
|
-
const testString = "hello world";
|
|
46
|
-
|
|
47
|
-
const wasmBuffer = DynamicWasmGenerator.generate(constants);
|
|
48
|
-
const module = new WebAssembly.Module(wasmBuffer);
|
|
49
|
-
const instance = new WebAssembly.Instance(module, {});
|
|
50
|
-
const exports = instance.exports;
|
|
51
|
-
const memory = exports.memory;
|
|
52
|
-
|
|
53
|
-
// Write string to WASM memory
|
|
54
|
-
const encoder = new TextEncoder();
|
|
55
|
-
const bytes = encoder.encode(testString);
|
|
56
|
-
const view = new Uint8Array(memory.buffer);
|
|
57
|
-
view.set(bytes, 0); // Assuming hash function expects string at address 0
|
|
58
|
-
|
|
59
|
-
// Calculate hash using WASM
|
|
60
|
-
const wasmHash = exports.hash(0, bytes.length);
|
|
61
|
-
|
|
62
|
-
// Calculate hash using JS fallback
|
|
63
|
-
const jsHash = DynamicWasmGenerator.hashJs(testString, constants);
|
|
64
|
-
|
|
65
|
-
// The WASM and JS implementations should yield the same result
|
|
66
|
-
expect(wasmHash).toBe(jsHash);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('should generate different WASM modules for different constants', () => {
|
|
70
|
-
const constants1 = { seed: 1, multiplier: 2, adder: 3 };
|
|
71
|
-
const constants2 = { seed: 4, multiplier: 5, adder: 6 };
|
|
72
|
-
|
|
73
|
-
const wasmBuffer1 = DynamicWasmGenerator.generate(constants1);
|
|
74
|
-
const wasmBuffer2 = DynamicWasmGenerator.generate(constants2);
|
|
75
|
-
|
|
76
|
-
expect(wasmBuffer1.toString('hex')).not.toBe(wasmBuffer2.toString('hex'));
|
|
77
|
-
});
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { DynamicWasmGenerator } from '../dynamic-wasm.js';
|
|
3
|
+
|
|
4
|
+
// Helper function to decode ULEB128 (for verification)
|
|
5
|
+
function decodeULEB128(bytes) {
|
|
6
|
+
let result = 0;
|
|
7
|
+
let shift = 0;
|
|
8
|
+
for (const byte of bytes) {
|
|
9
|
+
result |= (byte & 0x7f) << shift;
|
|
10
|
+
if (!(byte & 0x80)) {
|
|
11
|
+
break;
|
|
12
|
+
}
|
|
13
|
+
shift += 7;
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('DynamicWasmGenerator', () => {
|
|
19
|
+
it('should generate a valid WASM module buffer', () => {
|
|
20
|
+
const constants = { seed: 123, multiplier: 33, adder: 7 };
|
|
21
|
+
const wasmBuffer = DynamicWasmGenerator.generate(constants);
|
|
22
|
+
|
|
23
|
+
// Basic check: ensure it's a Buffer and starts with WASM magic number
|
|
24
|
+
expect(wasmBuffer).toBeInstanceOf(Buffer);
|
|
25
|
+
expect(wasmBuffer.slice(0, 4).toString('hex')).toBe('0061736d'); // \0asm
|
|
26
|
+
expect(wasmBuffer.slice(4, 8).toString('hex')).toBe('01000000'); // Version 1
|
|
27
|
+
|
|
28
|
+
// Attempt to compile and instantiate the module to ensure validity
|
|
29
|
+
let instance;
|
|
30
|
+
try {
|
|
31
|
+
const module = new WebAssembly.Module(wasmBuffer);
|
|
32
|
+
instance = new WebAssembly.Instance(module, {});
|
|
33
|
+
} catch (e) {
|
|
34
|
+
// If compilation/instantiation fails, it's an invalid WASM module
|
|
35
|
+
expect.fail(`Generated WASM module is invalid: ${e.message}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Verify the exported hash function exists
|
|
39
|
+
expect(instance.exports.hash).toBeInstanceOf(Function);
|
|
40
|
+
expect(instance.exports.memory).toBeInstanceOf(WebAssembly.Memory);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should produce a consistent hash result with the JS fallback', () => {
|
|
44
|
+
const constants = { seed: 42, multiplier: 1597334677, adder: 12345 };
|
|
45
|
+
const testString = "hello world";
|
|
46
|
+
|
|
47
|
+
const wasmBuffer = DynamicWasmGenerator.generate(constants);
|
|
48
|
+
const module = new WebAssembly.Module(wasmBuffer);
|
|
49
|
+
const instance = new WebAssembly.Instance(module, {});
|
|
50
|
+
const exports = instance.exports;
|
|
51
|
+
const memory = exports.memory;
|
|
52
|
+
|
|
53
|
+
// Write string to WASM memory
|
|
54
|
+
const encoder = new TextEncoder();
|
|
55
|
+
const bytes = encoder.encode(testString);
|
|
56
|
+
const view = new Uint8Array(memory.buffer);
|
|
57
|
+
view.set(bytes, 0); // Assuming hash function expects string at address 0
|
|
58
|
+
|
|
59
|
+
// Calculate hash using WASM
|
|
60
|
+
const wasmHash = exports.hash(0, bytes.length);
|
|
61
|
+
|
|
62
|
+
// Calculate hash using JS fallback
|
|
63
|
+
const jsHash = DynamicWasmGenerator.hashJs(testString, constants);
|
|
64
|
+
|
|
65
|
+
// The WASM and JS implementations should yield the same result
|
|
66
|
+
expect(wasmHash).toBe(jsHash);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should generate different WASM modules for different constants', () => {
|
|
70
|
+
const constants1 = { seed: 1, multiplier: 2, adder: 3 };
|
|
71
|
+
const constants2 = { seed: 4, multiplier: 5, adder: 6 };
|
|
72
|
+
|
|
73
|
+
const wasmBuffer1 = DynamicWasmGenerator.generate(constants1);
|
|
74
|
+
const wasmBuffer2 = DynamicWasmGenerator.generate(constants2);
|
|
75
|
+
|
|
76
|
+
expect(wasmBuffer1.toString('hex')).not.toBe(wasmBuffer2.toString('hex'));
|
|
77
|
+
});
|
|
78
78
|
});
|
|
@@ -17,8 +17,14 @@ Object.defineProperty(global, 'document', { value: dom.window.document, writable
|
|
|
17
17
|
Object.defineProperty(global, 'navigator', { value: dom.window.navigator, writable: true });
|
|
18
18
|
Object.defineProperty(global, 'screen', { value: dom.window.screen, writable: true });
|
|
19
19
|
Object.defineProperty(global, 'performance', { value: dom.window.performance, writable: true });
|
|
20
|
+
if (typeof globalThis.crypto !== 'undefined') {
|
|
21
|
+
Object.defineProperty(dom.window, 'crypto', { value: globalThis.crypto, writable: true, configurable: true });
|
|
22
|
+
}
|
|
23
|
+
Object.defineProperty(global, 'CustomEvent', { value: dom.window.CustomEvent, writable: true, configurable: true });
|
|
24
|
+
Object.defineProperty(global, 'Event', { value: dom.window.Event, writable: true, configurable: true });
|
|
20
25
|
|
|
21
26
|
global.fetch = vi.fn(); // Mock global fetch
|
|
27
|
+
window.fetch = (...args) => global.fetch(...args);
|
|
22
28
|
global.Headers = dom.window.Headers;
|
|
23
29
|
global.Request = dom.window.Request;
|
|
24
30
|
global.URL = dom.window.URL;
|
|
@@ -45,6 +51,7 @@ describe('ClientLibrary.initializeClient', () => {
|
|
|
45
51
|
initHoneypotsSpy = vi.spyOn(ClientLibrary, 'initializeHoneypots');
|
|
46
52
|
injectTrapsSpy = vi.spyOn(ClientLibrary, 'injectTrapLinks');
|
|
47
53
|
initFetchSpy = vi.spyOn(ClientLibrary, 'initializeFetch');
|
|
54
|
+
window.fetch = (...args) => global.fetch(...args);
|
|
48
55
|
injectPhantomTrapsSpy = vi.spyOn(ClientLibrary, 'injectPhantomTraps');
|
|
49
56
|
});
|
|
50
57
|
|
|
@@ -144,4 +151,105 @@ describe('ClientLibrary.initializeClient', () => {
|
|
|
144
151
|
expect(initFetchSpy).toHaveBeenCalledWith(config.fetch.targetDomains);
|
|
145
152
|
});
|
|
146
153
|
|
|
154
|
+
it('should abort challenge retry loop when maxRetries is reached', async () => {
|
|
155
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
156
|
+
const mockResponse = {
|
|
157
|
+
status: 404,
|
|
158
|
+
headers: new Headers({ 'content-type': 'application/json' }),
|
|
159
|
+
bodyUsed: false,
|
|
160
|
+
json: vi.fn().mockResolvedValue({
|
|
161
|
+
challenge: { type: 'cpu_target', nonce: 'nonce-1', cpuTarget: '0000ffff' }
|
|
162
|
+
}),
|
|
163
|
+
clone() { return this; }
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const result = await ClientLibrary.solveChallengeAndRetry(
|
|
167
|
+
mockResponse,
|
|
168
|
+
'http://localhost/api/data',
|
|
169
|
+
{ _powRetryCount: 3 },
|
|
170
|
+
3
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
expect(result).toBe(mockResponse);
|
|
174
|
+
expect(mockResponse.json).not.toHaveBeenCalled();
|
|
175
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Challenge retry limit reached (3)'));
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('should inject phantom traps within closed shadow root and fire trigger on interaction', () => {
|
|
179
|
+
const onTriggerSpy = vi.spyOn(ClientLibrary, 'onHoneypotTrigger');
|
|
180
|
+
let capturedShadow = null;
|
|
181
|
+
const origAttachShadow = dom.window.Element.prototype.attachShadow;
|
|
182
|
+
vi.spyOn(dom.window.Element.prototype, 'attachShadow').mockImplementation(function (init) {
|
|
183
|
+
capturedShadow = origAttachShadow.call(this, init);
|
|
184
|
+
return capturedShadow;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Ensure document.body exists
|
|
188
|
+
document.body.innerHTML = '';
|
|
189
|
+
|
|
190
|
+
ClientLibrary.injectPhantomTraps();
|
|
191
|
+
|
|
192
|
+
const host = document.body.lastElementChild;
|
|
193
|
+
expect(host).not.toBeNull();
|
|
194
|
+
expect(host.getAttribute('aria-hidden')).toBe('true');
|
|
195
|
+
// Under closed mode, host.shadowRoot is inaccessible (null) from external callers
|
|
196
|
+
expect(host.shadowRoot).toBeNull();
|
|
197
|
+
expect(capturedShadow).not.toBeNull();
|
|
198
|
+
|
|
199
|
+
// Dispatch events on attached phantom elements inside closed shadow root
|
|
200
|
+
const link = capturedShadow.querySelector('a');
|
|
201
|
+
expect(link).not.toBeNull();
|
|
202
|
+
link.dispatchEvent(new Event('focus'));
|
|
203
|
+
expect(onTriggerSpy).toHaveBeenCalled();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('should inject trap links within closed shadow root and trigger on honeypot click', () => {
|
|
207
|
+
const onTriggerSpy = vi.spyOn(ClientLibrary, 'onHoneypotTrigger');
|
|
208
|
+
let capturedShadow = null;
|
|
209
|
+
const origAttachShadow = dom.window.Element.prototype.attachShadow;
|
|
210
|
+
vi.spyOn(dom.window.Element.prototype, 'attachShadow').mockImplementation(function (init) {
|
|
211
|
+
capturedShadow = origAttachShadow.call(this, init);
|
|
212
|
+
return capturedShadow;
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
document.body.innerHTML = '';
|
|
216
|
+
|
|
217
|
+
ClientLibrary.injectTrapLinks(['/trap-url-1', '/trap-url-2']);
|
|
218
|
+
|
|
219
|
+
const host = document.body.lastElementChild;
|
|
220
|
+
expect(host).not.toBeNull();
|
|
221
|
+
expect(host.getAttribute('aria-hidden')).toBe('true');
|
|
222
|
+
expect(host.shadowRoot).toBeNull(); // Closed Shadow DOM verification
|
|
223
|
+
expect(capturedShadow).not.toBeNull();
|
|
224
|
+
|
|
225
|
+
const link = capturedShadow.querySelector('a');
|
|
226
|
+
expect(link).not.toBeNull();
|
|
227
|
+
link.dispatchEvent(new Event('click'));
|
|
228
|
+
expect(onTriggerSpy).toHaveBeenCalled();
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should initialize honeypot form inputs within closed shadow root', () => {
|
|
232
|
+
const onTriggerSpy = vi.spyOn(ClientLibrary, 'onHoneypotTrigger');
|
|
233
|
+
let capturedShadow = null;
|
|
234
|
+
const origAttachShadow = dom.window.Element.prototype.attachShadow;
|
|
235
|
+
vi.spyOn(dom.window.Element.prototype, 'attachShadow').mockImplementation(function (init) {
|
|
236
|
+
capturedShadow = origAttachShadow.call(this, init);
|
|
237
|
+
return capturedShadow;
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
document.body.innerHTML = '';
|
|
241
|
+
|
|
242
|
+
ClientLibrary.initializeHoneypots(['bot_field_trap']);
|
|
243
|
+
|
|
244
|
+
const host = document.body.lastElementChild;
|
|
245
|
+
expect(host).not.toBeNull();
|
|
246
|
+
expect(host.getAttribute('aria-hidden')).toBe('true');
|
|
247
|
+
expect(host.shadowRoot).toBeNull();
|
|
248
|
+
expect(capturedShadow).not.toBeNull();
|
|
249
|
+
|
|
250
|
+
const input = capturedShadow.querySelector('input');
|
|
251
|
+
expect(input).not.toBeNull();
|
|
252
|
+
input.dispatchEvent(new Event('input'));
|
|
253
|
+
expect(onTriggerSpy).toHaveBeenCalled();
|
|
254
|
+
});
|
|
147
255
|
});
|
|
@@ -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(),
|
|
@@ -3068,4 +3200,22 @@ describe('IP Registration and Filtering', () => {
|
|
|
3068
3200
|
expect(process.env.ED25519_PUBLIC_KEY).toBe(dummyKeys.publicKey);
|
|
3069
3201
|
});
|
|
3070
3202
|
});
|
|
3203
|
+
|
|
3204
|
+
describe('Closed-Shadow DOM Honeypot Traps (Feature 19)', () => {
|
|
3205
|
+
it('should generate closed declarative shadow DOM trap container for trap URLs', () => {
|
|
3206
|
+
const trapUrls = ['/trap-1', '/trap-2'];
|
|
3207
|
+
const markup = __internal.generateClosedShadowTraps(trapUrls);
|
|
3208
|
+
|
|
3209
|
+
expect(markup).toContain('aria-hidden="true"');
|
|
3210
|
+
expect(markup).toContain('shadowrootmode="closed"');
|
|
3211
|
+
expect(markup).toContain('shadowroot="closed"');
|
|
3212
|
+
expect(markup).toContain('href="/trap-1"');
|
|
3213
|
+
expect(markup).toContain('href="/trap-2"');
|
|
3214
|
+
});
|
|
3215
|
+
|
|
3216
|
+
it('should return an empty string if trapUrls array is empty or invalid', () => {
|
|
3217
|
+
expect(__internal.generateClosedShadowTraps([])).toBe('');
|
|
3218
|
+
expect(__internal.generateClosedShadowTraps(null)).toBe('');
|
|
3219
|
+
});
|
|
3220
|
+
});
|
|
3071
3221
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it } from 'vitest';
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
2
|
import { __internal, configureStore } from '../fingerprint.js';
|
|
3
3
|
|
|
4
4
|
describe('Threat Intelligence - Rationale-Driven Defensive Logic', () => {
|
|
@@ -73,4 +73,43 @@ describe('Threat Intelligence - Rationale-Driven Defensive Logic', () => {
|
|
|
73
73
|
expect(vector.threatIntelScore).toBeGreaterThan(70.0);
|
|
74
74
|
expect(vector.threatIntelScore).toBeLessThanOrEqual(95.0);
|
|
75
75
|
});
|
|
76
|
+
|
|
77
|
+
it('should broadcast banned ZKP with differential privacy (perturbed timestamps and decoy injection)', async () => {
|
|
78
|
+
const calls = [];
|
|
79
|
+
const originalFetch = globalThis.fetch;
|
|
80
|
+
globalThis.fetch = vi.fn(async (url, init) => {
|
|
81
|
+
calls.push({ url, init });
|
|
82
|
+
return { ok: true, json: async () => ({ status: 'synchronized' }) };
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const config = {
|
|
86
|
+
federatedPeers: ['https://peer1.example.com', 'https://peer2.example.com'],
|
|
87
|
+
federationSecret: 'test-secret-key-32-chars-long!!',
|
|
88
|
+
differentialPrivacy: {
|
|
89
|
+
enabled: true,
|
|
90
|
+
epsilon: 1.0,
|
|
91
|
+
dummyRate: 1.0 // Force decoy generation for deterministic testing
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const realZkpY = 'deadbeef12345678';
|
|
96
|
+
await __internal.broadcastBannedZkp(realZkpY, config);
|
|
97
|
+
|
|
98
|
+
// Should broadcast to 2 peers with real key + 2 peers with decoy key
|
|
99
|
+
expect(calls.length).toBe(4);
|
|
100
|
+
|
|
101
|
+
const bodies = calls.map(c => JSON.parse(c.init.body));
|
|
102
|
+
const zkpYs = bodies.map(b => b.zkpY);
|
|
103
|
+
|
|
104
|
+
expect(zkpYs).toContain(realZkpY);
|
|
105
|
+
const decoyKey = zkpYs.find(k => k !== realZkpY);
|
|
106
|
+
expect(decoyKey).toBeDefined();
|
|
107
|
+
expect(decoyKey).not.toBe(realZkpY);
|
|
108
|
+
|
|
109
|
+
for (const call of calls) {
|
|
110
|
+
const timestamp = Number(call.init.headers['X-Federation-Timestamp']);
|
|
111
|
+
expect(timestamp).toBeGreaterThan(0);
|
|
112
|
+
}
|
|
113
|
+
globalThis.fetch = originalFetch;
|
|
114
|
+
});
|
|
76
115
|
});
|
|
@@ -605,6 +605,11 @@ class ChallengeUtils
|
|
|
605
605
|
|
|
606
606
|
if (!$expiry || (int)floor(microtime(true) * 1000) > (int)$expiry) {
|
|
607
607
|
return false;
|
|
608
|
+
}
|
|
609
|
+
if (!empty($ticketData['greenlist']) || str_starts_with((string)$storedDeviceHash, 'webauthn:greenlist:')) {
|
|
610
|
+
if (empty($deviceId) || $deviceId === $storedDeviceId) {
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
608
613
|
}
|
|
609
614
|
if ($storedDeviceHash && str_starts_with($storedDeviceHash, 'zkp:')) {
|
|
610
615
|
$expectedY = explode(':', $storedDeviceHash, 2)[1] ?? '';
|
|
@@ -644,6 +649,11 @@ class ChallengeUtils
|
|
|
644
649
|
if (!$expiry || (int)floor(microtime(true) * 1000) > (int)$expiry) {
|
|
645
650
|
$store->delete("ticket:{$ticket}");
|
|
646
651
|
return false;
|
|
652
|
+
}
|
|
653
|
+
if (!empty($ticketData['greenlist']) || str_starts_with((string)$storedDeviceHash, 'webauthn:greenlist:')) {
|
|
654
|
+
if (empty($deviceId) || $deviceId === $storedDeviceId) {
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
647
657
|
}
|
|
648
658
|
if ($storedDeviceHash && str_starts_with($storedDeviceHash, 'zkp:')) {
|
|
649
659
|
$expectedY = explode(':', $storedDeviceHash, 2)[1] ?? '';
|
|
@@ -789,39 +799,38 @@ class ChallengeUtils
|
|
|
789
799
|
/**
|
|
790
800
|
* Vérifie le limiteur de débit Token Bucket pour les demandes de challenge d'un sous-réseau.
|
|
791
801
|
*/
|
|
792
|
-
public static function checkChallengeRateLimit(string $clientIp): bool
|
|
802
|
+
public static function checkChallengeRateLimit(string $clientIp, float $capacity = 5.0, float $refillRate = 0.1): bool
|
|
793
803
|
{
|
|
794
|
-
$subnet = RequestUtils::getIpSubnet($clientIp);
|
|
795
|
-
if ($subnet === null) {
|
|
796
|
-
return false;
|
|
797
|
-
}
|
|
804
|
+
$subnet = RequestUtils::getIpSubnet($clientIp) ?? $clientIp;
|
|
798
805
|
|
|
799
806
|
$store = StoreManager::getStore();
|
|
800
807
|
$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
808
|
$now = microtime(true);
|
|
809
809
|
|
|
810
|
-
$
|
|
811
|
-
$
|
|
810
|
+
$rateLimitData = $store->get($key);
|
|
811
|
+
if (!is_array($rateLimitData) || !isset($rateLimitData['tokens'], $rateLimitData['lastRefill'])) {
|
|
812
|
+
$rateLimitData = [
|
|
813
|
+
'tokens' => $capacity,
|
|
814
|
+
'lastRefill' => $now
|
|
815
|
+
];
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
$elapsed = max(0.0, $now - (float)$rateLimitData['lastRefill']);
|
|
819
|
+
$tokens = min($capacity, (float)$rateLimitData['tokens'] + $elapsed * $refillRate);
|
|
820
|
+
$ttl = (int)max(60, ceil($capacity / max(0.1, $refillRate)));
|
|
812
821
|
|
|
813
822
|
if ($tokens < 1.0) {
|
|
814
823
|
$store->set($key, [
|
|
815
824
|
'tokens' => $tokens,
|
|
816
825
|
'lastRefill' => $now
|
|
817
|
-
],
|
|
826
|
+
], $ttl);
|
|
818
827
|
return false;
|
|
819
828
|
}
|
|
820
829
|
|
|
821
830
|
$store->set($key, [
|
|
822
831
|
'tokens' => $tokens - 1.0,
|
|
823
832
|
'lastRefill' => $now
|
|
824
|
-
],
|
|
833
|
+
], $ttl);
|
|
825
834
|
|
|
826
835
|
return true;
|
|
827
836
|
}
|
|
@@ -43,6 +43,7 @@ class SecurityProfiles
|
|
|
43
43
|
'tcpAnomalyScore' => 0.8,
|
|
44
44
|
'protocolAnomalyScore' => 0.8, // NEW: Anomalie de protocole (HTTP/2 et QUIC)
|
|
45
45
|
'renderingAnomalyScore' => 0.8, // NEW: Anomalie de rendu
|
|
46
|
+
'mtuAnomalyScore' => 0.9, // NOUVEAU: Poids pour l'anomalie MTU/fragmentation
|
|
46
47
|
'ipReputationScore' => 0.5, // NOUVEAU: Poids pour la réputation IP
|
|
47
48
|
'virtualizationScore' => 0.8,
|
|
48
49
|
],
|
|
@@ -97,6 +98,7 @@ class SecurityProfiles
|
|
|
97
98
|
'tcpAnomalyScore' => 1.0, // NEW: Anomalie de pile TCP/IP
|
|
98
99
|
'protocolAnomalyScore' => 1.0, // NEW: Anomalie de protocole (HTTP/2 et QUIC)
|
|
99
100
|
'renderingAnomalyScore' => 1.0, // NEW: Anomalie de rendu
|
|
101
|
+
'mtuAnomalyScore' => 0.9,
|
|
100
102
|
|
|
101
103
|
],
|
|
102
104
|
'thresholds' => ['low' => 10, 'medium' => 35, 'high' => 65, 'block' => 90],
|
|
@@ -149,6 +151,7 @@ class SecurityProfiles
|
|
|
149
151
|
'botnetClusterScore' => 0.7, // NOUVEAU: Poids pour le clustering botnet
|
|
150
152
|
'tcpAnomalyScore' => 0.8, // NEW: Anomalie de pile TCP/IP
|
|
151
153
|
'protocolAnomalyScore' => 0.8, // NEW: Anomalie de protocole (HTTP/2 et QUIC)
|
|
154
|
+
'mtuAnomalyScore' => 0.9,
|
|
152
155
|
|
|
153
156
|
],
|
|
154
157
|
'thresholds' => ['low' => 25, 'medium' => 50, 'high' => 80, 'block' => 95],
|
|
@@ -205,6 +208,7 @@ class SecurityProfiles
|
|
|
205
208
|
'tcpAnomalyScore' => 0.5, // NEW: Anomalie de pile TCP/IP
|
|
206
209
|
'protocolAnomalyScore' => 0.5, // NEW: Anomalie de protocole (HTTP/2 et QUIC)
|
|
207
210
|
'renderingAnomalyScore' => 0.5, // NEW: Anomalie de rendu
|
|
211
|
+
'mtuAnomalyScore' => 0.9,
|
|
208
212
|
|
|
209
213
|
],
|
|
210
214
|
'thresholds' => ['low' => 25, 'medium' => 55, 'high' => 80, 'block' => 95],
|
|
@@ -259,6 +263,7 @@ class SecurityProfiles
|
|
|
259
263
|
'tcpAnomalyScore' => 0.9, // NEW: Anomalie de pile TCP/IP
|
|
260
264
|
'protocolAnomalyScore' => 0.9, // NEW: Anomalie de protocole (HTTP/2 et QUIC)
|
|
261
265
|
'renderingAnomalyScore' => 0.9, // NEW: Anomalie de rendu
|
|
266
|
+
'mtuAnomalyScore' => 0.9,
|
|
262
267
|
|
|
263
268
|
],
|
|
264
269
|
'thresholds' => ['low' => 15, 'medium' => 40, 'high' => 70, 'block' => 90],
|