@anonympins/fingerprint 0.5.1 → 0.5.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.
- package/CHANGELOG.md +9 -0
- package/README.md +118 -115
- package/package.json +1 -1
- package/src/js/dynamic-wasm.js +52 -13
- package/src/js/fingerprint.builder.js +169 -168
- package/src/js/fingerprint.client.js +3 -1
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +234 -54
- package/src/js/fingerprint.utils.js +213 -183
- package/src/js/gpu_pow.solver.js +96 -1
- package/src/js/library.js +16 -13
- package/src/js/pow.solver.inline.js +138 -27
- package/src/js/pow.solver.js +141 -28
- package/src/js/tests/fingerprint.engine.test.js +370 -370
- package/src/js/tests/fingerprint.test.js +87 -3
- package/src/js/tests/gpu_pow.test.js +81 -0
- package/src/js/tests/pow.solver.test.js +4 -4
- package/src/php/Challenge/ChallengeUtils.php +116 -27
- package/src/php/FingerprintEngine.php +103 -2
|
@@ -749,7 +749,7 @@ describe('Fingerprint & PoW Security Suite', () => {
|
|
|
749
749
|
pow_type: 'cpu_mem',
|
|
750
750
|
pow_nonce: nonce,
|
|
751
751
|
pow_solution_cpu: String(cpuSolution),
|
|
752
|
-
pow_solution_mem:
|
|
752
|
+
pow_solution_mem: JSON.stringify(memSolution),
|
|
753
753
|
pow_fp: solverFingerprint
|
|
754
754
|
},
|
|
755
755
|
headers: { 'user-agent': userAgent, 'x-device-fingerprint': solverFingerprint },
|
|
@@ -843,7 +843,7 @@ describe('Fingerprint & PoW Security Suite', () => {
|
|
|
843
843
|
pow_type: 'cpu_mem',
|
|
844
844
|
pow_nonce: nonce,
|
|
845
845
|
pow_solution_cpu: String(cpuSolution),
|
|
846
|
-
pow_solution_mem:
|
|
846
|
+
pow_solution_mem: JSON.stringify(memSolution),
|
|
847
847
|
pow_fp: solverFingerprint // The client submits its fingerprint.
|
|
848
848
|
},
|
|
849
849
|
headers: { 'user-agent': userAgent, 'x-device-fingerprint': solverFingerprint }, rawHeaders: ['User-Agent', userAgent], httpVersion: '1.1'
|
|
@@ -2122,7 +2122,7 @@ describe('Challenge Page Generation Security (XSS)', () => {
|
|
|
2122
2122
|
});
|
|
2123
2123
|
|
|
2124
2124
|
it('should escape the path parameter in generateCombinedPoWChallengePage to prevent XSS', () => {
|
|
2125
|
-
const maliciousPath = `test.com";\nconsole.log("pwned");//`;
|
|
2125
|
+
const maliciousPath = `test.com";\nconsole.log("pwned");//`; // Original malicious path
|
|
2126
2126
|
const challengeDetails = { nonce: 'test-nonce', target: '0000', path: maliciousPath };
|
|
2127
2127
|
|
|
2128
2128
|
const html = generateCombinedPoWChallengePage(challengeDetails, 16, '127.0.0.1', 'secret', {}, '');
|
|
@@ -2721,3 +2721,87 @@ describe('Botnet Cluster Scoring (Node.js)', () => {
|
|
|
2721
2721
|
const isDiffIpValid = await fingerprint.isTicketValid('192.168.1.1', ticket, 'device-123', 'hash-abc', false);
|
|
2722
2722
|
expect(isDiffIpValid).toBe(false);
|
|
2723
2723
|
});
|
|
2724
|
+
|
|
2725
|
+
describe('IP Registration and Filtering', () => {
|
|
2726
|
+
const localInMemoryStore = {
|
|
2727
|
+
_map: new Map(),
|
|
2728
|
+
async get(key) { return this._map.get(key); },
|
|
2729
|
+
async set(key, value) { this._map.set(key, value); },
|
|
2730
|
+
async has(key) { return this._map.has(key); },
|
|
2731
|
+
async delete(key) { this._map.delete(key); },
|
|
2732
|
+
clear() { this._map.clear(); }
|
|
2733
|
+
};
|
|
2734
|
+
|
|
2735
|
+
beforeEach(() => {
|
|
2736
|
+
localInMemoryStore.clear();
|
|
2737
|
+
configureStore(localInMemoryStore);
|
|
2738
|
+
vi.restoreAllMocks();
|
|
2739
|
+
vi.spyOn(fingerprint.__internal, 'getTlsFingerprint').mockReturnValue({
|
|
2740
|
+
ja3: 'mock-ja3', ja4: 'mock-ja4'
|
|
2741
|
+
});
|
|
2742
|
+
});
|
|
2743
|
+
|
|
2744
|
+
it('should immediately allow requests from IPs in the static allowlist', async () => {
|
|
2745
|
+
const whitelistedIp = '198.51.100.42';
|
|
2746
|
+
const config = {
|
|
2747
|
+
weights: { historyScore: 1.0 },
|
|
2748
|
+
thresholds: { low: 20, block: 95 },
|
|
2749
|
+
whitelist: [
|
|
2750
|
+
{ type: 'allowlist', entries: [whitelistedIp] }
|
|
2751
|
+
]
|
|
2752
|
+
};
|
|
2753
|
+
const engine = new FingerprintEngine(config);
|
|
2754
|
+
|
|
2755
|
+
const req = {
|
|
2756
|
+
clientIp: whitelistedIp,
|
|
2757
|
+
path: '/',
|
|
2758
|
+
cookies: {},
|
|
2759
|
+
query: {},
|
|
2760
|
+
headers: { 'user-agent': 'test-ua' },
|
|
2761
|
+
rawHeaders: ['User-Agent', 'test-ua'],
|
|
2762
|
+
httpVersion: '1.1'
|
|
2763
|
+
};
|
|
2764
|
+
|
|
2765
|
+
const decision = await engine.processRequest(req);
|
|
2766
|
+
expect(decision.action).toBe('next');
|
|
2767
|
+
expect(decision.vector.whitelisted).toBe(100);
|
|
2768
|
+
expect(decision.vector.type).toBe('allowlist');
|
|
2769
|
+
});
|
|
2770
|
+
|
|
2771
|
+
it('should register distinct client IPs for a device in the data store', async () => {
|
|
2772
|
+
const ip1 = '192.168.1.100';
|
|
2773
|
+
const ip2 = '192.168.1.101';
|
|
2774
|
+
const deviceId = 'test-ip-reg-device';
|
|
2775
|
+
|
|
2776
|
+
await localInMemoryStore.set(`device:${deviceId}`, {
|
|
2777
|
+
initialDeviceHash: 'some-hash',
|
|
2778
|
+
ips: new Set([ip1]),
|
|
2779
|
+
lastUpdate: Date.now(),
|
|
2780
|
+
lastFpHash: 'some-hash',
|
|
2781
|
+
lastChangeTimestamp: 0,
|
|
2782
|
+
rapidChangeCount: 0,
|
|
2783
|
+
});
|
|
2784
|
+
|
|
2785
|
+
const req = {
|
|
2786
|
+
clientIp: ip2,
|
|
2787
|
+
path: '/',
|
|
2788
|
+
cookies: { device_id: deviceId },
|
|
2789
|
+
query: {},
|
|
2790
|
+
headers: { 'user-agent': 'test-ua' },
|
|
2791
|
+
rawHeaders: ['User-Agent', 'test-ua'],
|
|
2792
|
+
httpVersion: '1.1'
|
|
2793
|
+
};
|
|
2794
|
+
|
|
2795
|
+
const engine = new FingerprintEngine({
|
|
2796
|
+
weights: { historyScore: 1.0 },
|
|
2797
|
+
thresholds: { low: 20, block: 95 }
|
|
2798
|
+
});
|
|
2799
|
+
|
|
2800
|
+
await engine.processRequest(req);
|
|
2801
|
+
|
|
2802
|
+
const storedData = await localInMemoryStore.get(`device:${deviceId}`);
|
|
2803
|
+
expect(storedData.ips).toBeInstanceOf(Set);
|
|
2804
|
+
expect(storedData.ips.has(ip1)).toBe(true);
|
|
2805
|
+
expect(storedData.ips.has(ip2)).toBe(true);
|
|
2806
|
+
});
|
|
2807
|
+
});
|
|
@@ -115,4 +115,85 @@ describe('GpuPowSolver - Chaotic Logistic Map PoW', () => {
|
|
|
115
115
|
expect(result.solution.split(',').length).toBe(64);
|
|
116
116
|
vi.restoreAllMocks();
|
|
117
117
|
});
|
|
118
|
+
|
|
119
|
+
it('should fallback to WebGL1 if both WebGPU and WebGL2 are not supported', async () => {
|
|
120
|
+
// Mock WebGPU as undefined
|
|
121
|
+
Object.defineProperty(global.navigator, 'gpu', {
|
|
122
|
+
value: undefined,
|
|
123
|
+
writable: true,
|
|
124
|
+
configurable: true
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Mock WebGL1 (webgl) context with float extensions enabled
|
|
128
|
+
const mockGl1 = {
|
|
129
|
+
FRAMEBUFFER: 36160,
|
|
130
|
+
FRAMEBUFFER_COMPLETE: 36053,
|
|
131
|
+
COLOR_ATTACHMENT0: 36064,
|
|
132
|
+
TEXTURE_2D: 3553,
|
|
133
|
+
RGBA: 6408,
|
|
134
|
+
FLOAT: 5126,
|
|
135
|
+
TRIANGLES: 4,
|
|
136
|
+
CLAMP_TO_EDGE: 33071,
|
|
137
|
+
TEXTURE_MAG_FILTER: 10240,
|
|
138
|
+
TEXTURE_MIN_FILTER: 10241,
|
|
139
|
+
NEAREST: 9728,
|
|
140
|
+
TEXTURE_WRAP_S: 10242,
|
|
141
|
+
TEXTURE_WRAP_T: 10243,
|
|
142
|
+
useProgram: vi.fn(),
|
|
143
|
+
createBuffer: vi.fn(),
|
|
144
|
+
bindBuffer: vi.fn(),
|
|
145
|
+
bufferData: vi.fn(),
|
|
146
|
+
enableVertexAttribArray: vi.fn(),
|
|
147
|
+
vertexAttribPointer: vi.fn(),
|
|
148
|
+
uniform1f: vi.fn(),
|
|
149
|
+
uniform1i: vi.fn(),
|
|
150
|
+
getUniformLocation: vi.fn(),
|
|
151
|
+
getAttribLocation: vi.fn(),
|
|
152
|
+
drawArrays: vi.fn(),
|
|
153
|
+
getExtension: vi.fn().mockImplementation((ext) => {
|
|
154
|
+
if (ext === 'OES_texture_float' || ext === 'WEBGL_color_buffer_float') {
|
|
155
|
+
return {};
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}),
|
|
159
|
+
createTexture: vi.fn(),
|
|
160
|
+
bindTexture: vi.fn(),
|
|
161
|
+
texImage2D: vi.fn(),
|
|
162
|
+
texParameteri: vi.fn(),
|
|
163
|
+
createFramebuffer: vi.fn(),
|
|
164
|
+
bindFramebuffer: vi.fn(),
|
|
165
|
+
framebufferTexture2D: vi.fn(),
|
|
166
|
+
checkFramebufferStatus: vi.fn().mockReturnValue(36053), // gl.FRAMEBUFFER_COMPLETE
|
|
167
|
+
viewport: vi.fn(),
|
|
168
|
+
readPixels: vi.fn((x, y, w, h, format, type, pixels) => {
|
|
169
|
+
for (let i = 0; i < pixels.length; i++) {
|
|
170
|
+
pixels[i] = 0.75;
|
|
171
|
+
}
|
|
172
|
+
})
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const mockCanvas = {
|
|
176
|
+
width: 8,
|
|
177
|
+
height: 8,
|
|
178
|
+
getContext: vi.fn().mockImplementation((contextId) => {
|
|
179
|
+
if (contextId === 'webgl2') return null;
|
|
180
|
+
if (contextId === 'webgl' || contextId === 'experimental-webgl') return mockGl1;
|
|
181
|
+
return null;
|
|
182
|
+
})
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
vi.spyOn(document, 'createElement').mockImplementation((tagName) => {
|
|
186
|
+
if (tagName === 'canvas') return mockCanvas;
|
|
187
|
+
return {};
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
vi.spyOn(GpuPowSolver, '_createProgram').mockReturnValue({});
|
|
191
|
+
|
|
192
|
+
const result = await GpuPowSolver.solve(seed, 100);
|
|
193
|
+
|
|
194
|
+
expect(result.platform).toBe('webgl1');
|
|
195
|
+
expect(result.solution.split(',').length).toBe(64);
|
|
196
|
+
expect(result.solution.split(',')[0]).toBe('0.750000');
|
|
197
|
+
vi.restoreAllMocks();
|
|
198
|
+
});
|
|
118
199
|
});
|
|
@@ -63,8 +63,8 @@ describe('Proof-of-Work Solvers', () => {
|
|
|
63
63
|
const solution1 = await solveMemory(seed, difficulty);
|
|
64
64
|
const solution2 = await solveMemory(seed, difficulty);
|
|
65
65
|
|
|
66
|
-
expect(solution1).
|
|
67
|
-
expect(solution1).not.
|
|
66
|
+
expect(solution1).toStrictEqual(solution2);
|
|
67
|
+
expect(solution1).not.toStrictEqual(await solveMemory('different-seed', difficulty));
|
|
68
68
|
});
|
|
69
69
|
});
|
|
70
70
|
|
|
@@ -106,7 +106,7 @@ describe('Proof-of-Work Solvers', () => {
|
|
|
106
106
|
|
|
107
107
|
const solutions = await solveChallenge(challenge, ''); // Le fingerprint est passé en 2e arg
|
|
108
108
|
expect(solutions.rawSolution).toHaveProperty('cpu', expect.any(Number));
|
|
109
|
-
expect(solutions.rawSolution).toHaveProperty('mem', expect.any(
|
|
109
|
+
expect(solutions.rawSolution).toHaveProperty('mem', expect.any(Object));
|
|
110
110
|
}, 20000);
|
|
111
111
|
|
|
112
112
|
it('should solve a "cpu_mem_inline" challenge for a browser', async () => {
|
|
@@ -122,7 +122,7 @@ describe('Proof-of-Work Solvers', () => {
|
|
|
122
122
|
|
|
123
123
|
const solutions = await solveChallenge(challenge, '');
|
|
124
124
|
expect(solutions.rawSolution).toHaveProperty('cpu', expect.any(Number));
|
|
125
|
-
expect(solutions.rawSolution).toHaveProperty('mem', expect.any(
|
|
125
|
+
expect(solutions.rawSolution).toHaveProperty('mem', expect.any(Object));
|
|
126
126
|
}, 20000);
|
|
127
127
|
|
|
128
128
|
it('should solve a "tsp" challenge', async () => {
|
|
@@ -638,6 +638,55 @@ class ChallengeUtils
|
|
|
638
638
|
/**
|
|
639
639
|
* Vérifie une solution de PoW mémoire.
|
|
640
640
|
*/
|
|
641
|
+
private static function getChallengedIndices(string $seed, int $solution, int $numBlocks, int $k = 4): array
|
|
642
|
+
{
|
|
643
|
+
$indices = [];
|
|
644
|
+
$h = (int)bcmod(\Anonympins\Fingerprint\FingerprintBuilder::cyrb53($seed . ":" . $solution), '4294967296');
|
|
645
|
+
for ($i = 0; $i < $k; $i++) {
|
|
646
|
+
$h = self::gmp_imul($h ^ $i, 1597334677);
|
|
647
|
+
$indices[] = abs($h) % $numBlocks;
|
|
648
|
+
}
|
|
649
|
+
return $indices;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
private static function verifyMerkleProof(string $leafHash, int $index, array $proof, string $root): bool
|
|
653
|
+
{
|
|
654
|
+
$currentHash = $leafHash;
|
|
655
|
+
$idx = $index;
|
|
656
|
+
foreach ($proof as $sibling) {
|
|
657
|
+
$combined = ($idx % 2 === 0) ? $currentHash . $sibling : $sibling . $currentHash;
|
|
658
|
+
$currentHash = hash('sha256', hex2bin($combined));
|
|
659
|
+
$idx = (int)floor($idx / 2);
|
|
660
|
+
}
|
|
661
|
+
return $currentHash === $root;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
private static function verifyMemoryPoWLegacy(string $nonce, int $solution, int $difficulty, string $clientSecret): bool
|
|
665
|
+
{
|
|
666
|
+
$size = $difficulty * 1024 * 1024;
|
|
667
|
+
$iterations = (int)floor($size / 16);
|
|
668
|
+
$buffer = new \SplFixedArray((int)floor($size / 4));
|
|
669
|
+
|
|
670
|
+
$seed = ":{$nonce}:{$clientSecret}";
|
|
671
|
+
$h = 0;
|
|
672
|
+
foreach (unpack('C*', $seed) as $byte) {
|
|
673
|
+
$h += $byte;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
for ($i = 0; $i < count($buffer); $i++) {
|
|
677
|
+
$buffer[$i] = $h = self::gmp_imul($h ^ $i, 1597334677);
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
$finalHash = 0;
|
|
681
|
+
$addr = count($buffer) > 0 ? $buffer[0] % count($buffer) : 0;
|
|
682
|
+
for ($i = 0; $i < $iterations; $i++) {
|
|
683
|
+
$addr = $buffer[$addr] % count($buffer);
|
|
684
|
+
$finalHash ^= $addr;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
return $finalHash === $solution;
|
|
688
|
+
}
|
|
689
|
+
|
|
641
690
|
public static function verifyMemoryPoW(
|
|
642
691
|
string $nonce,
|
|
643
692
|
string $solution,
|
|
@@ -654,37 +703,69 @@ class ChallengeUtils
|
|
|
654
703
|
return false;
|
|
655
704
|
}
|
|
656
705
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
706
|
+
$data = json_decode($solution, true);
|
|
707
|
+
if (json_last_error() !== JSON_ERROR_NONE || !isset($data['solution']) || !isset($data['merkleRoot']) || !isset($data['proofs'])) {
|
|
708
|
+
if ($difficulty <= 4 && preg_match('/^\d+$/', $solution)) {
|
|
709
|
+
return self::verifyMemoryPoWLegacy($nonce, (int)$solution, $difficulty, $clientSecret);
|
|
710
|
+
}
|
|
711
|
+
return false;
|
|
661
712
|
}
|
|
662
713
|
|
|
663
|
-
$
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
}
|
|
667
|
-
$iterations = (int)floor($size / 16);
|
|
668
|
-
$buffer = new \SplFixedArray((int)floor($size / 4));
|
|
714
|
+
$sol = $data['solution'];
|
|
715
|
+
$merkleRoot = $data['merkleRoot'];
|
|
716
|
+
$proofs = $data['proofs'];
|
|
669
717
|
|
|
718
|
+
$numBlocks = $difficulty * 256;
|
|
670
719
|
$seed = ":{$nonce}:{$clientSecret}";
|
|
671
|
-
$h = 0;
|
|
672
|
-
foreach (unpack('C*', $seed) as $byte) {
|
|
673
|
-
$h += $byte;
|
|
674
|
-
}
|
|
675
720
|
|
|
676
|
-
|
|
677
|
-
|
|
721
|
+
$challengedIndices = self::getChallengedIndices($seed, (int)$sol, $numBlocks, 4);
|
|
722
|
+
|
|
723
|
+
foreach ($challengedIndices as $b) {
|
|
724
|
+
$proof = $proofs[$b] ?? $proofs[(string)$b] ?? null;
|
|
725
|
+
if ($proof === null) return false;
|
|
726
|
+
|
|
727
|
+
$blockBytes = '';
|
|
728
|
+
$h = (int)bcmod(\Anonympins\Fingerprint\FingerprintBuilder::cyrb53($seed . ":" . $b), '4294967296');
|
|
729
|
+
for ($i = 0; $i < 1024; $i++) {
|
|
730
|
+
$h = self::gmp_imul($h ^ $i, 1597334677);
|
|
731
|
+
$blockBytes .= pack('V', $h);
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
$expectedLeaf = hash('sha256', $blockBytes);
|
|
735
|
+
|
|
736
|
+
if (!self::verifyMerkleProof($expectedLeaf, $b, $proof, $merkleRoot)) {
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
678
739
|
}
|
|
679
740
|
|
|
680
|
-
$
|
|
681
|
-
$
|
|
741
|
+
$blockCache = [];
|
|
742
|
+
$getBlockElement = function (int $blockIdx, int $elementIdx) use (&$blockCache, $seed): int {
|
|
743
|
+
if (!isset($blockCache[$blockIdx])) {
|
|
744
|
+
$block = [];
|
|
745
|
+
$h = (int)bcmod(\Anonympins\Fingerprint\FingerprintBuilder::cyrb53($seed . ":" . $blockIdx), '4294967296');
|
|
746
|
+
for ($i = 0; $i < 1024; $i++) {
|
|
747
|
+
$h = self::gmp_imul($h ^ $i, 1597334677);
|
|
748
|
+
$block[$i] = $h;
|
|
749
|
+
}
|
|
750
|
+
$blockCache[$blockIdx] = $block;
|
|
751
|
+
}
|
|
752
|
+
return $blockCache[$blockIdx][$elementIdx];
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
$totalElements = $numBlocks * 1024;
|
|
756
|
+
$addr = $totalElements > 0 ? $getBlockElement(0, 0) % $totalElements : 0;
|
|
757
|
+
$addr = $addr & 0xffffffff;
|
|
758
|
+
$expectedSolution = 0;
|
|
759
|
+
$iterations = 1024;
|
|
682
760
|
for ($i = 0; $i < $iterations; $i++) {
|
|
683
|
-
$
|
|
684
|
-
$
|
|
761
|
+
$blockIdx = (int)floor($addr / 1024);
|
|
762
|
+
$elementIdx = $addr % 1024;
|
|
763
|
+
$addr = $getBlockElement($blockIdx, $elementIdx) % $totalElements;
|
|
764
|
+
$addr = $addr & 0xffffffff;
|
|
765
|
+
$expectedSolution ^= $addr;
|
|
685
766
|
}
|
|
686
767
|
|
|
687
|
-
return $
|
|
768
|
+
return $expectedSolution === (int)$sol;
|
|
688
769
|
}
|
|
689
770
|
|
|
690
771
|
/**
|
|
@@ -759,11 +840,15 @@ class ChallengeUtils
|
|
|
759
840
|
$solverCode = self::getPowSolverCode();
|
|
760
841
|
$queriesJson = json_encode($queries);
|
|
761
842
|
|
|
843
|
+
$safePath = json_encode($path, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
|
|
844
|
+
$safeNonce = json_encode($nonce, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
|
|
845
|
+
$safeClientSecret = json_encode($clientSecret, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
|
|
846
|
+
|
|
762
847
|
$challengeScript = <<<JS
|
|
763
848
|
async function solve() {
|
|
764
|
-
const nonce =
|
|
765
|
-
const path =
|
|
766
|
-
const clientSecret =
|
|
849
|
+
const nonce = {$safeNonce};
|
|
850
|
+
const path = {$safePath};
|
|
851
|
+
const clientSecret = {$safeClientSecret};
|
|
767
852
|
const queries = {$queriesJson};
|
|
768
853
|
const sizeMb = {$sizeMb};
|
|
769
854
|
const nodeId = "{$nodeId}";
|
|
@@ -866,11 +951,15 @@ JS;
|
|
|
866
951
|
));
|
|
867
952
|
$trapContainerHtml = "<div style=\"position:absolute;left:-9999px;top:-9999px;transform:scale(0);pointer-events:none;\" aria-hidden=\"true\">{$trapLinksHtml}</div>";
|
|
868
953
|
|
|
954
|
+
$safePath = json_encode($path, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
|
|
955
|
+
$safeNonce = json_encode($nonce, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
|
|
956
|
+
$safeClientSecret = json_encode($clientSecret, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
|
|
957
|
+
|
|
869
958
|
$challengeScript = <<<JS
|
|
870
959
|
async function solve() {
|
|
871
|
-
const nonce =
|
|
872
|
-
const path =
|
|
873
|
-
const clientSecret =
|
|
960
|
+
const nonce = {$safeNonce};
|
|
961
|
+
const path = {$safePath};
|
|
962
|
+
const clientSecret = {$safeClientSecret};
|
|
874
963
|
const cpuTarget = BigInt("0x" + "{$target}");
|
|
875
964
|
const memDifficulty = {$memoryDifficulty};
|
|
876
965
|
const baseBlock = new Uint8Array({$baseBlockBytes});
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
private ?Logger $logger = null;
|
|
28
28
|
private bool $dryRun;
|
|
29
29
|
|
|
30
|
+
private static ?array $googlebotEntries = null;
|
|
31
|
+
private static ?array $yandexEntries = null;
|
|
32
|
+
private static ?array $bingbotEntries = null;
|
|
33
|
+
|
|
30
34
|
public function __construct(array $securityConfig)
|
|
31
35
|
{
|
|
32
36
|
$this->isProduction = ($_ENV['APP_ENV'] ?? getenv('APP_ENV')) === 'production';
|
|
@@ -103,7 +107,103 @@
|
|
|
103
107
|
}
|
|
104
108
|
}
|
|
105
109
|
}
|
|
110
|
+
private static function loadBotWhitelist(string $filename, array $fallbackEntries): array
|
|
111
|
+
{
|
|
112
|
+
$configDir = dirname(__DIR__, 2) . '/config';
|
|
113
|
+
$filePath = $configDir . '/' . $filename;
|
|
114
|
+
if (file_exists($filePath)) {
|
|
115
|
+
try {
|
|
116
|
+
$content = file_get_contents($filePath);
|
|
117
|
+
if ($content !== false) {
|
|
118
|
+
$decoded = json_decode($content, true);
|
|
119
|
+
if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
|
|
120
|
+
return $decoded;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} catch (\Throwable $e) {
|
|
124
|
+
error_log("[Fingerprint] Error loading whitelist file {$filename}: " . $e->getMessage());
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return $fallbackEntries;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public static function googlebot_whitelist(): array
|
|
131
|
+
{
|
|
132
|
+
if (self::$googlebotEntries === null) {
|
|
133
|
+
self::$googlebotEntries = self::loadBotWhitelist('googlebot.json', [
|
|
134
|
+
"2001:4860:4801:10::/64",
|
|
135
|
+
"2001:4860:4801:11::/64",
|
|
136
|
+
"2001:4860:4801:12::/64",
|
|
137
|
+
"66.249.79.64"
|
|
138
|
+
]);
|
|
139
|
+
}
|
|
140
|
+
return [
|
|
141
|
+
'type' => 'allowlist',
|
|
142
|
+
'entries' => self::$googlebotEntries
|
|
143
|
+
];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public static function yandex_whitelist(): array
|
|
147
|
+
{
|
|
148
|
+
if (self::$yandexEntries === null) {
|
|
149
|
+
self::$yandexEntries = self::loadBotWhitelist('yandex.json', [
|
|
150
|
+
"2a02:6b8::/29",
|
|
151
|
+
"5.45.192.0/18",
|
|
152
|
+
"213.180.192.0/19"
|
|
153
|
+
]);
|
|
154
|
+
}
|
|
155
|
+
return [
|
|
156
|
+
'type' => 'allowlist',
|
|
157
|
+
'entries' => self::$yandexEntries
|
|
158
|
+
];
|
|
159
|
+
}
|
|
106
160
|
|
|
161
|
+
public static function bingbot_whitelist(): array
|
|
162
|
+
{
|
|
163
|
+
if (self::$bingbotEntries === null) {
|
|
164
|
+
self::$bingbotEntries = self::loadBotWhitelist('bingbot.json', [
|
|
165
|
+
"157.55.39.0/24",
|
|
166
|
+
"207.46.13.0/24",
|
|
167
|
+
"40.77.178.0/23"
|
|
168
|
+
]);
|
|
169
|
+
}
|
|
170
|
+
return [
|
|
171
|
+
'type' => 'allowlist',
|
|
172
|
+
'entries' => self::$bingbotEntries
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
public static function default_whitelist(): array
|
|
177
|
+
{
|
|
178
|
+
return [
|
|
179
|
+
self::googlebot_whitelist(),
|
|
180
|
+
self::bingbot_whitelist(),
|
|
181
|
+
self::yandex_whitelist(),
|
|
182
|
+
['userAgent' => 'Googlebot', 'hostnameSuffix' => '.googlebot.com'],
|
|
183
|
+
['userAgent' => 'Google-Extended', 'hostnameSuffix' => '.google.com'],
|
|
184
|
+
['userAgent' => 'AdsBot-Google', 'hostnameSuffix' => '.googlebot.com'],
|
|
185
|
+
['userAgent' => 'Mediapartners-Google', 'hostnameSuffix' => '.google.com'],
|
|
186
|
+
['userAgent' => 'Google-InspectionTool', 'hostnameSuffix' => '.google.com'],
|
|
187
|
+
['userAgent' => '(bingbot|adidxbot)', 'hostnameSuffix' => '.search.msn.com'],
|
|
188
|
+
['userAgent' => 'DuckDuckBot', 'hostnameSuffix' => '.duckduckgo.com'],
|
|
189
|
+
['userAgent' => 'YandexBot', 'hostnameSuffix' => '.yandex.com'],
|
|
190
|
+
['userAgent' => 'YandexImages', 'hostnameSuffix' => '.yandex.com'],
|
|
191
|
+
['userAgent' => 'Baiduspider', 'hostnameSuffix' => '.crawl.baidu.com'],
|
|
192
|
+
['userAgent' => 'Slurp', 'hostnameSuffix' => '.crawl.yahoo.net'],
|
|
193
|
+
['userAgent' => 'Sogou web spider', 'hostnameSuffix' => '.sogou.com'],
|
|
194
|
+
['userAgent' => 'Exabot', 'hostnameSuffix' => '.exabot.com'],
|
|
195
|
+
['userAgent' => 'ia_archiver', 'hostnameSuffix' => '.alexa.com'],
|
|
196
|
+
['userAgent' => 'SeznamBot', 'hostnameSuffix' => '.seznam.cz'],
|
|
197
|
+
['userAgent' => 'Mail.RU_Bot', 'hostnameSuffix' => '.mail.ru'],
|
|
198
|
+
['userAgent' => 'Yeti', 'hostnameSuffix' => '.naver.com'],
|
|
199
|
+
['userAgent' => 'AhrefsBot', 'hostnameSuffix' => '.ahrefs.com'],
|
|
200
|
+
['userAgent' => 'SemrushBot', 'hostnameSuffix' => '.semrush.com'],
|
|
201
|
+
['userAgent' => 'MJ12bot', 'hostnameSuffix' => '.mj12bot.com'],
|
|
202
|
+
['userAgent' => 'rogerbot', 'hostnameSuffix' => '.moz.com'],
|
|
203
|
+
['userAgent' => 'DotBot', 'hostnameSuffix' => '.moz.com'],
|
|
204
|
+
['userAgent' => 'Screaming Frog SEO Spider', 'hostnameSuffix' => '.screamingfrog.co.uk'],
|
|
205
|
+
];
|
|
206
|
+
}
|
|
107
207
|
private function log(string $message, array $data = [], string $level = 'info'): void
|
|
108
208
|
{
|
|
109
209
|
if ($this->verbose) {
|
|
@@ -1039,8 +1139,9 @@
|
|
|
1039
1139
|
'target' => ChallengeUtils::calculateCpuTarget($suspicionFactor, $this->securityConfig),
|
|
1040
1140
|
'path' => $context->path,
|
|
1041
1141
|
];
|
|
1042
|
-
|
|
1043
|
-
|
|
1142
|
+
|
|
1143
|
+
// Alignement linéaire parfait du ratio d'effort CPU/Mémoire
|
|
1144
|
+
$memActivationFactor = $suspicionFactor;
|
|
1044
1145
|
$memDifficulty = (int)round($memActivationFactor * 48); // 0 à 48MB
|
|
1045
1146
|
|
|
1046
1147
|
$originalFingerprint = RequestUtils::getCompositeDeviceHash($context);
|