@anonympins/fingerprint 0.7.1 → 0.7.3
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 +28 -3
- package/README.md +1 -1
- package/package.json +1 -1
- package/public/fingerprint-anti-bot.zip +0 -0
- package/src/js/fingerprint.js +106 -29
- package/src/js/fingerprint.utils.js +32 -5
- package/src/js/tests/fingerprint.test.js +91 -4
- package/src/js/tests/threatIntel.test.js +76 -0
- package/src/php/AutoTuner.php +36 -18
- package/src/php/Challenge/ChallengeUtils.php +107 -225
- package/src/php/DirectFingerprint.php +16 -4
- package/src/php/FingerprintClient.php +17 -19
- package/src/php/FingerprintEngine.php +114 -51
- package/src/php/Ja3AnomalyDetector.php +20 -12
- package/src/php/Optimization/Optimization.php +5 -1
- package/src/php/Optimization/OptimizationOperators.php +5 -1
- package/src/php/Optimization/ProblemInitializers.php +16 -4
- package/src/php/ProblemManager.php +20 -14
- package/src/php/Tests/FingerprintEngineTest.php +705 -662
- package/src/php/Tests/ThreatIntelTest.php +64 -0
- package/src/php/Utils/BlockList.php +99 -99
- package/src/php/Utils/Env.php +51 -49
- package/src/php/Utils/MaliciousPatterns.php +74 -74
- package/src/php/Utils/RequestUtils.php +22 -5
- package/src/php/WordPress/WpDbStore.php +4 -0
- package/src/php/WordPress/fingerprint-anti-bot.php +559 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +262 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +268 -0
- package/src/php/WordPress/package.php +102 -4
- package/src/php/WordPress/readme.txt +36 -0
- package/public/fingerprint-wordpress.zip +0 -0
- package/src/js/fingerprint.client.obfuscated.js +0 -1
- package/src/php/WordPress/fingerprint-wordpress.php +0 -217
|
@@ -34,17 +34,25 @@ class DirectFingerprint
|
|
|
34
34
|
public function protect(): ?array
|
|
35
35
|
{
|
|
36
36
|
// 1. Build request context from PHP superglobals
|
|
37
|
+
// phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended -- Direct WAF inspection firewall before WP core logic
|
|
37
38
|
$body = $_POST ?: json_decode(file_get_contents('php://input'), true);
|
|
38
39
|
$headers = function_exists('getallheaders') ? getallheaders() : [];
|
|
39
40
|
|
|
41
|
+
$remoteAddr = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '127.0.0.1';
|
|
42
|
+
$rawUri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '/';
|
|
43
|
+
// phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- Uses wp_parse_url when available
|
|
44
|
+
$path = function_exists('wp_parse_url') ? (string)wp_parse_url($rawUri, PHP_URL_PATH) : (string)parse_url($rawUri, PHP_URL_PATH);
|
|
45
|
+
$serverProtocol = isset($_SERVER['SERVER_PROTOCOL']) ? sanitize_text_field(wp_unslash($_SERVER['SERVER_PROTOCOL'])) : '1.1';
|
|
46
|
+
|
|
40
47
|
$context = new RequestContext(
|
|
41
|
-
$
|
|
42
|
-
|
|
48
|
+
$remoteAddr,
|
|
49
|
+
!empty($path) ? $path : '/',
|
|
43
50
|
$headers,
|
|
51
|
+
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
|
44
52
|
$_GET,
|
|
45
53
|
$body,
|
|
46
54
|
$_COOKIE,
|
|
47
|
-
$
|
|
55
|
+
$serverProtocol
|
|
48
56
|
);
|
|
49
57
|
|
|
50
58
|
// 2. Process request with engine
|
|
@@ -62,9 +70,11 @@ class DirectFingerprint
|
|
|
62
70
|
http_response_code($decision['status'] ?? 403);
|
|
63
71
|
if (is_array($decision['body'])) {
|
|
64
72
|
header('Content-Type: application/json');
|
|
73
|
+
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON API challenge payload
|
|
65
74
|
echo json_encode($decision['body']);
|
|
66
75
|
} else {
|
|
67
76
|
header('Content-Type: text/html; charset=utf-8');
|
|
77
|
+
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Proof-of-Work challenge HTML page
|
|
68
78
|
echo $decision['body'];
|
|
69
79
|
}
|
|
70
80
|
exit();
|
|
@@ -152,7 +162,8 @@ class DirectFingerprint
|
|
|
152
162
|
switch ($decision['action']) {
|
|
153
163
|
case 'block':
|
|
154
164
|
http_response_code($decision['status'] ?? 403);
|
|
155
|
-
|
|
165
|
+
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
|
166
|
+
echo esc_html($decision['body'] ?? "Access denied.");
|
|
156
167
|
exit();
|
|
157
168
|
case 'redirect':
|
|
158
169
|
header('Location: ' . $decision['path'], true, $decision['status'] ?? 302);
|
|
@@ -176,6 +187,7 @@ class DirectFingerprint
|
|
|
176
187
|
|
|
177
188
|
// 3. If authorized, stream metrics output
|
|
178
189
|
header('Content-Type: text/plain; version=0.0.4; charset=utf-8');
|
|
190
|
+
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Prometheus exposition format output
|
|
179
191
|
echo MetricsManager::getPrometheusMetrics();
|
|
180
192
|
exit();
|
|
181
193
|
}
|
|
@@ -87,7 +87,7 @@ class FingerprintClient
|
|
|
87
87
|
$containerTags = ['div', 'span', 'p', 'section'];
|
|
88
88
|
$tag = $containerTags[array_rand($containerTags)];
|
|
89
89
|
|
|
90
|
-
$nestingType =
|
|
90
|
+
$nestingType = function_exists('wp_rand') ? wp_rand(0, 1) : random_int(0, 1);
|
|
91
91
|
if ($nestingType === 1) {
|
|
92
92
|
return '<' . $tag . ' style="' . $styles . '" aria-hidden="true">'
|
|
93
93
|
. '<label for="' . htmlspecialchars($fieldName) . '">' . htmlspecialchars($fieldName)
|
|
@@ -113,26 +113,24 @@ class FingerprintClient
|
|
|
113
113
|
$nonceAttr = $this->nonce ? ' nonce="' . $this->nonce . '"' : '';
|
|
114
114
|
|
|
115
115
|
// Inline initialization script embedded in HTML
|
|
116
|
-
$initScript =
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
wasmScript.
|
|
123
|
-
wasmScript.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
JS;
|
|
116
|
+
$initScript = "document.addEventListener('DOMContentLoaded', function() {\n"
|
|
117
|
+
. " const config = " . $configJson . ";\n"
|
|
118
|
+
. " if (window.ClientLibrary) {\n"
|
|
119
|
+
. " if (config.wasmPath) {\n"
|
|
120
|
+
. " const wasmScript = document.createElement('script');\n"
|
|
121
|
+
. " wasmScript.src = config.wasmPath;\n"
|
|
122
|
+
. " wasmScript.async = true;\n"
|
|
123
|
+
. " wasmScript.nonce = '" . addslashes((string)$this->nonce) . "';\n"
|
|
124
|
+
. " document.head.appendChild(wasmScript);\n"
|
|
125
|
+
. " }\n"
|
|
126
|
+
. " window.ClientLibrary.initializeClient(config);\n"
|
|
127
|
+
. " } else {\n"
|
|
128
|
+
. " console.error('Fingerprint client library not loaded.');\n"
|
|
129
|
+
. " }\n"
|
|
130
|
+
. "});";
|
|
134
131
|
|
|
135
132
|
// Combine library loader script and inline initialization
|
|
133
|
+
// phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript -- Polymorphic dynamic client script tag generation
|
|
136
134
|
return '<script src="' . htmlspecialchars($this->clientScriptPath) . '"' . $nonceAttr . '></script>'
|
|
137
135
|
. '<script' . $nonceAttr . '>' . $initScript . '</script>';
|
|
138
136
|
}
|
|
@@ -127,41 +127,27 @@
|
|
|
127
127
|
$useAsymmetric = ($securityConfig['useAsymmetricTickets'] ?? false) === true || ($securityConfig['ed25519'] ?? '') === 'auto';
|
|
128
128
|
$envPrivate = Env::get('ED25519_PRIVATE_KEY');
|
|
129
129
|
if ($useAsymmetric && ($envPrivate === null || $envPrivate === '')) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
130
|
+
if (function_exists('get_option') && function_exists('update_option')) {
|
|
131
|
+
// In WordPress, persist cryptographic keys in the database options table
|
|
132
|
+
$storedKeys = get_option('fingerprint_ed25519_keys');
|
|
133
|
+
if (is_array($storedKeys) && isset($storedKeys['privateKey'], $storedKeys['publicKey'])) {
|
|
134
|
+
Env::set('ED25519_PRIVATE_KEY', $storedKeys['privateKey']);
|
|
135
|
+
Env::set('ED25519_PUBLIC_KEY', $storedKeys['publicKey']);
|
|
136
|
+
} elseif (defined('OPENSSL_KEYTYPE_ED25519')) {
|
|
137
|
+
$pkey = openssl_pkey_new(["private_key_type" => OPENSSL_KEYTYPE_ED25519]);
|
|
138
|
+
if ($pkey && openssl_pkey_export($pkey, $privateKeyPem)) {
|
|
139
|
+
$details = openssl_pkey_get_details($pkey); // @phpstan-ignore-line
|
|
140
|
+
$publicKeyPem = $details['key'] ?? ''; // @phpstan-ignore-line
|
|
141
|
+
Env::set('ED25519_PRIVATE_KEY', $privateKeyPem);
|
|
142
|
+
Env::set('ED25519_PUBLIC_KEY', $publicKeyPem);
|
|
143
|
+
update_option('fingerprint_ed25519_keys', [
|
|
144
|
+
'privateKey' => $privateKeyPem,
|
|
145
|
+
'publicKey' => $publicKeyPem,
|
|
146
|
+
], false);
|
|
139
147
|
}
|
|
140
|
-
} catch (\Throwable $e) {
|
|
141
|
-
error_log('[Fingerprint] Failed to load persistent Ed25519 keys: ' . $e->getMessage());
|
|
142
148
|
}
|
|
143
149
|
} else {
|
|
144
|
-
|
|
145
|
-
if (defined('OPENSSL_KEYTYPE_ED25519')) {
|
|
146
|
-
$pkey = openssl_pkey_new(["private_key_type" => OPENSSL_KEYTYPE_ED25519]);
|
|
147
|
-
if ($pkey && openssl_pkey_export($pkey, $privateKeyPem)) {
|
|
148
|
-
$details = openssl_pkey_get_details($pkey); // @phpstan-ignore-line
|
|
149
|
-
$publicKeyPem = $details['key']; // @phpstan-ignore-line
|
|
150
|
-
Env::set('ED25519_PRIVATE_KEY', $privateKeyPem);
|
|
151
|
-
Env::set('ED25519_PUBLIC_KEY', $publicKeyPem);
|
|
152
|
-
// Also update $_ENV and $_SERVER for consistency
|
|
153
|
-
if (!is_dir($configDir)) {
|
|
154
|
-
mkdir($configDir, 0777, true);
|
|
155
|
-
}
|
|
156
|
-
file_put_contents($persistentKeyPath, json_encode([
|
|
157
|
-
'privateKey' => $privateKeyPem,
|
|
158
|
-
'publicKey' => $publicKeyPem
|
|
159
|
-
], JSON_PRETTY_PRINT));
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
} catch (\Throwable $e) {
|
|
163
|
-
error_log('[Fingerprint] Native Ed25519 key generation failed: ' . $e->getMessage());
|
|
164
|
-
}
|
|
150
|
+
self::initStandaloneEd25519Keys();
|
|
165
151
|
}
|
|
166
152
|
}
|
|
167
153
|
|
|
@@ -175,10 +161,16 @@
|
|
|
175
161
|
$securityConfig = SecurityProfiles::deepMerge($securityConfig, $savedConfig);
|
|
176
162
|
}
|
|
177
163
|
} catch (\Throwable $e) {
|
|
178
|
-
|
|
164
|
+
self::logError("[FingerprintEngine] Failed to auto-load optimized config from {$savePath}: " . $e->getMessage());
|
|
179
165
|
}
|
|
180
166
|
}
|
|
181
167
|
}
|
|
168
|
+
|
|
169
|
+
// Si aucune liste blanche n'est explicitement fournie, appliquer la liste par défaut.
|
|
170
|
+
if (!isset($securityConfig['whitelist'])) {
|
|
171
|
+
$securityConfig['whitelist'] = self::default_whitelist();
|
|
172
|
+
}
|
|
173
|
+
|
|
182
174
|
$this->securityConfig = $securityConfig;
|
|
183
175
|
$this->verbose = $securityConfig['verbose'] ?? false;
|
|
184
176
|
$this->allowlist = $this->buildAllowlist();
|
|
@@ -190,6 +182,49 @@
|
|
|
190
182
|
}
|
|
191
183
|
}
|
|
192
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Initialise et persiste les cles Ed25519 pour les environnements PHP hors WordPress.
|
|
187
|
+
*/
|
|
188
|
+
private static function initStandaloneEd25519Keys(): void
|
|
189
|
+
{
|
|
190
|
+
$configDir = dirname(__DIR__, 1) . '/config';
|
|
191
|
+
$persistentKeyPath = $configDir . '/ed25519_key.json';
|
|
192
|
+
|
|
193
|
+
if (file_exists($persistentKeyPath)) {
|
|
194
|
+
try {
|
|
195
|
+
$keys = json_decode((string)file_get_contents($persistentKeyPath), true);
|
|
196
|
+
if (isset($keys['privateKey'], $keys['publicKey'])) {
|
|
197
|
+
Env::set('ED25519_PRIVATE_KEY', $keys['privateKey']);
|
|
198
|
+
Env::set('ED25519_PUBLIC_KEY', $keys['publicKey']);
|
|
199
|
+
}
|
|
200
|
+
} catch (\Throwable $e) {
|
|
201
|
+
self::logError('[Fingerprint] Failed to load persistent Ed25519 keys: ' . $e->getMessage());
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
try {
|
|
205
|
+
if (defined('OPENSSL_KEYTYPE_ED25519')) {
|
|
206
|
+
$pkey = openssl_pkey_new(["private_key_type" => OPENSSL_KEYTYPE_ED25519]);
|
|
207
|
+
if ($pkey && openssl_pkey_export($pkey, $privateKeyPem)) {
|
|
208
|
+
$details = openssl_pkey_get_details($pkey); // @phpstan-ignore-line
|
|
209
|
+
$publicKeyPem = $details['key'] ?? ''; // @phpstan-ignore-line
|
|
210
|
+
Env::set('ED25519_PRIVATE_KEY', $privateKeyPem);
|
|
211
|
+
Env::set('ED25519_PUBLIC_KEY', $publicKeyPem);
|
|
212
|
+
if (!is_dir($configDir)) {
|
|
213
|
+
mkdir($configDir, 0777, true);
|
|
214
|
+
}
|
|
215
|
+
// phpcs:ignore PluginCheck.CodeAnalysis.WriteFile.PluginDirectoryWrite -- Non-WordPress standalone environment fallback
|
|
216
|
+
file_put_contents($persistentKeyPath, json_encode([
|
|
217
|
+
'privateKey' => $privateKeyPem,
|
|
218
|
+
'publicKey' => $publicKeyPem
|
|
219
|
+
], JSON_PRETTY_PRINT));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} catch (\Throwable $e) {
|
|
223
|
+
self::logError('[Fingerprint] Native Ed25519 key generation failed: ' . $e->getMessage());
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
193
228
|
/**
|
|
194
229
|
* Applique à chaud une nouvelle configuration de sécurité (poids, seuils, etc.)
|
|
195
230
|
* sans nécessiter de redémarrage.
|
|
@@ -260,7 +295,7 @@
|
|
|
260
295
|
}
|
|
261
296
|
}
|
|
262
297
|
} catch (\Throwable $e) {
|
|
263
|
-
|
|
298
|
+
self::logError("[Fingerprint] Error loading whitelist file {$filename}: " . $e->getMessage());
|
|
264
299
|
}
|
|
265
300
|
}
|
|
266
301
|
return $fallbackEntries;
|
|
@@ -352,7 +387,7 @@
|
|
|
352
387
|
} else {
|
|
353
388
|
$logMessage = "[FingerprintEngine] {$message}";
|
|
354
389
|
if (!empty($data)) $logMessage .= ' ' . json_encode($data);
|
|
355
|
-
|
|
390
|
+
self::logError($logMessage);
|
|
356
391
|
}
|
|
357
392
|
}
|
|
358
393
|
}
|
|
@@ -551,10 +586,8 @@
|
|
|
551
586
|
|
|
552
587
|
$deviceId = bin2hex(random_bytes(16)); // UUID-like
|
|
553
588
|
|
|
554
|
-
$isHttps = (
|
|
555
|
-
|
|
556
|
-
($context->getHeader('x-forwarded-proto') === 'https');
|
|
557
|
-
$secureOption = $isHttps || $this->isProduction;
|
|
589
|
+
$isHttps = !empty($context->isHttps);
|
|
590
|
+
$secureOption = $isHttps;
|
|
558
591
|
|
|
559
592
|
// Préparer le cookie à envoyer
|
|
560
593
|
$newCookie = [
|
|
@@ -985,13 +1018,36 @@
|
|
|
985
1018
|
// Le fingerprint est cohérent, on peut valider la solution
|
|
986
1019
|
if ($powType === 'cpu_target' || $powType === 'cpu_mem') {
|
|
987
1020
|
$cpuSolution = $context->query['pow_solution_cpu'] ?? $context->query['pow_solution'] ?? null;
|
|
988
|
-
if ($cpuSolution) {
|
|
989
|
-
$
|
|
1021
|
+
if ($cpuSolution !== null && $cpuSolution !== '') {
|
|
1022
|
+
$identity = $this->resolveRequestIdentity($context, $suspicionVector);
|
|
1023
|
+
$deviceId = (string)($identity['deviceId'] ?? '');
|
|
1024
|
+
$currentDeviceHash = (string)($identity['currentDeviceHash'] ?? RequestUtils::getCompositeDeviceHash($context));
|
|
1025
|
+
|
|
1026
|
+
$ticket = ChallengeUtils::verifyCpuTargetPoWAndGenerateTicket(
|
|
1027
|
+
$context->clientIp,
|
|
1028
|
+
3600000,
|
|
1029
|
+
$powNonce,
|
|
1030
|
+
(string)$cpuSolution,
|
|
1031
|
+
$challengeContext,
|
|
1032
|
+
$deviceId,
|
|
1033
|
+
$currentDeviceHash
|
|
1034
|
+
);
|
|
990
1035
|
$isValid = $ticket !== null;
|
|
991
1036
|
|
|
992
1037
|
if ($powType === 'cpu_mem') {
|
|
993
|
-
|
|
994
|
-
|
|
1038
|
+
if (!empty($challengeContext['isHttp'])) {
|
|
1039
|
+
$isMemValid = true;
|
|
1040
|
+
} else {
|
|
1041
|
+
$memSolution = $context->query['pow_solution_mem'] ?? null;
|
|
1042
|
+
$isMemValid = ($memSolution !== null && $memSolution !== '')
|
|
1043
|
+
? ChallengeUtils::verifyMemoryPoW(
|
|
1044
|
+
$powNonce,
|
|
1045
|
+
(string)$memSolution,
|
|
1046
|
+
$challengeContext['memDifficulty'] ?? 0,
|
|
1047
|
+
$challengeContext['clientSecret'] ?? ''
|
|
1048
|
+
)
|
|
1049
|
+
: false;
|
|
1050
|
+
}
|
|
995
1051
|
$isValid = $isValid && $isMemValid;
|
|
996
1052
|
}
|
|
997
1053
|
}
|
|
@@ -1073,10 +1129,8 @@
|
|
|
1073
1129
|
MetricsManager::incrementCounter('challenges_solved_total');
|
|
1074
1130
|
$this->log('Challenge solution valid - issuing ticket', ['ticketMaxAge' => $ticketTtl]);
|
|
1075
1131
|
|
|
1076
|
-
$isHttps = (
|
|
1077
|
-
|
|
1078
|
-
($context->getHeader('x-forwarded-proto') === 'https');
|
|
1079
|
-
$secureOption = $isHttps || $this->isProduction;
|
|
1132
|
+
$isHttps = !empty($context->isHttps);
|
|
1133
|
+
$secureOption = $isHttps;
|
|
1080
1134
|
|
|
1081
1135
|
return [
|
|
1082
1136
|
'action' => 'redirect',
|
|
@@ -1529,7 +1583,7 @@
|
|
|
1529
1583
|
$isAsymmetric = true;
|
|
1530
1584
|
}
|
|
1531
1585
|
} catch (\Throwable $e) {
|
|
1532
|
-
|
|
1586
|
+
self::logError('[Fingerprint] Asymmetric broadcast signing failed: ' . $e->getMessage());
|
|
1533
1587
|
}
|
|
1534
1588
|
}
|
|
1535
1589
|
|
|
@@ -1550,7 +1604,8 @@
|
|
|
1550
1604
|
|
|
1551
1605
|
private function asyncPost(string $url, array $params): void
|
|
1552
1606
|
{
|
|
1553
|
-
|
|
1607
|
+
// phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- wp_parse_url used if available
|
|
1608
|
+
$parts = function_exists('wp_parse_url') ? wp_parse_url($url) : parse_url($url);
|
|
1554
1609
|
if ($parts === false) return;
|
|
1555
1610
|
|
|
1556
1611
|
$host = $parts['host'];
|
|
@@ -1580,7 +1635,9 @@
|
|
|
1580
1635
|
$out .= "Connection: Close\r\n\r\n";
|
|
1581
1636
|
$out .= $postData;
|
|
1582
1637
|
|
|
1638
|
+
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- Socket stream write
|
|
1583
1639
|
@fwrite($fp, $out);
|
|
1640
|
+
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Socket stream close
|
|
1584
1641
|
@fclose($fp);
|
|
1585
1642
|
}
|
|
1586
1643
|
}
|
|
@@ -1707,7 +1764,7 @@
|
|
|
1707
1764
|
$b3 = ord($str[$offset++]); $b4 = ord($str[$offset++]);
|
|
1708
1765
|
return ($b1 << 24) | ($b2 << 16) | ($b3 << 8) | $b4;
|
|
1709
1766
|
}
|
|
1710
|
-
throw new \Exception("Unsupported integer size: " . $val);
|
|
1767
|
+
throw new \Exception("Unsupported integer size: " . (int)$val);
|
|
1711
1768
|
};
|
|
1712
1769
|
|
|
1713
1770
|
if ($major === 0) {
|
|
@@ -1829,8 +1886,14 @@
|
|
|
1829
1886
|
return openssl_verify($verifyBuffer, base64_decode($anchor['signature']), $publicKey, OPENSSL_ALGO_SHA256) === 1;
|
|
1830
1887
|
}
|
|
1831
1888
|
} catch (\Throwable $e) {
|
|
1832
|
-
|
|
1889
|
+
self::logError('[WebAuthn-Server] PHP verification failed: ' . $e->getMessage());
|
|
1833
1890
|
}
|
|
1834
1891
|
return false;
|
|
1835
1892
|
}
|
|
1893
|
+
|
|
1894
|
+
private static function logError(string $message): void
|
|
1895
|
+
{
|
|
1896
|
+
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Engine diagnostic error log
|
|
1897
|
+
error_log($message);
|
|
1898
|
+
}
|
|
1836
1899
|
}
|
|
@@ -212,10 +212,12 @@ class Ja3AnomalyDetector
|
|
|
212
212
|
|
|
213
213
|
/**
|
|
214
214
|
* Correlates low-level TCP RTT with application layer latency to detect residential proxy hops.
|
|
215
|
+
* Evaluates continuous physical transit discrepancies rather than arbitrary step numbers.
|
|
216
|
+
*
|
|
215
217
|
* @param RequestContext $context
|
|
216
|
-
* @return
|
|
218
|
+
* @return float Suspicion score between 0.0 and 95.0
|
|
217
219
|
*/
|
|
218
|
-
public static function getRttProxyScore(RequestContext $context):
|
|
220
|
+
public static function getRttProxyScore(RequestContext $context): float
|
|
219
221
|
{
|
|
220
222
|
$tcpRttHeader = $context->headers['x-tcp-rtt'] ?? $context->headers['x-real-rtt'] ?? null;
|
|
221
223
|
$tcpRtt = $tcpRttHeader !== null ? (int)$tcpRttHeader : null;
|
|
@@ -223,22 +225,28 @@ class Ja3AnomalyDetector
|
|
|
223
225
|
$behaviorHeader = $context->headers['x-behavior-metrics'] ?? null;
|
|
224
226
|
if ($behaviorHeader) {
|
|
225
227
|
$metrics = json_decode($behaviorHeader, true);
|
|
226
|
-
if (json_last_error() === JSON_ERROR_NONE && isset($metrics['clientTimestamp'])) {
|
|
228
|
+
if (json_last_error() === JSON_ERROR_NONE && isset($metrics['clientTimestamp']) && is_numeric($metrics['clientTimestamp'])) {
|
|
227
229
|
$clientTimestamp = (int)$metrics['clientTimestamp'];
|
|
228
230
|
$appLatency = $context->requestTimestamp - $clientTimestamp;
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
231
|
+
|
|
232
|
+
if ($tcpRtt !== null && $tcpRtt > 0 && $appLatency > 0) {
|
|
233
|
+
// Marge de tolérance défensive contre le jitter réseau et le scheduling JS / Garbage Collection
|
|
234
|
+
$jitterAllowance = 60.0;
|
|
235
|
+
$effectiveRtt = max((float)$tcpRtt, 5.0);
|
|
236
|
+
$tunnelDelta = $appLatency - ($tcpRtt + $jitterAllowance);
|
|
237
|
+
$divergenceRatio = $appLatency / $effectiveRtt;
|
|
238
|
+
|
|
239
|
+
// Condition de disjonction : liaison edge ultra-proche (< 40ms) + transit applicatif au moins 3x supérieur
|
|
240
|
+
if ($tcpRtt <= 40 && $divergenceRatio >= 3.0 && $tunnelDelta > 0) {
|
|
241
|
+
$scaling = 120.0;
|
|
242
|
+
$ratioWeight = min(1.0, ($divergenceRatio - 3.0) / 5.0);
|
|
243
|
+
$proxyScore = min(95.0, 40.0 + 55.0 * tanh($tunnelDelta / $scaling) * $ratioWeight);
|
|
244
|
+
return round($proxyScore, 1);
|
|
237
245
|
}
|
|
238
246
|
}
|
|
239
247
|
}
|
|
240
248
|
}
|
|
241
|
-
return 0;
|
|
249
|
+
return 0.0;
|
|
242
250
|
}
|
|
243
251
|
}
|
|
244
252
|
|
|
@@ -17,7 +17,11 @@ class Optimization
|
|
|
17
17
|
try {
|
|
18
18
|
return random_int(0, PHP_INT_MAX - 1) / PHP_INT_MAX;
|
|
19
19
|
} catch (\Exception $e) {
|
|
20
|
-
|
|
20
|
+
if (function_exists('wp_rand')) {
|
|
21
|
+
return (float)wp_rand(0, 1000000) / 1000000.0;
|
|
22
|
+
}
|
|
23
|
+
// phpcs:ignore WordPress.WP.AlternativeFunctions.rand_mt_rand -- Standalone fallback
|
|
24
|
+
return (float)mt_rand() / (float)mt_getrandmax();
|
|
21
25
|
}
|
|
22
26
|
}
|
|
23
27
|
|
|
@@ -44,7 +44,11 @@ class OptimizationOperators
|
|
|
44
44
|
try {
|
|
45
45
|
return random_int(0, PHP_INT_MAX - 1) / PHP_INT_MAX;
|
|
46
46
|
} catch (\Exception $e) {
|
|
47
|
-
|
|
47
|
+
if (function_exists('wp_rand')) {
|
|
48
|
+
return (float)wp_rand(0, 1000000) / 1000000.0;
|
|
49
|
+
}
|
|
50
|
+
// phpcs:ignore WordPress.WP.AlternativeFunctions.rand_mt_rand -- Standalone fallback
|
|
51
|
+
return (float)mt_rand() / (float)mt_getrandmax();
|
|
48
52
|
}
|
|
49
53
|
}
|
|
50
54
|
|
|
@@ -12,6 +12,18 @@ class ProblemInitializers
|
|
|
12
12
|
/** @var array<string, callable> */
|
|
13
13
|
private static array $initializers = [];
|
|
14
14
|
|
|
15
|
+
private static function randomFloat(): float
|
|
16
|
+
{
|
|
17
|
+
try {
|
|
18
|
+
return (float)random_int(0, PHP_INT_MAX - 1) / (float)PHP_INT_MAX;
|
|
19
|
+
} catch (\Throwable $e) {
|
|
20
|
+
if (function_exists('wp_rand')) {
|
|
21
|
+
return (float)wp_rand(0, 1000000) / 1000000.0;
|
|
22
|
+
}
|
|
23
|
+
return 0.5;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
15
27
|
private static function initialize(): void
|
|
16
28
|
{
|
|
17
29
|
if (empty(self::$initializers)) {
|
|
@@ -22,8 +34,8 @@ class ProblemInitializers
|
|
|
22
34
|
$points = [];
|
|
23
35
|
for ($i = 0; $i < $count; $i++) {
|
|
24
36
|
$points[] = [
|
|
25
|
-
'x' =>
|
|
26
|
-
'y' =>
|
|
37
|
+
'x' => self::randomFloat() * $bounds['x'],
|
|
38
|
+
'y' => self::randomFloat() * $bounds['y']
|
|
27
39
|
];
|
|
28
40
|
}
|
|
29
41
|
return $points;
|
|
@@ -36,8 +48,8 @@ class ProblemInitializers
|
|
|
36
48
|
for ($i = 0; $i < $count; $i++) {
|
|
37
49
|
$assets[] = [
|
|
38
50
|
'name' => 'Asset ' . ($i + 1),
|
|
39
|
-
'expectedReturn' =>
|
|
40
|
-
'volatility' => 0.1 +
|
|
51
|
+
'expectedReturn' => self::randomFloat() * 0.2,
|
|
52
|
+
'volatility' => 0.1 + self::randomFloat() * 0.3
|
|
41
53
|
];
|
|
42
54
|
}
|
|
43
55
|
return $assets;
|
|
@@ -59,17 +59,17 @@ class ProblemManager
|
|
|
59
59
|
private function loadProblems(): void
|
|
60
60
|
{
|
|
61
61
|
if (!file_exists($this->configPath)) {
|
|
62
|
-
|
|
62
|
+
self::logError("[ProblemManager] Problem config file not found: {$this->configPath}");
|
|
63
63
|
return;
|
|
64
64
|
}
|
|
65
65
|
$data = file_get_contents($this->configPath);
|
|
66
66
|
if ($data === false) {
|
|
67
|
-
|
|
67
|
+
self::logError("[ProblemManager] Failed to read problem config file: {$this->configPath}");
|
|
68
68
|
return;
|
|
69
69
|
}
|
|
70
70
|
$problemsFromFile = json_decode($data, true);
|
|
71
71
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
72
|
-
|
|
72
|
+
self::logError("[ProblemManager] Failed to parse problem config JSON: " . json_last_error_msg());
|
|
73
73
|
return;
|
|
74
74
|
}
|
|
75
75
|
|
|
@@ -88,7 +88,7 @@ class ProblemManager
|
|
|
88
88
|
$problem['workUnit']['scoreFunction'] = FunctionRegistry::get($problem['workUnit']['scoreFunction']);
|
|
89
89
|
if ($problem['workUnit']['scoreFunction'] === null) {
|
|
90
90
|
// @codeCoverageIgnoreStart
|
|
91
|
-
|
|
91
|
+
self::logError("[ProblemManager] Warning: scoreFunction '{$problem['workUnit']['scoreFunction']}' not found in registry for problem '{$problem['id']}'.");
|
|
92
92
|
// @codeCoverageIgnoreEnd
|
|
93
93
|
}
|
|
94
94
|
}
|
|
@@ -167,7 +167,7 @@ class ProblemManager
|
|
|
167
167
|
$task['solverName'] = $problem['workUnit']['solverName'];
|
|
168
168
|
break;
|
|
169
169
|
default:
|
|
170
|
-
|
|
170
|
+
self::logError("[ProblemManager] Unknown useful work type: {$problem['workUnit']['type']}");
|
|
171
171
|
return null;
|
|
172
172
|
}
|
|
173
173
|
|
|
@@ -198,17 +198,17 @@ class ProblemManager
|
|
|
198
198
|
if (isset($solutionData['solution']) && isset($solutionData['energy'])) {
|
|
199
199
|
$scoreFunction = $problem['workUnit']['scoreFunction'] ?? null;
|
|
200
200
|
if (!$scoreFunction) {
|
|
201
|
-
|
|
201
|
+
self::logError("[ProblemManager] No score function defined for {$problemId}.");
|
|
202
202
|
return;
|
|
203
203
|
}
|
|
204
204
|
// DoS mitigation: validate solution size and structure
|
|
205
205
|
if (is_array($solutionData['solution']) && count($solutionData['solution']) > 500) {
|
|
206
|
-
|
|
206
|
+
self::logError("[ProblemManager] Solution array too large for {$problemId} verification.");
|
|
207
207
|
return;
|
|
208
208
|
}
|
|
209
209
|
$serializedSolution = json_encode($solutionData['solution']);
|
|
210
210
|
if ($serializedSolution !== false && strlen($serializedSolution) > 65536) {
|
|
211
|
-
|
|
211
|
+
self::logError("[ProblemManager] Solution payload size exceeds safe limit for {$problemId}.");
|
|
212
212
|
return;
|
|
213
213
|
}
|
|
214
214
|
// 1. Never trust client-reported score. Recalculate server-side.
|
|
@@ -222,19 +222,19 @@ class ProblemManager
|
|
|
222
222
|
$problem['state']['bestEnergy'] = $recalculatedEnergy; // 3. Store verified score
|
|
223
223
|
$problem['state']['lastUpdate'] = (new \DateTime())->format(\DateTime::ATOM);
|
|
224
224
|
$stateChanged = true;
|
|
225
|
-
|
|
225
|
+
self::logError("[ProblemManager] New best solution for {$problemId}: {$recalculatedEnergy}"); // @phpstan-ignore-line
|
|
226
226
|
}
|
|
227
227
|
}
|
|
228
228
|
break;
|
|
229
229
|
case 'genetic_algorithm_generations':
|
|
230
230
|
if (isset($solutionData['population']) && is_array($solutionData['population'])) {
|
|
231
231
|
if (count($solutionData['population']) > 150) {
|
|
232
|
-
|
|
232
|
+
self::logError("[ProblemManager] Population size exceeds safe limit for {$problemId}.");
|
|
233
233
|
return;
|
|
234
234
|
}
|
|
235
235
|
foreach ($solutionData['population'] as $ind) {
|
|
236
236
|
if (isset($ind['chromosome']) && is_array($ind['chromosome']) && count($ind['chromosome']) > 100) {
|
|
237
|
-
|
|
237
|
+
self::logError("[ProblemManager] Chromosome size too large for {$problemId}.");
|
|
238
238
|
return;
|
|
239
239
|
}
|
|
240
240
|
}
|
|
@@ -251,7 +251,7 @@ class ProblemManager
|
|
|
251
251
|
$recalculated = $fitnessFunction($individual['chromosome'], $problem['payload'] ?? []);
|
|
252
252
|
if (isset($individual['fitness']) && $individual['fitness'] !== -1) {
|
|
253
253
|
if (abs($individual['fitness'] - $recalculated) > 1e-4) {
|
|
254
|
-
|
|
254
|
+
self::logError("[ProblemManager] Cheating detected for {$problemId}! Reported: {$individual['fitness']}, recalculated: {$recalculated}");
|
|
255
255
|
return; // Reject inconsistent population
|
|
256
256
|
}
|
|
257
257
|
}
|
|
@@ -272,7 +272,7 @@ class ProblemManager
|
|
|
272
272
|
}
|
|
273
273
|
break;
|
|
274
274
|
default:
|
|
275
|
-
|
|
275
|
+
self::logError("[ProblemManager] Integration not implemented for useful work type: {$problem['workUnit']['type']}");
|
|
276
276
|
break;
|
|
277
277
|
}
|
|
278
278
|
// Persist updated state to datastore
|
|
@@ -294,7 +294,7 @@ class ProblemManager
|
|
|
294
294
|
if (!empty($newFront) && json_encode($newFront) !== json_encode($currentFront)) {
|
|
295
295
|
$problem['state']['paretoFront'] = $newFront;
|
|
296
296
|
$problem['state']['lastUpdate'] = (new \DateTime())->format(\DateTime::ATOM);
|
|
297
|
-
|
|
297
|
+
self::logError("[ProblemManager] New Pareto front for {$problem['id']} with " . count($newFront) . " solutions."); // @phpstan-ignore-line
|
|
298
298
|
return true;
|
|
299
299
|
}
|
|
300
300
|
return false;
|
|
@@ -407,4 +407,10 @@ class ProblemManager
|
|
|
407
407
|
{
|
|
408
408
|
return $this->problems;
|
|
409
409
|
}
|
|
410
|
+
|
|
411
|
+
private static function logError(string $message): void
|
|
412
|
+
{
|
|
413
|
+
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- ProblemManager logging
|
|
414
|
+
error_log($message);
|
|
415
|
+
}
|
|
410
416
|
}
|