@anonympins/fingerprint 0.5.4 → 0.6.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.
@@ -1,186 +1,194 @@
1
- <?php
2
-
3
- declare(strict_types=1);
4
-
5
- namespace Anonympins\Fingerprint;
6
-
7
- /**
8
- * Classe pour construire une empreinte composite (Multi-Hash).
9
- * Format de sortie : "grp1:hash1|grp2:hash2|grp3:hash3"
10
- */
11
- class FingerprintBuilder
12
- {
13
- /**
14
- * @var array<string, string|int>
15
- */
16
- private array $components = [];
17
-
18
- /**
19
- * Ajoute un composant à l'empreinte.
20
- * La valeur est hachée pour l'anonymiser et réduire sa taille.
21
- *
22
- * @param string $group Le nom du groupe (ex: 'hw', 'screen', 'geo').
23
- * @param string|int|bool|null $value La valeur brute à hacher.
24
- * @return self
25
- */
26
- public function add(string $group, $value): self
27
- {
28
- if ($value === null || $value === '') {
29
- return $this;
30
- }
31
- // On hache la valeur individuellement.
32
- $this->components[$group] = self::cyrb53((string)$value);
33
- return $this;
34
- }
35
-
36
- /**
37
- * Ajoute un composant brut sans le hacher.
38
- * Utile pour les métriques qui doivent être lues telles quelles par le serveur.
39
- *
40
- * @param string $group Le nom du groupe.
41
- * @param string|int|null $value La valeur brute.
42
- * @return self
43
- */
44
- public function addRaw(string $group, $value): self
45
- {
46
- if ($value === null) {
47
- return $this;
48
- }
49
- $this->components[$group] = $value;
50
- return $this;
51
- }
52
-
53
- /**
54
- * Génère la chaîne de l'empreinte finale.
55
- * Les composants sont triés par clé pour garantir un ordre déterministe.
56
- *
57
- * @return string
58
- */
59
- public function __toString(): string
60
- {
61
- // ksort trie le tableau par clé.
62
- ksort($this->components);
63
-
64
- $parts = [];
65
- foreach ($this->components as $key => $hash) {
66
- $parts[] = "{$key}:{$hash}";
67
- }
68
-
69
- return implode('|', $parts);
70
- }
71
-
72
- /**
73
- * Compare deux empreintes et retourne un score de similarité (de 0 à 1).
74
- * Utilise une pondération pour donner plus d'importance aux invariants forts (Canvas, GPU, JA3).
75
- *
76
- * @param string|null $fpString1 Empreinte A.
77
- * @param string|null $fpString2 Empreinte B.
78
- * @return float
79
- */
80
- public static function compare(?string $fpString1, ?string $fpString2): float
81
- {
82
- if (empty($fpString1) || empty($fpString2)) {
83
- return 0.0;
84
- }
85
-
86
- $parse = function (string $str): array {
87
- $map = [];
88
- foreach (explode('|', $str) as $part) {
89
- $pair = explode(':', $part, 2);
90
- if (count($pair) === 2 && !empty($pair[0]) && !empty($pair[1])) {
91
- $map[$pair[0]] = $pair[1];
92
- }
93
- }
94
- return $map;
95
- };
96
-
97
- $map1 = $parse($fpString1);
98
- $map2 = $parse($fpString2);
99
-
100
- $volatileKeys = [
101
- 'ch_ua', 'ch_platform', 'ch_mobile', 'ch_model', 'ch_arch', 'ch_bitness',
102
- 'cookie_keys', 'upgrade', 'network', 'http_ver',
103
- 'x_forwarded_for', 'x_real_ip', 'cf_connecting_ip'
104
- ];
105
-
106
- $weights = [
107
- 'cvs' => 5.0, 'gpu' => 4.0, 'ja3' => 3.5, 'ja4' => 4.0, 'ja4s' => 4.0, 'ja4h' => 3.8,
108
- 'h2_settings' => 3.0, 'tcp_fp' => 2.5, 'ua' => 2.0,
109
- 'client_fp_hash' => 3.0, 'browser' => 1.5, 'os_version' => 1.5,
110
- 'device_type' => 1.0, 'hw' => 1.5, 'scr' => 1.0, 'os' => 0.8, 'geo' => 0.5,
111
- ];
112
-
113
- $weightedMatches = 0.0;
114
- $totalWeight = 0.0;
115
-
116
- $allKeys = array_unique(array_merge(array_keys($map1), array_keys($map2)));
117
-
118
- foreach ($allKeys as $key) {
119
- // On ignore les clés volatiles pour cette comparaison spécifique.
120
- if (in_array($key, $volatileKeys, true)) { // @phpstan-ignore-line
121
- continue;
122
- }
123
-
124
- // On ne compare que les clés qui ont un poids défini.
125
- $weight = $weights[$key] ?? null;
126
- if ($weight === null) continue;
127
-
128
- $totalWeight += $weight;
129
- if (isset($map1[$key]) && isset($map2[$key])) {
130
- if ($map1[$key] === $map2[$key]) {
131
- $weightedMatches += $weight;
132
- }
133
- }
134
- }
135
-
136
- return $totalWeight === 0.0 ? 0.0 : $weightedMatches / $totalWeight;
137
- }
138
-
139
- /**
140
- * Algorithme de hachage cyrb53 (rapide et faible taux de collision).
141
- * Porté depuis la version JavaScript.
142
- *
143
- * @param string $str La chaîne à hacher.
144
- * @param int $seed Une graine optionnelle.
145
- * @return string Le hash sous forme de chaîne de caractères.
146
- */
147
- public static function cyrb53(string $str, int $seed = 0): string
148
- {
149
- $h1 = 0xdeadbeef ^ $seed;
150
- $h2 = 0x41c6ce57 ^ $seed;
151
-
152
- for ($i = 0, $l = strlen($str); $i < $l; $i++) {
153
- $ch = ord($str[$i]);
154
- $h1 = self::imul($h1 ^ $ch, 2654435761);
155
- $h2 = self::imul($h2 ^ $ch, 1597334677);
156
- }
157
-
158
- $h1 = self::imul($h1 ^ ($h1 >> 16), 2246822507) ^ self::imul($h2 ^ ($h2 >> 13), 3266489909);
159
- $h2 = self::imul($h2 ^ ($h2 >> 16), 2246822507) ^ self::imul($h1 ^ ($h1 >> 13), 3266489909);
160
-
161
- // En PHP, les opérations sur les grands nombres peuvent être délicates.
162
- // On utilise bcmath pour une arithmétique de précision arbitraire, garantissant le même résultat que JS.
163
- $val_h2 = bcadd(bcmul((string)(2097151 & $h2), '4294967296'), (string)($h1 >= 0 ? $h1 : $h1 + 4294967296));
164
- return $val_h2;
165
- }
166
-
167
- /**
168
- * Émule la multiplication 32-bit `Math.imul` de JavaScript.
169
- *
170
- * @param int $a
171
- * @param int $b
172
- * @return int Un entier signé 32-bit.
173
- */
174
- private static function imul(int $a, int $b): int
175
- {
176
- // Emulation of JavaScript's Math.imul for signed 32-bit integer multiplication.
177
- // This version correctly handles overflows on 64-bit systems.
178
- $ah = ($a >> 16) & 0xffff;
179
- $al = $a & 0xffff;
180
- $bh = ($b >> 16) & 0xffff;
181
- $bl = $b & 0xffff;
182
- $lo = $al * $bl;
183
- $hi = (($lo >> 16) + ($al * $bh) + ($ah * $bl)) & 0xffff;
184
- return (($hi << 16) | ($lo & 0xffff)) | 0;
185
- }
1
+ <?php
2
+
3
+ declare(strict_types=1);
4
+
5
+ namespace Anonympins\Fingerprint;
6
+
7
+ /**
8
+ * Classe pour construire une empreinte composite (Multi-Hash).
9
+ * Format de sortie : "grp1:hash1|grp2:hash2|grp3:hash3"
10
+ */
11
+ class FingerprintBuilder
12
+ {
13
+ /**
14
+ * @var array<string, string|int>
15
+ */
16
+ private array $components = [];
17
+
18
+ /**
19
+ * Ajoute un composant à l'empreinte.
20
+ * La valeur est hachée pour l'anonymiser et réduire sa taille.
21
+ *
22
+ * @param string $group Le nom du groupe (ex: 'hw', 'screen', 'geo').
23
+ * @param string|int|bool|null $value La valeur brute à hacher.
24
+ * @return self
25
+ */
26
+ public function add(string $group, $value): self
27
+ {
28
+ if ($value === null || $value === '') {
29
+ return $this;
30
+ }
31
+ // On hache la valeur individuellement.
32
+ $this->components[$group] = self::cyrb53((string)$value);
33
+ return $this;
34
+ }
35
+
36
+ /**
37
+ * Ajoute un composant brut sans le hacher.
38
+ * Utile pour les métriques qui doivent être lues telles quelles par le serveur.
39
+ *
40
+ * @param string $group Le nom du groupe.
41
+ * @param string|int|null $value La valeur brute.
42
+ * @return self
43
+ */
44
+ public function addRaw(string $group, $value): self
45
+ {
46
+ if ($value === null) {
47
+ return $this;
48
+ }
49
+ $this->components[$group] = $value;
50
+ return $this;
51
+ }
52
+
53
+ /**
54
+ * Génère la chaîne de l'empreinte finale.
55
+ * Les composants sont triés par clé pour garantir un ordre déterministe.
56
+ *
57
+ * @return string
58
+ */
59
+ public function __toString(): string
60
+ {
61
+ // ksort trie le tableau par clé.
62
+ ksort($this->components);
63
+
64
+ $parts = [];
65
+ foreach ($this->components as $key => $hash) {
66
+ $parts[] = "{$key}:{$hash}";
67
+ }
68
+
69
+ return implode('|', $parts);
70
+ }
71
+
72
+ /**
73
+ * Compare deux empreintes et retourne un score de similarité (de 0 à 1).
74
+ * Utilise une pondération pour donner plus d'importance aux invariants forts (Canvas, GPU, JA3).
75
+ *
76
+ * @param string|null $fpString1 Empreinte A.
77
+ * @param string|null $fpString2 Empreinte B.
78
+ * @return float
79
+ */
80
+ public static function compare(?string $fpString1, ?string $fpString2): float
81
+ {
82
+ if (empty($fpString1) || empty($fpString2)) {
83
+ return 0.0;
84
+ }
85
+
86
+ $parse = function (string $str): array {
87
+ $map = [];
88
+ foreach (explode('|', $str) as $part) {
89
+ $pair = explode(':', $part, 2);
90
+ if (count($pair) === 2 && !empty($pair[0]) && !empty($pair[1])) {
91
+ $map[$pair[0]] = $pair[1];
92
+ }
93
+ }
94
+ return $map;
95
+ };
96
+
97
+ $map1 = $parse($fpString1);
98
+ $map2 = $parse($fpString2);
99
+
100
+ $volatileKeys = [
101
+ 'ch_ua', 'ch_platform', 'ch_mobile', 'ch_model', 'ch_arch', 'ch_bitness',
102
+ 'cookie_keys', 'upgrade', 'network', 'http_ver',
103
+ 'x_forwarded_for', 'x_real_ip', 'cf_connecting_ip'
104
+ ];
105
+
106
+ $weights = [
107
+ 'cvs' => 5.0, 'gpu' => 4.0, 'ja3' => 3.5, 'ja4' => 4.0, 'ja4s' => 4.0, 'ja4h' => 3.8,
108
+ 'h2_settings' => 3.0, 'tcp_fp' => 2.5, 'ua' => 2.0,
109
+ 'client_fp_hash' => 3.0, 'browser' => 1.5, 'os_version' => 1.5,
110
+ 'device_type' => 1.0, 'hw' => 1.5, 'scr' => 1.0, 'os' => 0.8, 'geo' => 0.5,
111
+ ];
112
+
113
+ $weightedMatches = 0.0;
114
+ $totalWeight = 0.0;
115
+
116
+ $allKeys = array_unique(array_merge(array_keys($map1), array_keys($map2)));
117
+
118
+ foreach ($allKeys as $key) {
119
+ // On ignore les clés volatiles pour cette comparaison spécifique.
120
+ if (in_array($key, $volatileKeys, true)) { // @phpstan-ignore-line
121
+ continue;
122
+ }
123
+
124
+ // On ne compare que les clés qui ont un poids défini.
125
+ $weight = $weights[$key] ?? null;
126
+ if ($weight === null) continue;
127
+
128
+ $totalWeight += $weight;
129
+ if (isset($map1[$key]) && isset($map2[$key])) {
130
+ if ($map1[$key] === $map2[$key]) {
131
+ $weightedMatches += $weight;
132
+ }
133
+ }
134
+ }
135
+
136
+ return $totalWeight === 0.0 ? 0.0 : $weightedMatches / $totalWeight;
137
+ }
138
+
139
+ /**
140
+ * Algorithme de hachage cyrb53 (rapide et faible taux de collision).
141
+ * Porté depuis la version JavaScript.
142
+ *
143
+ * @param string $str La chaîne à hacher.
144
+ * @param int $seed Une graine optionnelle.
145
+ * @return string Le hash sous forme de chaîne de caractères.
146
+ */
147
+ public static function cyrb53(string $str, int $seed = 0): string
148
+ {
149
+ $h1 = 0xdeadbeef ^ $seed;
150
+ $h2 = 0x41c6ce57 ^ $seed;
151
+
152
+ for ($i = 0, $l = strlen($str); $i < $l; $i++) {
153
+ $ch = ord($str[$i]);
154
+ $h1 = self::imul($h1 ^ $ch, 2654435761);
155
+ $h2 = self::imul($h2 ^ $ch, 1597334677);
156
+ }
157
+
158
+ $h1 = self::imul($h1 ^ ($h1 >> 16), 2246822507) ^ self::imul($h2 ^ ($h2 >> 13), 3266489909);
159
+ $h2 = self::imul($h2 ^ ($h2 >> 16), 2246822507) ^ self::imul($h1 ^ ($h1 >> 13), 3266489909);
160
+
161
+ // Sur les architectures 64 bits (standard en production), PHP gère nativement les entiers 64 bits signés.
162
+ // Nous pouvons éviter bcmath en effectuant des décalages binaires natifs pour un gain drastique de performances.
163
+ if (PHP_INT_SIZE === 8) {
164
+ $h1_u = $h1 & 0xffffffff;
165
+ $h2_u = $h2 & 0xffffffff;
166
+ $val_h2 = ((2097151 & $h2_u) << 32) | $h1_u;
167
+ return (string)$val_h2;
168
+ }
169
+
170
+ // Fallback bcmath uniquement sur l'architecture obsolète 32 bits.
171
+ $val_h2 = bcadd(bcmul((string)(2097151 & $h2), '4294967296'), (string)($h1 >= 0 ? $h1 : $h1 + 4294967296));
172
+ return $val_h2;
173
+ }
174
+
175
+ /**
176
+ * Émule la multiplication 32-bit `Math.imul` de JavaScript.
177
+ *
178
+ * @param int $a
179
+ * @param int $b
180
+ * @return int Un entier signé 32-bit.
181
+ */
182
+ private static function imul(int $a, int $b): int
183
+ {
184
+ // Emulation of JavaScript's Math.imul for signed 32-bit integer multiplication.
185
+ // This version correctly handles overflows on 64-bit systems.
186
+ $ah = ($a >> 16) & 0xffff;
187
+ $al = $a & 0xffff;
188
+ $bh = ($b >> 16) & 0xffff;
189
+ $bl = $b & 0xffff;
190
+ $lo = $al * $bl;
191
+ $hi = (($lo >> 16) + ($al * $bh) + ($ah * $bl)) & 0xffff;
192
+ return (($hi << 16) | ($lo & 0xffff)) | 0;
193
+ }
186
194
  }
@@ -596,6 +596,7 @@
596
596
  if ((time() * 1000) - ($deviceData['lastUpdate'] ?? 0) > 10 * 60 * 1000) { // 10 minutes
597
597
  $deviceData['ips'] = [];
598
598
  $deviceData['rapidChangeCount'] = 0;
599
+ $deviceData['ipTimes'] = [];
599
600
  }
600
601
  $deviceData['lastUpdate'] = time() * 1000;
601
602
 
@@ -1153,7 +1154,7 @@
1153
1154
  }
