@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.
Files changed (35) hide show
  1. package/CHANGELOG.md +28 -3
  2. package/README.md +1 -1
  3. package/package.json +1 -1
  4. package/public/fingerprint-anti-bot.zip +0 -0
  5. package/src/js/fingerprint.js +106 -29
  6. package/src/js/fingerprint.utils.js +32 -5
  7. package/src/js/tests/fingerprint.test.js +91 -4
  8. package/src/js/tests/threatIntel.test.js +76 -0
  9. package/src/php/AutoTuner.php +36 -18
  10. package/src/php/Challenge/ChallengeUtils.php +107 -225
  11. package/src/php/DirectFingerprint.php +16 -4
  12. package/src/php/FingerprintClient.php +17 -19
  13. package/src/php/FingerprintEngine.php +114 -51
  14. package/src/php/Ja3AnomalyDetector.php +20 -12
  15. package/src/php/Optimization/Optimization.php +5 -1
  16. package/src/php/Optimization/OptimizationOperators.php +5 -1
  17. package/src/php/Optimization/ProblemInitializers.php +16 -4
  18. package/src/php/ProblemManager.php +20 -14
  19. package/src/php/Tests/FingerprintEngineTest.php +705 -662
  20. package/src/php/Tests/ThreatIntelTest.php +64 -0
  21. package/src/php/Utils/BlockList.php +99 -99
  22. package/src/php/Utils/Env.php +51 -49
  23. package/src/php/Utils/MaliciousPatterns.php +74 -74
  24. package/src/php/Utils/RequestUtils.php +22 -5
  25. package/src/php/WordPress/WpDbStore.php +4 -0
  26. package/src/php/WordPress/fingerprint-anti-bot.php +559 -0
  27. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
  28. package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +262 -0
  29. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
  30. package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +268 -0
  31. package/src/php/WordPress/package.php +102 -4
  32. package/src/php/WordPress/readme.txt +36 -0
  33. package/public/fingerprint-wordpress.zip +0 -0
  34. package/src/js/fingerprint.client.obfuscated.js +0 -1
  35. package/src/php/WordPress/fingerprint-wordpress.php +0 -217
@@ -129,7 +129,8 @@ class AutoTuner
129
129
  try {
130
130
  call_user_func($this->onCleanup, $removed);
131
131
  } catch (\Throwable $e) {
132
- error_log("[AutoTuning] Error in onCleanup callback: " . $e->getMessage());
132
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- AutoTuning system log
133
+ $this->logError("[AutoTuning] Error in onCleanup callback: " . $e->getMessage());
133
134
  }
134
135
  }
135
136
  }
@@ -155,24 +156,24 @@ class AutoTuner
155
156
 
156
157
  if (count($sanitizedData) < $this->minDataPoints || !$hasEnoughSignal) {
157
158
  if (count($sanitizedData) < $this->minDataPoints) {
158
- echo sprintf("[AutoTuning] Postponed: %d/%d data points collected.\n", count($sanitizedData), $this->minDataPoints);
159
+ $this->logMessage(sprintf("[AutoTuning] Postponed: %d/%d data points collected.\n", count($sanitizedData), $this->minDataPoints));
159
160
  } else {
160
- echo sprintf("[AutoTuning] Postponed: Insufficient confidence signals (Ratio: %.2f%% < %.2f%% and count: %d < %d).\n", $highConfidenceRatio * 100, $minConfidenceRatio * 100, $highConfidenceLogs, $minHighConfidenceCount);
161
+ $this->logMessage(sprintf("[AutoTuning] Postponed: Insufficient confidence signals (Ratio: %.2f%% < %.2f%% and count: %d < %d).\n", $highConfidenceRatio * 100, $minConfidenceRatio * 100, $highConfidenceLogs, $minHighConfidenceCount));
161
162
  }
162
163
  return;
163
164
  }
164
165
 
165
166
  if (count($this->trafficData) > $this->maxDataPoints) {
166
- echo sprintf("[AutoTuning] Traffic log reached %d entries (max: %d). Truncating oldest logs.\n", count($this->trafficData), $this->maxDataPoints);
167
+ $this->logMessage(sprintf("[AutoTuning] Traffic log reached %d entries (max: %d). Truncating oldest logs.\n", count($this->trafficData), $this->maxDataPoints));
167
168
  $this->trafficData = array_slice($this->trafficData, count($this->trafficData) - $this->maxDataPoints);
168
169
  }
169
170
 
170
- echo sprintf("[AutoTuning] Starting complete optimization cycle with %d sanitized data points.\n", count($sanitizedData));
171
+ $this->logMessage(sprintf("[AutoTuning] Starting complete optimization cycle with %d sanitized data points.\n", count($sanitizedData)));
171
172
 
172
173
  $paretoFront = OptimizationOperators::solveFullSecurityTuning(['trafficData' => $sanitizedData, 'currentConfig' => $this->securityConfig], []);