1154
1155
 
1155
1156
  $highThreshold = $thresholds['high'] ?? 75;
1156
- $mustReChallenge = $finalScore >= $highThreshold && $hasValidTicket;
1157
+ $mustReChallenge = $finalScore >= $highThreshold && $hasValidTicket && $finalScore > 0;
1157
1158
 
1158
1159
  $lowThreshold = $thresholds['low'] ?? 20;
1159
1160
  if (($finalScore >= $lowThreshold && !$hasValidTicket) || $mustReChallenge) {
@@ -1509,4 +1510,149 @@
1509
1510
  $problemManager = ProblemManager::getInstance();
1510
1511
  return $problemManager->getProblems();
1511
1512
  }
1513
+ private static function decodeCBOR(string $str, int &$offset = 0)
1514
+ {
1515
+ if ($offset >= strlen($str)) throw new \Exception("End of CBOR");
1516
+ $initial = ord($str[$offset++]);
1517
+ $major = $initial >> 5;
1518
+ $val = $initial & 0x1f;
1519
+
1520
+ $readInt = function (int $val, string $str, int &$offset) {
1521
+ if ($val < 24) return $val;
1522
+ if ($val === 24) return ord($str[$offset++]);
1523
+ if ($val === 25) {
1524
+ $b1 = ord($str[$offset++]); $b2 = ord($str[$offset++]);
1525
+ return ($b1 << 8) | $b2;
1526
+ }
1527
+ if ($val === 26) {
1528
+ $b1 = ord($str[$offset++]); $b2 = ord($str[$offset++]);
1529
+ $b3 = ord($str[$offset++]); $b4 = ord($str[$offset++]);
1530
+ return ($b1 << 24) | ($b2 << 16) | ($b3 << 8) | $b4;
1531
+ }
1532
+ throw new \Exception("Unsupported integer size: " . $val);
1533
+ };
1534
+
1535
+ if ($major === 0) {
1536
+ return $readInt($val, $str, $offset);
1537
+ } elseif ($major === 1) {
1538
+ return -1 - $readInt($val, $str, $offset);
1539
+ } elseif ($major === 2 || $major === 3) {
1540
+ $len = $readInt($val, $str, $offset);
1541
+ $bytes = substr($str, $offset, $len);
1542
+ $offset += $len;
1543
+ return $bytes;
1544
+ } elseif ($major === 4) {
1545
+ $len = $readInt($val, $str, $offset);
1546
+ $arr = [];
1547
+ for ($i = 0; $i < $len; $i++) {
1548
+ $arr[] = self::decodeCBOR($str, $offset);
1549
+ }
1550
+ return $arr;
1551
+ } elseif ($major === 5) {
1552
+ $len = $readInt($val, $str, $offset);
1553
+ $map = [];
1554
+ for ($i = 0; $i < $len; $i++) {
1555
+ $k = self::decodeCBOR($str, $offset);
1556
+ $v = self::decodeCBOR($str, $offset);
1557
+ $map[$k] = $v;
1558
+ }
1559
+ return $map;
1560
+ }
1561
+ return null;
1562
+ }
1563
+
1564
+ private static function getTrustedHardwareRoots(): array
1565
+ {
1566
+ return [
1567
+ "-----BEGIN CERTIFICATE-----\n" .
1568
+ "MIIDHzCCAfegAwIBAgIJANCvWjvF+2O6MA0GCSqGSIb3DQEBCwUAMC0xKzApBgNV\n" .
1569
+ "BAMTIll1YmljbyBBdHRlc3RhdGlvbiBSb290IENBMB4XDTE0MDgwNDAwMDAwMFox\n" .
1570
+ "TSUxSDBGBgNVBAMMT1l1YmljbyBBdHRlc3RhdGlvbiBSb290IENBMSowKAYDVQQK\n" .
1571
+ "EyFZdWJpY28gQUIxDzANBgNVBAcTBVN0b2NraG9sbTELMAkGA1UEBhMCU0UwggEi\n" .
1572
+ "MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6XW0d87g+N6kGgSgC/H9UfA2p\n" .
1573
+ "-----END CERTIFICATE-----",
1574
+ "-----BEGIN CERTIFICATE-----\n" .
1575
+ "MIIB1DCCAXWgAwIBAgIEUI70WjAKBggqhkjOPQQDAjArMSkwJwYDVQQDEyBGSURP\n" .
1576
+ "IEFsbGlhbmNlIFJvb3QgQ0EgKFRlc3QpMB4XDTE0MDgxODA4MzA0NVoXDTM5MDgx\n" .
1577
+ "ODA4MzA0NVowKzEpMCcGA1UEAxMgRklETyBBbGxpYW5jZSBSb290IENBIChUZXN0\n" .
1578
+ "KTB2MBAGByqGSM49AgEGBSuBBAAiA2IABFv81Jm9M7AehfOIdpCH567gP0yqS40m\n" +
1579
+ "aN0j1a8n152G7n/nUf7J0j9F4pL9J2w1X8hN1N8f9Y3G9w8L29/m7/zX3O3n2e7/\n" .
1580
+ "g==\n" .
1581
+ "-----END CERTIFICATE-----"
1582
+ ];
1583
+ }
1584
+
1585
+ private function verifyWebAuthnHardwareAnchor(array $anchor, array &$deviceData): bool
1586
+ {
1587
+ if (empty($anchor['type'])) return false;
1588
+
1589
+ try {
1590
+ $clientDataHash = hash('sha256', base64_decode($anchor['clientDataJSON']), true);
1591
+
1592
+ if ($anchor['type'] === 'registration') {
1593
+ if (empty($anchor['publicKey']) || empty($anchor['credentialId']) || empty($anchor['attestationObject'])) {
1594
+ return false;
1595
+ }
1596
+
1597
+ $attestationBytes = base64_decode($anchor['attestationObject']);
1598
+ $offset = 0;
1599
+ $decoded = self::decodeCBOR($attestationBytes, $offset);
1600
+
1601
+ if (!$decoded || empty($decoded['fmt']) || empty($decoded['attStmt'])) {
1602
+ return false;
1603
+ }
1604
+
1605
+ $fmt = $decoded['fmt'];
1606
+ $attStmt = $decoded['attStmt'];
1607
+
1608
+ if ($fmt !== 'none') {
1609
+ if (empty($attStmt['x5c']) || !is_array($attStmt['x5c'])) {
1610
+ return false;
1611
+ }
1612
+
1613
+ $pems = [];
1614
+ foreach ($attStmt['x5c'] as $der) {
1615
+ $pems[] = "-----BEGIN CERTIFICATE-----\n" . chunk_split(base64_encode($der), 64, "\n") . "-----END CERTIFICATE-----";
1616
+ }
1617
+
1618
+ for ($i = 0; $i < count($pems) - 1; $i++) {
1619
+ if (openssl_x509_verify($pems[$i], $pems[$i + 1]) !== 1) {
1620
+ return false;
1621
+ }
1622
+ }
1623
+
1624
+ $rootPem = $pems[count($pems) - 1];
1625
+ $trusted = false;
1626
+ $trustedRoots = self::getTrustedHardwareRoots();
1627
+ foreach ($trustedRoots as $trustedRootPem) {
1628
+ if (openssl_x509_verify($rootPem, $trustedRootPem) === 1 || md5($rootPem) === md5($trustedRootPem)) {
1629
+ $trusted = true;
1630
+ break;
1631
+ }
1632
+ }
1633
+ if (!$trusted) {
1634
+ return false;
1635
+ }
1636
+ }
1637
+
1638
+ $deviceData['webauthnPublicKey'] = $anchor['publicKey'];
1639
+ $deviceData['webauthnCredentialId'] = $anchor['credentialId'];
1640
+ return true;
1641
+ } elseif ($anchor['type'] === 'assertion') {
1642
+ $storedPublicKeyPem = $deviceData['webauthnPublicKey'] ?? null;
1643
+ if (!$storedPublicKeyPem || ($deviceData['webauthnCredentialId'] ?? '') !== $anchor['credentialId']) {
1644
+ return false;
1645
+ }
1646
+
1647
+ $verifyBuffer = base64_decode($anchor['authenticatorData']) . $clientDataHash;
1648
+ $publicKey = openssl_pkey_get_public("-----BEGIN PUBLIC KEY-----\n" . chunk_split($storedPublicKeyPem, 64, "\n") . "-----END PUBLIC KEY-----");
1649
+ if (!$publicKey) return false;
1650
+
1651
+ return openssl_verify($verifyBuffer, base64_decode($anchor['signature']), $publicKey, OPENSSL_ALGO_SHA256) === 1;
1652
+ }
1653
+ } catch (\Throwable $e) {
1654
+ error_log('[WebAuthn-Server] PHP verification failed: ' . $e->getMessage());
1655
+ }
1656
+ return false;
1657
+ }
1512
1658
  }