173
174
 
174
175
  if (empty($paretoFront)) {
175
- echo "[AutoTuning] Optimization returned no solutions.\n";
176
+ $this->logMessage("[AutoTuning] Optimization returned no solutions.\n");
176
177
  return;
177
178
  }
178
179
 
@@ -192,7 +193,7 @@ class AutoTuner
192
193
 
193
194
  $filteredFront = array_filter($paretoFront, fn ($p) => $isValidSecurityConfig($p['solution']));
194
195
  if (empty($filteredFront)) {
195
- echo "[AutoTuning] Warning: All Pareto solutions violated sanity guardrails. Restoring raw front.\n";
196
+ $this->logMessage("[AutoTuning] Warning: All Pareto solutions violated sanity guardrails. Restoring raw front.\n");
196
197
  $filteredFront = $paretoFront;
197
198
  } else {
198
199
  $filteredFront = array_values($filteredFront);
@@ -277,7 +278,8 @@ class AutoTuner
277
278
  $validationTolerance = $this->securityConfig['autotuning']['validationTolerance'] ?? $this->validationTolerance;
278
279
 
279
280
  if ($proposedFPR > $currentFPR + $validationTolerance || $proposedFNR > $currentFNR + $validationTolerance) {
280
- error_log(sprintf(
281
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- AutoTuning system alert
282
+ $this->logError(sprintf(
281
283
  "[AutoTuning] [SECURITY ALERT] Proposed configuration rejected due to instability/poisoning risk! Proposed FPR: %.4f (Current: %.4f), Proposed FNR: %.4f (Current: %.4f)",
282
284
  $proposedFPR, $currentFPR, $proposedFNR, $currentFNR
283
285
  ));
@@ -300,22 +302,23 @@ class AutoTuner
300
302
 
301
303
  self::$lastBestSolution = $bestSolution;
302
304
 
303
- echo "[AutoTuning] New optimized security configuration applied.\n";
304
- echo "[AutoTuning] Objectives achieved: " . json_encode([
305
+ $this->logMessage("[AutoTuning] New optimized security configuration applied.\n");
306
+ $this->logMessage("[AutoTuning] Objectives achieved: " . json_encode([
305
307
  'falsePositiveRate' => round($bestSolution['objectives'][0], 4),
306
308
  'falseNegativeRate' => round($bestSolution['objectives'][1], 4)
307
- ]) . "\n";
308
- echo "[AutoTuning] New thresholds: " . json_encode($this->securityConfig['thresholds']) . "\n";
309
- echo "[AutoTuning] New weights: " . json_encode($this->securityConfig['weights']) . "\n";
310
- echo "[AutoTuning] New patterns: " . json_encode($this->securityConfig['patterns']) . "\n";
309
+ ]) . "\n");
310
+ $this->logMessage("[AutoTuning] New thresholds: " . json_encode($this->securityConfig['thresholds']) . "\n");
311
+ $this->logMessage("[AutoTuning] New weights: " . json_encode($this->securityConfig['weights']) . "\n");
312
+ $this->logMessage("[AutoTuning] New patterns: " . json_encode($this->securityConfig['patterns']) . "\n");
311
313
 
312
314
  // Persist best configuration if savePath is configured
313
315
  if ($this->savePath !== null) {
314
316
  try {
315
317
  file_put_contents($this->savePath, json_encode($bestSolution['solution'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
316
- echo "[AutoTuning] Best configuration saved to: {$this->savePath}\n";
318
+ $this->logMessage("[AutoTuning] Best configuration saved to: {$this->savePath}\n");
317
319
  } catch (\Throwable $e) {
318
- error_log("[AutoTuning] Error saving optimized configuration: " . $e->getMessage());
320
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- AutoTuning system log
321
+ $this->logError("[AutoTuning] Error saving optimized configuration: " . $e->getMessage());
319
322
  }
320
323
  }
321
324
 
@@ -325,13 +328,28 @@ class AutoTuner
325
328
  try {
326
329
  call_user_func($this->onCleanup, $cleared);
327
330
  } catch (\Throwable $e) {
328
- error_log("[AutoTuning] Error in onCleanup callback after clearing: " . $e->getMessage());
331
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- AutoTuning system log
332
+ $this->logError("[AutoTuning] Error in onCleanup callback after clearing: " . $e->getMessage());
329
333
  }
330
334
  }
331
- echo sprintf("[AutoTuning] Explicitly cleared %d processed traffic data points.\n", count($cleared));
335
+ $this->logMessage(sprintf("[AutoTuning] Explicitly cleared %d processed traffic data points.\n", count($cleared)));
332
336
  }
333
337
  }
334
338
 
339
+ private function logMessage(string $message): void
340
+ {
341
+ if (PHP_SAPI === 'cli') {
342
+ // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- CLI message output
343
+ echo $message;
344
+ }
345
+ }
346
+
347
+ private function logError(string $message): void
348
+ {
349
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- AutoTuning system log
350
+ error_log($message);
351
+ }
352
+
335
353
  /**
336
354
  * Returns the latest best solution found by the auto-tuner.
337
355
  * @return array<string, mixed>|null
@@ -178,7 +178,8 @@ class ChallengeUtils
178
178
  $peers = $config['federatedPeers'] ?? [];
179
179
  if (!empty($peers)) {
180
180
  $allowedHosts = array_map(function ($url) {
181
- $host = parse_url($url, PHP_URL_HOST);
181
+ // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url
182
+ $host = function_exists('wp_parse_url') ? wp_parse_url($url, PHP_URL_HOST) : parse_url($url, PHP_URL_HOST);
182
183
  return !empty($host) ? $host : $url;
183
184
  }, $peers);
184
185
 
@@ -188,9 +189,9 @@ class ChallengeUtils
188
189
  }
189
190
  $zkpY = $params['zkpY'] ?? '';
190
191
  $headers = function_exists('getallheaders') ? array_change_key_case(getallheaders(), CASE_LOWER) : [];
191
- $sigEd25519 = $params['signature_ed25519'] ?? $headers['x-federation-signature-ed25519'] ?? $_SERVER['HTTP_X_FEDERATION_SIGNATURE_ED25519'] ?? '';
192
- $sigHmac = $params['signature'] ?? $headers['x-federation-signature'] ?? $_SERVER['HTTP_X_FEDERATION_SIGNATURE'] ?? '';
193
- $timestamp = (int)($params['timestamp'] ?? $headers['x-federation-timestamp'] ?? $_SERVER['HTTP_X_FEDERATION_TIMESTAMP'] ?? 0);
192
+ $sigEd25519 = $params['signature_ed25519'] ?? $headers['x-federation-signature-ed25519'] ?? self::getSanitizedServerVar('HTTP_X_FEDERATION_SIGNATURE_ED25519', '');
193
+ $sigHmac = $params['signature'] ?? $headers['x-federation-signature'] ?? self::getSanitizedServerVar('HTTP_X_FEDERATION_SIGNATURE', '');
194
+ $timestamp = (int)($params['timestamp'] ?? $headers['x-federation-timestamp'] ?? self::getSanitizedServerVar('HTTP_X_FEDERATION_TIMESTAMP', 0));
194
195
 
195
196
  if (empty($zkpY) || empty($timestamp)) {
196
197
  return ['error' => 'Missing threat intel parameters'];
@@ -205,7 +206,7 @@ class ChallengeUtils
205
206
  $msg = "{$timestamp}:{$zkpY}";
206
207
 
207
208
  if (!empty($sigEd25519)) {
208
- $publicKey = $config['ed25519_public_key'] ?? $_ENV['ED25519_PUBLIC_KEY'] ?? getenv('ED25519_PUBLIC_KEY');
209
+ $publicKey = $config['ed25519_public_key'] ?? Env::get('ED25519_PUBLIC_KEY');
209
210
  if (!$publicKey) {
210
211
  return ['error' => 'Missing public key for asymmetric verification'];
211
212
  }
@@ -298,8 +299,8 @@ class ChallengeUtils
298
299
 
299
300
  switch ($op) {
300
301
  case 'register':
301
- $clientIp = $_SERVER['REMOTE_ADDR'] ?? '127.0.0.1';
302
- $seed = $params['seed'] ?? '';
302
+ $clientIp = self::getSanitizedServerVar('REMOTE_ADDR', '127.0.0.1');
303
+ $seed = $params['seed'] ?? ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
303
304
  self::registerCooperativeNode($clientIp, $nodeId, $seed);
304
305
  return ['status' => 'registered'];
305
306
 
@@ -504,7 +505,7 @@ class ChallengeUtils
504
505
  return 'ed25519.' . $base64UrlEncode($serialized) . '.' . $base64UrlEncode($signature);
505
506
  }
506
507
  } catch (\Throwable $e) {
507
- error_log("[ChallengeUtils] Ed25519 signing failed: " . $e->getMessage());
508
+ self::logError("[ChallengeUtils] Ed25519 signing failed: " . $e->getMessage());
508
509
  }
509
510
  }
510
511
 
@@ -540,7 +541,7 @@ class ChallengeUtils
540
541
 
541
542
  $ed25519PubKey = Env::get('ED25519_PUBLIC_KEY');
542
543
  if (!$ed25519PubKey) {
543
- error_log("[ChallengeUtils] ED25519_PUBLIC_KEY is not defined in environment.");
544
+ self::logError("[ChallengeUtils] ED25519_PUBLIC_KEY is not defined in environment.");
544
545
  return null;
545
546
  }
546
547
 
@@ -551,7 +552,7 @@ class ChallengeUtils
551
552
  return null;
552
553
  }
553
554
  } catch (\Throwable $e) {
554
- error_log("[ChallengeUtils] Ed25519 verification failed: " . $e->getMessage());
555
+ self::logError("[ChallengeUtils] Ed25519 verification failed: " . $e->getMessage());
555
556
  return null;
556
557
  }
557
558
 
@@ -737,7 +738,7 @@ class ChallengeUtils
737
738
  ): ?string {
738
739
  // En mode HTTP (non sécurisé), aucun calcul SHA-256 n'est exigé
739
740
  if (!empty($challengeContext['isHttp'])) {
740
- error_log('[FP Server Verify] Mode HTTP détecté (insecure policy) : validation sans SHA-256 acceptée.');
741
+ self::logError('[FP Server Verify] Mode HTTP détecté (insecure policy) : validation sans SHA-256 acceptée.');
741
742
  $expiry = (int)floor(microtime(true) * 1000) + $ticketTtl;
742
743
  return self::generateStatelessTicket([
743
744
  'expiry' => $expiry,
@@ -751,7 +752,7 @@ class ChallengeUtils
751
752
  $baseBlock = $challengeContext['baseBlock'] ?? null;
752
753
 
753
754
  if ($cpuTargetHex === null || $baseBlock === null) {
754
- error_log('[FP Server Verify] Invalid challenge context. Missing cpuTarget or baseBlock.');
755
+ self::logError('[FP Server Verify] Invalid challenge context. Missing cpuTarget or baseBlock.');
755
756
  return null;
756
757
  }
757
758
 
@@ -763,7 +764,7 @@ class ChallengeUtils
763
764
  $isValid = strcmp($hash, $paddedTarget) < 0;
764
765
 
765
766
  if ($isValid) {
766
- error_log('[FP Server Verify] CPU PoW verification PASSED.');
767
+ self::logError('[FP Server Verify] CPU PoW verification PASSED.');
767
768
 
768
769
  $expiry = (int)floor(microtime(true) * 1000) + $ticketTtl;
769
770
  $payload = [
@@ -776,7 +777,7 @@ class ChallengeUtils
776
777
  }
777
778
 
778
779
  // Log details on failure
779
- error_log(sprintf(
780
+ self::logError(sprintf(
780
781
  '[FP Server Verify] CPU PoW verification FAILED. Details: hashCalculated=0x%s, target=0x%s',
781
782
  $hash,
782
783
  $cpuTargetHex
@@ -888,11 +889,11 @@ class ChallengeUtils
888
889
  }
889
890
  $maxAllowedMemDifficulty = 128; // 128MB
890
891
  if ($difficulty > $maxAllowedMemDifficulty) {
891
- error_log("[Security] Memory PoW verification attempt with excessive difficulty: {$difficulty}MB. Denied.");
892
+ self::logError("[Security] Memory PoW verification attempt with excessive difficulty: {$difficulty}MB. Denied.");
892
893
  return false;
893
894
  }
894
895
 
895
- if (empty($solution)) {
896
+ if ($solution === '') {
896
897
  return false;
897
898
  }
898
899
 
@@ -1020,7 +1021,7 @@ class ChallengeUtils
1020
1021
  dirname(__DIR__, 1) . '/assets/pow.solver.inline.js',
1021
1022
  ];
1022
1023
  if (defined('ABSPATH')) {
1023
- $wpPluginSolver = ABSPATH . 'wp-content/plugins/fingerprint-wordpress/assets/pow.solver.inline.js';
1024
+ $wpPluginSolver = ABSPATH . 'wp-content/plugins/fingerprint-anti-bot/assets/pow.solver.inline.js';
1024
1025
  if (file_exists($wpPluginSolver)) {
1025
1026
  return file_get_contents($wpPluginSolver) ?: '';
1026
1027
  }
@@ -1030,7 +1031,7 @@ class ChallengeUtils
1030
1031
  return file_get_contents($solverPath) ?: '';
1031
1032
  }
1032
1033
  }
1033
- error_log("[ChallengeUtils] Erreur: Le fichier pow.solver.inline.js n'a pas été trouvé à l'emplacement attendu.");
1034
+ self::logError("[ChallengeUtils] Erreur: Le fichier pow.solver.inline.js n'a pas été trouvé à l'emplacement attendu.");
1034
1035
  return '';
1035
1036
  }
1036
1037
 
@@ -1052,173 +1053,28 @@ class ChallengeUtils
1052
1053
  $safeNonce = json_encode($nonce, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
1053
1054
  $safeClientSecret = json_encode($clientSecret, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
1054
1055
 
1055
- $challengeScript = <<<JS
1056
- async function solve() {
1057
- const nonce = {$safeNonce};
1058
- const path = {$safePath};
1059
- const clientSecret = {$safeClientSecret};
1060
- const queries = {$queriesJson};
1061
- const sizeMb = {$sizeMb};
1062
- const nodeId = "{$nodeId}";
1063
- const peerId = "{$peerId}";
1064
- const peerBlockIdx = {$peerBlockIdx};
1065
- const coopTimeout = {$coopTimeout};
1066
-
1067
- async function signCoop(op, nid, extra = "") {
1068
- const msg = clientSecret + ":" + op + ":" + nid + (extra ? ":" + extra : "");
1069
- const encoder = new TextEncoder();
1070
- const data = encoder.encode(msg);
1071
- const hashBuffer = await crypto.subtle.digest("SHA-256", data);
1072
- return Array.from(new Uint8Array(hashBuffer)).map(b => b.toString(16).padStart(2, '0')).join('');
1073
- }
1074
-
1075
- async function sendWebRtcSignal(targetId, type, data) {
1076
- const sig = await signCoop("webrtc_signal", nodeId, targetId + ":" + type + ":" + data);
1077
- await fetch(window.location.pathname + "?coop_op=webrtc_signal&node_id=" + nodeId + "&target_peer_id=" + targetId + "&signal_type=" + type + "&signal_data=" + encodeURIComponent(data) + "&coop_sig=" + sig);
1078
- }
1079
-
1080
- document.getElementById('loader').innerText = '⚙️ Checking persistent local storage...';
1081
- await new Promise(r => setTimeout(r, 10));
1082
-
1083
- try {
1084
- await window.initializeSpace(nonce + ":" + clientSecret, sizeMb);
1085
-
1086
- if (peerId && peerBlockIdx !== -1) {
1087
- // Enregistrement coopératif
1088
- const sig = await signCoop("register", nodeId, nonce + ":" + clientSecret);
1089
- await fetch(window.location.pathname + "?coop_op=register&node_id=" + nodeId + "&seed=" + encodeURIComponent(nonce + ":" + clientSecret) + "&coop_sig=" + sig);
1090
- }
1091
-
1092
- const peerConnections = {};
1093
-
1094
- // Écoute des signaux WebRTC et requêtes entrantes
1095
- setInterval(async () => {
1096
- try {
1097
- const sigWebrtc = await signCoop("poll_signals", nodeId);
1098
- const resWebrtc = await fetch(window.location.pathname + "?coop_op=poll_signals&node_id=" + nodeId + "&coop_sig=" + sigWebrtc);
1099
- const dataWebrtc = await resWebrtc.json();
1100
- if (dataWebrtc.signals && dataWebrtc.signals.length > 0) {
1101
- for (const sig of dataWebrtc.signals) {
1102
- const fromId = sig.from_peer_id;
1103
- if (sig.signal_type === 'offer') {
1104
- const pc = new RTCPeerConnection({ iceServers: [] });
1105
- peerConnections[fromId] = pc;
1106
- pc.onicecandidate = (e) => {
1107
- if (e.candidate) sendWebRtcSignal(fromId, 'candidate', JSON.stringify(e.candidate));
1108
- };
1109
- pc.ondatachannel = (e) => {
1110
- const dc = e.channel;
1111
- dc.onmessage = async (evt) => {
1112
- try {
1113
- const req = JSON.parse(evt.data);
1114
- if (req.type === 'get_block') {
1115
- document.getElementById('loader').innerText = '📤 Transfert direct P2P (WebRTC) du bloc vers le pair...';
1116
- const blockData = await window.readSpaceBlock(req.block_idx);
1117
- dc.send(JSON.stringify({ type: 'block_data', block_data: blockData }));
1118
- }
1119
- } catch (err) {}
1120
- };
1121
- };
1122
- await pc.setRemoteDescription(new RTCSessionDescription(JSON.parse(sig.signal_data)));
1123
- const answer = await pc.createAnswer();
1124
- await pc.setLocalDescription(answer);
1125
- await sendWebRtcSignal(fromId, 'answer', JSON.stringify(answer));
1126
- } else if (sig.signal_type === 'answer' && peerConnections[fromId]) {
1127
- await peerConnections[fromId].setRemoteDescription(new RTCSessionDescription(JSON.parse(sig.signal_data)));
1128
- } else if (sig.signal_type === 'candidate' && peerConnections[fromId]) {
1129
- await peerConnections[fromId].addIceCandidate(new RTCIceCandidate(JSON.parse(sig.signal_data)));
1130
- }
1131
- }
1132
- }
1133
- } catch (e) {}
1134
-
1135
- try {
1136
- const sig = await signCoop("poll_requests", nodeId);
1137
- const res = await fetch(window.location.pathname + "?coop_op=poll_requests&node_id=" + nodeId + "&coop_sig=" + sig);
1138
- const data = await res.json();
1139
- if (data.requests && data.requests.length > 0) {
1140
- for (const req of data.requests) {
1141
- document.getElementById('loader').innerText = '📤 Transfert coopératif de bloc vers le pair...';
1142
- const blockData = await window.readSpaceBlock(req.block_idx);
1143
- const respSig = await signCoop("respond_block", nodeId, req.requester_id + ":" + req.req_id + ":" + blockData);
1144
- await fetch(window.location.pathname + "?coop_op=respond_block&node_id=" + nodeId + "&requester_id=" + req.requester_id + "&req_id=" + req.req_id + "&block_data=" + encodeURIComponent(blockData) + "&coop_sig=" + respSig);
1145
- }
1146
- }
1147
- } catch (e) {
1148
- console.error("Cooperative polling error", e);
1149
- }
1150
- }, 800);
1151
-
1152
- let peerBlock = "";
1153
- if (peerId && peerBlockIdx !== -1) {
1154
- document.getElementById('loader').innerText = '📥 Connexion WebRTC P2P directe au pair (' + peerId + ')...';
1155
-
1156
- // 1. Échange direct P2P via WebRTC DataChannel (charge serveur = 0)
1157
- const webrtcTransferPromise = new Promise(async (resolve) => {
1158
- if (!window.RTCPeerConnection) return resolve(null);
1159
- try {
1160
- const pc = new RTCPeerConnection({ iceServers: [] });
1161
- peerConnections[peerId] = pc;
1162
- const dc = pc.createDataChannel("pospace-transfer");
1163
- pc.onicecandidate = (e) => {
1164
- if (e.candidate) sendWebRtcSignal(peerId, 'candidate', JSON.stringify(e.candidate));
1165
- };
1166
- dc.onopen = () => {
1167
- dc.send(JSON.stringify({ type: 'get_block', block_idx: peerBlockIdx }));
1168
- };
1169
- dc.onmessage = (e) => {
1170
- try {
1171
- const msg = JSON.parse(e.data);
1172
- if (msg.type === 'block_data' && msg.block_data) {
1173
- resolve(msg.block_data);
1174
- }
1175
- } catch (err) {}
1176
- };
1177
- const offer = await pc.createOffer();
1178
- await pc.setLocalDescription(offer);
1179
- await sendWebRtcSignal(peerId, 'offer', JSON.stringify(offer));
1180
- } catch (err) {
1181
- resolve(null);
1182
- }
1183
- });
1184
-
1185
- const webrtcTimeoutPromise = new Promise((resolve) => setTimeout(() => resolve(null), 5000));
1186
- peerBlock = await Promise.race([webrtcTransferPromise, webrtcTimeoutPromise]);
1187
-
1188
- if (peerBlock) {
1189
- document.getElementById('loader').innerText = '⚡ Bloc reçu en direct via WebRTC P2P sans transit serveur !';
1190
- } else {
1191
- // 2. Repli vers le relais HTTP si WebRTC échoue
1192
- document.getElementById('loader').innerText = '⚠️ WebRTC indisponible. Téléchargement via relais HTTP...';
1193
- const reqId = Math.random().toString(36).substring(2);
1194
- const reqSig = await signCoop("request_peer_block", nodeId, peerId + ":" + peerBlockIdx + ":" + reqId);
1195
- await fetch(window.location.pathname + "?coop_op=request_peer_block&node_id=" + nodeId + "&peer_id=" + peerId + "&block_idx=" + peerBlockIdx + "&req_id=" + reqId + "&coop_sig=" + reqSig);
1196
-
1197
- let attempts = 0;
1198
- while (attempts < coopTimeout) {
1199
- const pollSig = await signCoop("poll_response", nodeId, reqId);
1200
- const res = await fetch(window.location.pathname + "?coop_op=poll_response&node_id=" + nodeId + "&req_id=" + reqId + "&coop_sig=" + pollSig);
1201
- const data = await res.json();
1202
- if (data.status === 'ready') {
1203
- peerBlock = data.block_data;
1204
- break;
1205
- }
1206
- await new Promise(r => setTimeout(r, 1000));
1207
- attempts++;
1208
- }
1209
- }
1210
- }
1211
-
1212
- document.getElementById('loader').innerText = '⚙️ Génération de la Preuve d\\'Espace...';
1213
- const hash = await window.solveSpaceChallenge(nonce + ":" + clientSecret, queries, nonce, clientSecret, peerBlock);
1214
-
1215
- window.location.href = path + "?pow_type=pospace&pow_nonce=" + nonce + "&pow_solution_space=" + hash + (peerBlock ? "&pow_coop=1" : "");
1216
- } catch(e) {
1217
- document.getElementById('loader').innerText = "Error initializing local storage: " + e.message;
1218
- }
1219
- }
1220
- solve();
1221
- JS;
1056
+ $challengeScript = 'async function solve() {' . "\n"
1057
+ . ' const nonce = ' . $safeNonce . ";\n"
1058
+ . ' const path = ' . $safePath . ";\n"
1059
+ . ' const clientSecret = ' . $safeClientSecret . ";\n"
1060
+ . ' const queries = ' . $queriesJson . ";\n"
1061
+ . ' const sizeMb = ' . $sizeMb . ";\n"
1062
+ . ' const nodeId = "' . $nodeId . "\";\n"
1063
+ . ' const peerId = "' . $peerId . "\";\n"
1064
+ . ' const peerBlockIdx = ' . $peerBlockIdx . ";\n"
1065
+ . ' const coopTimeout = ' . $coopTimeout . ";\n"
1066
+ . ' document.getElementById("loader").innerText = "⚙️ Checking persistent local storage...";' . "\n"
1067
+ . ' await new Promise(r => setTimeout(r, 10));' . "\n"
1068
+ . ' try {' . "\n"
1069
+ . ' await window.initializeSpace(nonce + ":" + clientSecret, sizeMb);' . "\n"
1070
+ . ' document.getElementById("loader").innerText = "⚙️ Proof of Space generation...";' . "\n"
1071
+ . ' const hash = await window.solveSpaceChallenge(nonce + ":" + clientSecret, queries, nonce, clientSecret, "");' . "\n"
1072
+ . ' window.location.href = path + "?pow_type=pospace&pow_nonce=" + nonce + "&pow_solution_space=" + hash;' . "\n"
1073
+ . ' } catch(e) {' . "\n"
1074
+ . ' document.getElementById("loader").innerText = "Error initializing local storage: " + e.message;' . "\n"
1075
+ . ' }' . "\n"
1076
+ . '}' . "\n"
1077
+ . 'solve();';
1222
1078
 
1223
1079
  return "<html><head><title>Security Check</title></head><body style=\"font-family:sans-serif; text-align:center; padding-top:50px;\"><h1>Security Check (Level 2)</h1><p>We are verifying your storage allocation. This may take a few seconds on first load.</p><div id=\"loader\" style=\"margin:20px;\">⚙️ Initializing storage space...</div><script>{$solverCode}</script><script>{$challengeScript}</script></body></html>";
1224
1080
  }
@@ -1267,7 +1123,7 @@ JS;
1267
1123
 
1268
1124
  $trapLinks = [];
1269
1125
  foreach ($trapUrls as $index => $url) {
1270
- $nestingType = rand(0, 2);
1126
+ $nestingType = function_exists('wp_rand') ? wp_rand(0, 2) : random_int(0, 2);
1271
1127
  $innerHtml = ($nestingType === 1) ? "<b>&gt; " . ($index + 1) . "</b>" : (($nestingType === 2) ? "<i>&gt; " . ($index + 1) . "</i>" : "<span>&gt; " . ($index + 1) . "</span>");
1272
1128
  $trapLinks[] = "<a href=\"{$url}\" tabindex=\"-1\">{$innerHtml}</a>";
1273
1129
  }
@@ -1279,45 +1135,38 @@ JS;
1279
1135
  $safeClientSecret = json_encode($clientSecret, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_UNESCAPED_SLASHES);
1280
1136
  $isHttpJs = $isHttp ? 'true' : 'false';
1281
1137
 
1282
- $challengeScript = <<<JS
1283
- async function solve() {
1284
- const nonce = {$safeNonce};
1285
- const path = {$safePath};
1286
- const clientSecret = {$safeClientSecret};
1287
- const isHttp = {$isHttpJs};
1288
- const cpuTarget = BigInt("0x" + "{$target}");
1289
- const memDifficulty = {$memoryDifficulty};
1290
- const baseBlock = new Uint8Array({$baseBlockBytes});
1291
-
1292
- if (isHttp) {
1293
- document.getElementById('loader').innerText = '⚠️ Connexion HTTP non sécurisée : validation simplifiée en cours...';
1294
- await new Promise(r => setTimeout(r, 150));
1295
- const finalUrl = path + "?pow_type=cpu_mem&pow_nonce=" + nonce + "&pow_solution_cpu=http_simulacre_ack&pow_solution_mem=0";
1296
- window.location.href = finalUrl;
1297
- return;
1298
- }
1299
-
1300
- document.getElementById('loader').innerText = '⚙️ Performing CPU security calculation...';
1301
- const cpuSolution = await window.solveCpuChallengeInline(baseBlock, cpuTarget, (progress) => {});
1302
-
1303
- if (memDifficulty > 0) {
1304
- document.getElementById('loader').innerText = '⚙️ Performing memory allocation and calculation... (' + memDifficulty + ' MB)';
1305
- await new Promise(r => setTimeout(r, 10));
1306
- }
1307
- let memSolution = 0;
1308
- try {
1309
- const memSeed = ":" + nonce + ":" + clientSecret;
1310
- memSolution = await window.solveMemoryChallenge(memSeed, memDifficulty);
1311
- } catch(e) {
1312
- document.getElementById('loader').innerText = "Error: Insufficient memory. Please refresh.";
1313
- return;
1314
- }
1315
-
1316
- const finalUrl = path + "?pow_type=cpu_mem&pow_nonce=" + nonce + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + encodeURIComponent(JSON.stringify(memSolution));
1317
- window.location.href = finalUrl;
1318
- }
1319
- solve();
1320
- JS;
1138
+ $challengeScript = 'async function solve() {' . "\n"
1139
+ . ' const nonce = ' . $safeNonce . ";\n"
1140
+ . ' const path = ' . $safePath . ";\n"
1141
+ . ' const clientSecret = ' . $safeClientSecret . ";\n"
1142
+ . ' const isHttp = ' . $isHttpJs . ";\n"
1143
+ . ' const cpuTarget = BigInt("0x" + "' . $target . "\");\n"
1144
+ . ' const memDifficulty = ' . $memoryDifficulty . ";\n"
1145
+ . ' const baseBlock = new Uint8Array(' . $baseBlockBytes . ");\n"
1146
+ . ' if (isHttp) {' . "\n"
1147
+ . ' document.getElementById("loader").innerText = "HTTP connection: quick acknowledgment...";' . "\n"
1148
+ . ' await new Promise(r => setTimeout(r, 150));' . "\n"
1149
+ . ' window.location.href = path + "?pow_type=cpu_mem&pow_nonce=" + nonce + "&pow_solution_cpu=http_simulacre_ack&pow_solution_mem=0";' . "\n"
1150
+ . ' return;' . "\n"
1151
+ . ' }' . "\n"
1152
+ . ' document.getElementById("loader").innerText = "⚙️ Performing CPU security calculation...";' . "\n"
1153
+ . ' const cpuSolution = await window.solveCpuChallengeInline(baseBlock, cpuTarget, (progress) => {});' . "\n"
1154
+ . ' if (memDifficulty > 0) {' . "\n"
1155
+ . ' document.getElementById("loader").innerText = "⚙️ Performing memory allocation and calculation... (" + memDifficulty + " MB)";' . "\n"
1156
+ . ' await new Promise(r => setTimeout(r, 10));' . "\n"
1157
+ . ' }' . "\n"
1158
+ . ' let memSolution = 0;' . "\n"
1159
+ . ' try {' . "\n"
1160
+ . ' const memSeed = ":" + nonce + ":" + clientSecret;' . "\n"
1161
+ . ' memSolution = await window.solveMemoryChallenge(memSeed, memDifficulty);' . "\n"
1162
+ . ' } catch(e) {' . "\n"
1163
+ . ' document.getElementById("loader").innerText = "Error: Insufficient memory. Please refresh.";' . "\n"
1164
+ . ' return;' . "\n"
1165
+ . ' }' . "\n"
1166
+ . ' const finalUrl = path + "?pow_type=cpu_mem&pow_nonce=" + nonce + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + encodeURIComponent(JSON.stringify(memSolution));' . "\n"
1167
+ . ' window.location.href = finalUrl;' . "\n"
1168
+ . '}' . "\n"
1169
+ . 'solve();';
1321
1170
 
1322
1171
  $htmlTemplate = '<html><head><title>Advanced Security Check</title></head><body style="font-family:sans-serif; text-align:center; padding-top:50px;"><h1>Enhanced Verification... (Level 2)</h1><p>Your activity requires an additional security check. This may take a few moments.</p><div id="loader" style="margin:20px;">⚙️ Initializing combined verification...</div><script><!-- FINGERPRINT_SOLVER_SCRIPT --></script><script><!-- FINGERPRINT_CHALLENGE_SCRIPT --></script><!-- FINGERPRINT_TRAPS --></body></html>';
1323
1172
  $customTemplatePath = $securityConfig['challengePagePath'] ?? null;
@@ -1332,4 +1181,37 @@ JS;
1332
1181
  $htmlTemplate
1333
1182
  );
1334
1183
  }
1184
+
1185
+ private static function logError(string $message): void
1186
+ {
1187
+ // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- Challenge diagnostic logging
1188
+ error_log($message);
1189
+ }
1190
+
1191
+ /**
1192
+ * Safely retrieves and sanitizes a value from the $_SERVER superglobal.
1193
+ * Uses WordPress functions if available, otherwise falls back to basic PHP sanitization.
1194
+ *
1195
+ * @param string $key The key to retrieve from $_SERVER.
1196
+ * @param mixed $default The default value to return if the key is not found.
1197
+ * @return mixed The sanitized value.
1198
+ */
1199
+ private static function getSanitizedServerVar(string $key, $default = '')
1200
+ {
1201
+ if (!isset($_SERVER[$key])) {
1202
+ return $default;
1203
+ }
1204
+
1205
+ // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Unslashed and sanitized immediately below
1206
+ $value = $_SERVER[$key];
1207
+
1208
+ if (function_exists('wp_unslash')) {
1209
+ $value = wp_unslash($value);
1210
+ }
1211
+ if (function_exists('sanitize_text_field')) {
1212
+ return sanitize_text_field($value);
1213
+ }
1214
+
1215
+ return is_scalar($value) ? (string) $value : '';
1216
+ }
1335
1217
  }