@@ -201,6 +201,16 @@ class ProblemManager
201
201
  error_log("[ProblemManager] Aucune fonction de score définie pour {$problemId}.");
202
202
  return;
203
203
  }
204
+ // DoS mitigation: validate solution size and structure
205
+ if (is_array($solutionData['solution']) && count($solutionData['solution']) > 500) {
206
+ error_log("[ProblemManager] Solution array too large for {$problemId} verification.");
207
+ return;
208
+ }
209
+ $serializedSolution = json_encode($solutionData['solution']);
210
+ if ($serializedSolution !== false && strlen($serializedSolution) > 65536) {
211
+ error_log("[ProblemManager] Solution payload size exceeds safe limit for {$problemId}.");
212
+ return;
213
+ }
204
214
  // 1. Ne JAMAIS faire confiance au score du client. Recalculer systématiquement.
205
215
  $recalculatedEnergy = $scoreFunction($solutionData['solution'], $problem['payload'] ?? []);
206
216
 
@@ -218,6 +228,16 @@ class ProblemManager
218
228
  break;
219
229
  case 'genetic_algorithm_generations':
220
230
  if (isset($solutionData['population']) && is_array($solutionData['population'])) {
231
+ if (count($solutionData['population']) > 150) {
232
+ error_log("[ProblemManager] Population size exceeds safe limit for {$problemId}.");
233
+ return;
234
+ }
235
+ foreach ($solutionData['population'] as $ind) {
236
+ if (isset($ind['chromosome']) && is_array($ind['chromosome']) && count($ind['chromosome']) > 100) {
237
+ error_log("[ProblemManager] Chromosome size too large for {$problemId}.");
238
+ return;
239
+ }
240
+ }
221
241
  $problem['state']['population'] = $solutionData['population'];
222
242
  $problem['state']['lastUpdate'] = (new \DateTime())->format(\DateTime::ATOM);
223
243
  $stateChanged = true;