@anonympins/fingerprint 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/README.md +1 -1
- package/package.json +4 -2
- package/public/anonympins-bot-mitigation-pow.zip +0 -0
- package/src/js/asn-lookup.js +223 -0
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +214 -29
- package/src/js/tests/asn-lookup.test.js +57 -0
- package/src/php/AsnLookupEngine.php +173 -0
- package/src/php/Challenge/ChallengeUtils.php +162 -64
- package/src/php/DirectFingerprint.php +25 -5
- package/src/php/FingerprintEngine.php +126 -30
- package/src/php/NetworkProfile.php +68 -0
- package/src/php/ProblemManager.php +12 -2
- package/src/php/Tests/AsnLookupTest.php +61 -0
- package/src/php/Tests/FingerprintEngineTest.php +4 -2
- package/src/php/Utils/Env.php +7 -2
- package/src/php/Utils/RequestUtils.php +24 -6
- package/src/php/WordPress/WpDbStore.php +27 -0
- package/src/php/WordPress/anonympins-bot-mitigation-pow.php +303 -64
- package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.po +1 -1
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +1 -1
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +1 -1
- package/src/php/WordPress/package.php +10 -0
- package/src/php/WordPress/readme.txt +7 -1
|
@@ -236,6 +236,31 @@ class ChallengeUtils
|
|
|
236
236
|
return $activePeers[array_rand($activePeers)];
|
|
237
237
|
}
|
|
238
238
|
|
|
239
|
+
public static function verifyEd25519Signature(string $message, string $signatureHex, array $config = []): bool
|
|
240
|
+
{
|
|
241
|
+
if (empty($message) || empty($signatureHex)) {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
$publicKey = $config['ed25519_public_key'] ?? Env::get('ED25519_PUBLIC_KEY');
|
|
245
|
+
if (empty($publicKey)) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
$cleanKey = str_replace('\n', "\n", $publicKey);
|
|
250
|
+
$pubKeyObj = openssl_pkey_get_public($cleanKey);
|
|
251
|
+
if (!$pubKeyObj) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
$sigBytes = hex2bin($signatureHex);
|
|
255
|
+
if ($sigBytes === false) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
return openssl_verify($message, $sigBytes, $pubKeyObj, null) === 1;
|
|
259
|
+
} catch (\Throwable $e) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
239
264
|
/**
|
|
240
265
|
* Handles cooperative peer-to-peer operations (federation threat intel sharing,
|
|
241
266
|
* node registration, peer discovery, WebRTC signaling, block requests/responses).
|
|
@@ -261,7 +286,7 @@ class ChallengeUtils
|
|
|
261
286
|
$peers = $config['federatedPeers'] ?? [];
|
|
262
287
|
if (!empty($peers)) {
|
|
263
288
|
$allowedHosts = array_map(function ($url) {
|
|
264
|
-
// phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url
|
|
289
|
+
// phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- Fallback for standalone PHP environments
|
|
265
290
|
$host = function_exists('wp_parse_url') ? wp_parse_url($url, PHP_URL_HOST) : parse_url($url, PHP_URL_HOST);
|
|
266
291
|
return !empty($host) ? $host : $url;
|
|
267
292
|
}, $peers);
|
|
@@ -289,18 +314,8 @@ class ChallengeUtils
|
|
|
289
314
|
$msg = "{$timestamp}:{$zkpY}";
|
|
290
315
|
|
|
291
316
|
if (!empty($sigEd25519)) {
|
|
292
|
-
$
|
|
293
|
-
|
|
294
|
-
return ['error' => 'Missing public key for asymmetric verification'];
|
|
295
|
-
}
|
|
296
|
-
try {
|
|
297
|
-
$cleanKey = str_replace('\n', "\n", $publicKey);
|
|
298
|
-
$pubKeyObj = openssl_pkey_get_public($cleanKey);
|
|
299
|
-
if (!$pubKeyObj || openssl_verify($msg, hex2bin($sigEd25519), $pubKeyObj, null) !== 1) {
|
|
300
|
-
return ['error' => 'Invalid asymmetric federation signature'];
|
|
301
|
-
}
|
|
302
|
-
} catch (\Throwable $e) {
|
|
303
|
-
return ['error' => 'Asymmetric signature verification failed'];
|
|
317
|
+
if (!self::verifyEd25519Signature($msg, $sigEd25519, $config)) {
|
|
318
|
+
return ['error' => 'Invalid asymmetric federation signature'];
|
|
304
319
|
}
|
|
305
320
|
} elseif (!empty($sigHmac)) {
|
|
306
321
|
$secret = $params['federationSecret'] ?? $config['federationSecret'] ?? self::getPowSecret();
|
|
@@ -330,6 +345,51 @@ class ChallengeUtils
|
|
|
330
345
|
return ['status' => 'synchronized', 'banned' => false, 'reportsCount' => count($reportedPeers)];
|
|
331
346
|
}
|
|
332
347
|
|
|
348
|
+
if ($op === 'share_whitelist') {
|
|
349
|
+
$peers = $config['federatedPeers'] ?? [];
|
|
350
|
+
if (!empty($peers)) {
|
|
351
|
+
$allowedHosts = array_map(function ($url) {
|
|
352
|
+
// phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- Fallback for standalone PHP environments
|
|
353
|
+
$host = function_exists('wp_parse_url') ? wp_parse_url($url, PHP_URL_HOST) : parse_url($url, PHP_URL_HOST);
|
|
354
|
+
return !empty($host) ? $host : $url;
|
|
355
|
+
}, $peers);
|
|
356
|
+
|
|
357
|
+
if (!in_array($clientIp, $allowedHosts, true)) {
|
|
358
|
+
return ['error' => 'Unauthorized federation sender IP'];
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
$entry = $params['entry'] ?? '';
|
|
363
|
+
$entryType = $params['entry_type'] ?? 'ip';
|
|
364
|
+
$headers = function_exists('getallheaders') ? array_change_key_case(getallheaders(), CASE_LOWER) : [];
|
|
365
|
+
$sigEd25519 = $params['signature_ed25519'] ?? $headers['x-federation-signature-ed25519'] ?? self::getSanitizedServerVar('HTTP_X_FEDERATION_SIGNATURE_ED25519', '');
|
|
366
|
+
$timestamp = (int)($params['timestamp'] ?? $headers['x-federation-timestamp'] ?? self::getSanitizedServerVar('HTTP_X_FEDERATION_TIMESTAMP', 0));
|
|
367
|
+
$ttl = (int)($params['ttl'] ?? 86400);
|
|
368
|
+
|
|
369
|
+
$cleanEntry = trim($entry);
|
|
370
|
+
if ($cleanEntry === '' || empty($sigEd25519) || empty($timestamp)) {
|
|
371
|
+
return ['error' => 'Missing required parameters for share_whitelist'];
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if ($cleanEntry === '*' || $cleanEntry === '0.0.0.0/0' || $cleanEntry === '::/0') {
|
|
375
|
+
return ['error' => 'Permissive wildcard entries are prohibited'];
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
$now = (int)(microtime(true) * 1000);
|
|
379
|
+
if (abs($now - $timestamp) > 300000) {
|
|
380
|
+
return ['error' => 'Timestamp expired or clock skew too high'];
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
$msg = "{$timestamp}:whitelist:{$entryType}:{$cleanEntry}";
|
|
384
|
+
if (!self::verifyEd25519Signature($msg, $sigEd25519, $config)) {
|
|
385
|
+
return ['error' => 'Invalid Ed25519 signature for whitelist synchronization'];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
$boundedTtl = min(604800, max(60, $ttl));
|
|
389
|
+
$store->set("federated-whitelist:{$entryType}:{$cleanEntry}", true, $boundedTtl);
|
|
390
|
+
return ['status' => 'whitelist_synchronized', 'entry' => $cleanEntry, 'entryType' => $entryType, 'ttl' => $boundedTtl];
|
|
391
|
+
}
|
|
392
|
+
|
|
333
393
|
$nodeId = $params['node_id'] ?? '';
|
|
334
394
|
if (empty($nodeId)) {
|
|
335
395
|
return ['error' => 'Missing node_id'];
|
|
@@ -353,19 +413,19 @@ class ChallengeUtils
|
|
|
353
413
|
$expectedMsg = "{$clientSecret}:find_peer:{$nodeId}";
|
|
354
414
|
break;
|
|
355
415
|
case 'webrtc_signal':
|
|
356
|
-
$expectedMsg = "{$clientSecret}:webrtc_signal:{$nodeId}:" . ($params['target_peer_id']
|
|
416
|
+
$expectedMsg = "{$clientSecret}:webrtc_signal:{$nodeId}:" . (isset($params['target_peer_id']) ? self::sanitizeString($params['target_peer_id']) : '') . ":" . (isset($params['signal_type']) ? self::sanitizeString($params['signal_type']) : '') . ":" . (isset($params['signal_data']) ? self::sanitizeString($params['signal_data']) : '');
|
|
357
417
|
break;
|
|
358
418
|
case 'poll_signals':
|
|
359
419
|
$expectedMsg = "{$clientSecret}:poll_signals:{$nodeId}";
|
|
360
420
|
break;
|
|
361
421
|
case 'request_peer_block':
|
|
362
|
-
$expectedMsg = "{$clientSecret}:request_peer_block:{$nodeId}:" . ($params['peer_id']
|
|
422
|
+
$expectedMsg = "{$clientSecret}:request_peer_block:{$nodeId}:" . (isset($params['peer_id']) ? self::sanitizeString($params['peer_id']) : '') . ":" . ($params['block_idx'] ?? '0') . ":" . (isset($params['req_id']) ? self::sanitizeString($params['req_id']) : '');
|
|
363
423
|
break;
|
|
364
424
|
case 'poll_requests':
|
|
365
425
|
$expectedMsg = "{$clientSecret}:poll_requests:{$nodeId}";
|
|
366
426
|
break;
|
|
367
427
|
case 'respond_block':
|
|
368
|
-
$expectedMsg = "{$clientSecret}:respond_block:{$nodeId}:" . ($params['requester_id']
|
|
428
|
+
$expectedMsg = "{$clientSecret}:respond_block:{$nodeId}:" . (isset($params['requester_id']) ? self::sanitizeString($params['requester_id']) : '') . ":" . (isset($params['req_id']) ? self::sanitizeString($params['req_id']) : '') . ":" . (isset($params['block_data']) ? self::sanitizeString($params['block_data']) : '');
|
|
369
429
|
break;
|
|
370
430
|
case 'poll_response':
|
|
371
431
|
$expectedMsg = "{$clientSecret}:poll_response:{$nodeId}:" . ($params['req_id'] ?? '');
|
|
@@ -383,7 +443,7 @@ class ChallengeUtils
|
|
|
383
443
|
switch ($op) {
|
|
384
444
|
case 'register':
|
|
385
445
|
$clientIp = self::getSanitizedServerVar('REMOTE_ADDR', '127.0.0.1');
|
|
386
|
-
$seed = $params['seed']
|
|
446
|
+
$seed = isset($params['seed']) ? self::sanitizeString($params['seed']) : '';
|
|
387
447
|
self::registerCooperativeNode($clientIp, $nodeId, $seed);
|
|
388
448
|
return ['status' => 'registered'];
|
|
389
449
|
|
|
@@ -395,9 +455,9 @@ class ChallengeUtils
|
|
|
395
455
|
return ['status' => 'no_peers'];
|
|
396
456
|
|
|
397
457
|
case 'webrtc_signal':
|
|
398
|
-
$targetPeerId = $params['target_peer_id']
|
|
399
|
-
$signalType = $params['signal_type']
|
|
400
|
-
$signalData = $params['signal_data']
|
|
458
|
+
$targetPeerId = isset($params['target_peer_id']) ? self::sanitizeString($params['target_peer_id']) : '';
|
|
459
|
+
$signalType = isset($params['signal_type']) ? self::sanitizeString($params['signal_type']) : '';
|
|
460
|
+
$signalData = isset($params['signal_data']) ? self::sanitizeString($params['signal_data']) : '';
|
|
401
461
|
if (empty($targetPeerId) || empty($signalType) || empty($signalData)) {
|
|
402
462
|
return ['error' => 'Invalid parameters'];
|
|
403
463
|
}
|
|
@@ -420,9 +480,9 @@ class ChallengeUtils
|
|
|
420
480
|
return ['status' => 'ok', 'signals' => $signals];
|
|
421
481
|
|
|
422
482
|
case 'request_peer_block':
|
|
423
|
-
$peerId = $params['peer_id']
|
|
483
|
+
$peerId = isset($params['peer_id']) ? self::sanitizeString($params['peer_id']) : '';
|
|
424
484
|
$blockIdx = (int)($params['block_idx'] ?? 0);
|
|
425
|
-
$requestId = $params['req_id']
|
|
485
|
+
$requestId = isset($params['req_id']) ? self::sanitizeString($params['req_id']) : '';
|
|
426
486
|
if (empty($peerId) || empty($requestId)) {
|
|
427
487
|
return ['error' => 'Invalid parameters'];
|
|
428
488
|
}
|
|
@@ -444,9 +504,9 @@ class ChallengeUtils
|
|
|
444
504
|
return ['requests' => $requests];
|
|
445
505
|
|
|
446
506
|
case 'respond_block':
|
|
447
|
-
$requesterId = $params['requester_id']
|
|
448
|
-
$requestId = $params['req_id']
|
|
449
|
-
$blockData = $params['block_data']
|
|
507
|
+
$requesterId = isset($params['requester_id']) ? self::sanitizeString($params['requester_id']) : '';
|
|
508
|
+
$requestId = isset($params['req_id']) ? self::sanitizeString($params['req_id']) : '';
|
|
509
|
+
$blockData = isset($params['block_data']) ? self::sanitizeString($params['block_data']) : '';
|
|
450
510
|
if (empty($requesterId) || empty($requestId)) {
|
|
451
511
|
return ['error' => 'Invalid parameters'];
|
|
452
512
|
}
|
|
@@ -456,7 +516,7 @@ class ChallengeUtils
|
|
|
456
516
|
return ['status' => 'delivered'];
|
|
457
517
|
|
|
458
518
|
case 'poll_response':
|
|
459
|
-
$requestId = $params['req_id']
|
|
519
|
+
$requestId = isset($params['req_id']) ? self::sanitizeString($params['req_id']) : '';
|
|
460
520
|
$responseKey = "coop-mailbox:res:{$nodeId}:{$requestId}";
|
|
461
521
|
$data = $store->get($responseKey);
|
|
462
522
|
if ($data) {
|
|
@@ -634,28 +694,44 @@ class ChallengeUtils
|
|
|
634
694
|
throw new \RuntimeException("Failed to extract Ed25519 public key.");
|
|
635
695
|
}
|
|
636
696
|
|
|
637
|
-
if ($outDir === '') {
|
|
638
|
-
$outDir = dirname(__DIR__, 2) . '/config';
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
if (!is_dir($outDir)) {
|
|
642
|
-
if (function_exists('wp_mkdir_p')) {
|
|
643
|
-
wp_mkdir_p($outDir);
|
|
644
|
-
} else {
|
|
645
|
-
mkdir($outDir, 0755, true);
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
|
|
649
697
|
$privName = $options['privateKeyName'] ?? 'issuer-private.pem';
|
|
650
698
|
$pubName = $options['publicKeyName'] ?? 'issuer-public.pem';
|
|
651
699
|
|
|
652
|
-
$privateKeyPath =
|
|
653
|
-
$publicKeyPath =
|
|
700
|
+
$privateKeyPath = '';
|
|
701
|
+
$publicKeyPath = '';
|
|
702
|
+
|
|
703
|
+
if ($outDir !== '') {
|
|
704
|
+
if (!is_dir($outDir)) {
|
|
705
|
+
if (function_exists('wp_mkdir_p')) {
|
|
706
|
+
wp_mkdir_p($outDir);
|
|
707
|
+
} else {
|
|
708
|
+
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir -- Standalone filesystem fallback
|
|
709
|
+
mkdir($outDir, 0755, true);
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
$privateKeyPath = rtrim($outDir, '/\\') . '/' . $privName;
|
|
713
|
+
$publicKeyPath = rtrim($outDir, '/\\') . '/' . $pubName;
|
|
714
|
+
|
|
715
|
+
global $wp_filesystem;
|
|
716
|
+
if (empty($wp_filesystem) && defined('ABSPATH')) {
|
|
717
|
+
require_once ABSPATH . 'wp-admin/includes/file.php';
|
|
718
|
+
WP_Filesystem();
|
|
719
|
+
}
|
|
654
720
|
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
721
|
+
if (!empty($wp_filesystem) && is_object($wp_filesystem)) {
|
|
722
|
+
$wp_filesystem->put_contents($privateKeyPath, $privateKeyPem, 0600);
|
|
723
|
+
$wp_filesystem->put_contents($publicKeyPath, $publicKeyPem, 0644);
|
|
724
|
+
} else {
|
|
725
|
+
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Standalone filesystem fallback
|
|
726
|
+
file_put_contents($privateKeyPath, $privateKeyPem);
|
|
727
|
+
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- Standalone filesystem fallback
|
|
728
|
+
@chmod($privateKeyPath, 0600);
|
|
729
|
+
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- Standalone filesystem fallback
|
|
730
|
+
file_put_contents($publicKeyPath, $publicKeyPem);
|
|
731
|
+
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- Standalone filesystem fallback
|
|
732
|
+
@chmod($publicKeyPath, 0644);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
659
735
|
|
|
660
736
|
return [
|
|
661
737
|
'privateKeyPath' => $privateKeyPath,
|
|
@@ -1294,20 +1370,25 @@ class ChallengeUtils
|
|
|
1294
1370
|
*/
|
|
1295
1371
|
private static function getPowSolverCode(): string
|
|
1296
1372
|
{
|
|
1373
|
+
if (defined('ANONYMPINS_BOT_MITIGATION_DIR')) {
|
|
1374
|
+
$wpPluginSolver = rtrim(ANONYMPINS_BOT_MITIGATION_DIR, '/\\') . '/assets/pow.solver.inline.js';
|
|
1375
|
+
if (file_exists($wpPluginSolver)) {
|
|
1376
|
+
return (string)file_get_contents($wpPluginSolver);
|
|
1377
|
+
}
|
|
1378
|
+
} elseif (defined('WP_PLUGIN_DIR')) {
|
|
1379
|
+
$wpPluginSolver = WP_PLUGIN_DIR . '/anonympins-bot-mitigation-pow/assets/pow.solver.inline.js';
|
|
1380
|
+
if (file_exists($wpPluginSolver)) {
|
|
1381
|
+
return (string)file_get_contents($wpPluginSolver);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1297
1385
|
$candidates = [
|
|
1298
1386
|
__DIR__ . '/../../js/pow.solver.inline.js',
|
|
1299
1387
|
__DIR__ . '/../assets/pow.solver.inline.js',
|
|
1300
1388
|
__DIR__ . '/../js/pow.solver.inline.js',
|
|
1301
|
-
dirname(__DIR__
|
|
1302
|
-
dirname(__DIR__
|
|
1303
|
-
dirname(__DIR__, 1) . '/assets/pow.solver.inline.js',
|
|
1389
|
+
dirname(__DIR__) . '/assets/pow.solver.inline.js',
|
|
1390
|
+
dirname(__DIR__) . '/js/pow.solver.inline.js',
|
|
1304
1391
|
];
|
|
1305
|
-
if (defined('ABSPATH')) {
|
|
1306
|
-
$wpPluginSolver = ABSPATH . 'wp-content/plugins/fingerprint-anti-bot/assets/pow.solver.inline.js';
|
|
1307
|
-
if (file_exists($wpPluginSolver)) {
|
|
1308
|
-
return file_get_contents($wpPluginSolver) ?: '';
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
1392
|
foreach ($candidates as $solverPath) {
|
|
1312
1393
|
if (file_exists($solverPath)) {
|
|
1313
1394
|
return file_get_contents($solverPath) ?: '';
|
|
@@ -1368,7 +1449,7 @@ class ChallengeUtils
|
|
|
1368
1449
|
. '}' . "\n"
|
|
1369
1450
|
. 'solve();';
|
|
1370
1451
|
|
|
1371
|
-
return "<html><head><title>Security
|
|
1452
|
+
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>";
|
|
1372
1453
|
}
|
|
1373
1454
|
|
|
1374
1455
|
/**
|
|
@@ -1467,7 +1548,7 @@ class ChallengeUtils
|
|
|
1467
1548
|
. '}' . "\n"
|
|
1468
1549
|
. 'solve();';
|
|
1469
1550
|
|
|
1470
|
-
$htmlTemplate = '<html><head><title>Advanced
|
|
1551
|
+
$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>';
|
|
1471
1552
|
$customTemplatePath = $securityConfig['challengePagePath'] ?? null;
|
|
1472
1553
|
|
|
1473
1554
|
if ($customTemplatePath && file_exists($customTemplatePath)) {
|
|
@@ -1493,6 +1574,31 @@ class ChallengeUtils
|
|
|
1493
1574
|
error_log($message);
|
|
1494
1575
|
}
|
|
1495
1576
|
|
|
1577
|
+
/**
|
|
1578
|
+
* Safely sanitizes a string input using WordPress functions if available, or fallback PHP functions.
|
|
1579
|
+
*
|
|
1580
|
+
* @param mixed $value
|
|
1581
|
+
* @return string
|
|
1582
|
+
*/
|
|
1583
|
+
private static function sanitizeString($value): string
|
|
1584
|
+
{
|
|
1585
|
+
if (!is_scalar($value)) {
|
|
1586
|
+
return '';
|
|
1587
|
+
}
|
|
1588
|
+
$val = (string)$value;
|
|
1589
|
+
if (function_exists('wp_unslash')) {
|
|
1590
|
+
$val = wp_unslash($val);
|
|
1591
|
+
}
|
|
1592
|
+
if (function_exists('sanitize_text_field')) {
|
|
1593
|
+
return sanitize_text_field($val);
|
|
1594
|
+
}
|
|
1595
|
+
if (function_exists('wp_strip_all_tags')) {
|
|
1596
|
+
return trim(wp_strip_all_tags($val));
|
|
1597
|
+
}
|
|
1598
|
+
// phpcs:ignore WordPress.WP.AlternativeFunctions.strip_tags_strip_tags -- Standalone fallback when running outside WordPress
|
|
1599
|
+
return trim(strip_tags($val));
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1496
1602
|
/**
|
|
1497
1603
|
* Safely retrieves and sanitizes a value from the $_SERVER superglobal.
|
|
1498
1604
|
* Uses WordPress functions if available, otherwise falls back to basic PHP sanitization.
|
|
@@ -1509,14 +1615,6 @@ class ChallengeUtils
|
|
|
1509
1615
|
|
|
1510
1616
|
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Unslashed and sanitized immediately below
|
|
1511
1617
|
$value = $_SERVER[$key];
|
|
1512
|
-
|
|
1513
|
-
if (function_exists('wp_unslash')) {
|
|
1514
|
-
$value = wp_unslash($value);
|
|
1515
|
-
}
|
|
1516
|
-
if (function_exists('sanitize_text_field')) {
|
|
1517
|
-
return sanitize_text_field($value);
|
|
1518
|
-
}
|
|
1519
|
-
|
|
1520
|
-
return is_scalar($value) ? (string) $value : '';
|
|
1618
|
+
return self::sanitizeString($value);
|
|
1521
1619
|
}
|
|
1522
1620
|
}
|
|
@@ -29,24 +29,44 @@ class DirectFingerprint
|
|
|
29
29
|
*/
|
|
30
30
|
public function buildRequestContext(): RequestContext
|
|
31
31
|
{
|
|
32
|
+
$sanitizeDeep = function ($data) use (&$sanitizeDeep) {
|
|
33
|
+
if (is_array($data)) {
|
|
34
|
+
$clean = [];
|
|
35
|
+
foreach ($data as $k => $v) {
|
|
36
|
+
$cleanKey = is_string($k) ? (function_exists('sanitize_text_field') && function_exists('wp_unslash') ? sanitize_text_field(wp_unslash($k)) : trim((string)$k)) : $k;
|
|
37
|
+
$clean[$cleanKey] = $sanitizeDeep($v);
|
|
38
|
+
}
|
|
39
|
+
return $clean;
|
|
40
|
+
}
|
|
41
|
+
if (is_string($data)) {
|
|
42
|
+
return function_exists('sanitize_text_field') && function_exists('wp_unslash') ? sanitize_text_field(wp_unslash($data)) : trim($data);
|
|
43
|
+
}
|
|
44
|
+
return $data;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
$rawInput = (string)file_get_contents('php://input');
|
|
48
|
+
$decodedJson = !empty($rawInput) ? json_decode($rawInput, true) : null;
|
|
32
49
|
// phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended -- Direct WAF inspection firewall before WP core logic
|
|
33
|
-
$body = $_POST
|
|
34
|
-
$
|
|
50
|
+
$body = !empty($_POST) ? $sanitizeDeep($_POST) : (is_array($decodedJson) ? $sanitizeDeep($decodedJson) : null);
|
|
51
|
+
$rawHeaders = function_exists('getallheaders') ? getallheaders() : [];
|
|
52
|
+
$headers = is_array($rawHeaders) ? $sanitizeDeep($rawHeaders) : [];
|
|
35
53
|
|
|
36
54
|
$remoteAddr = isset($_SERVER['REMOTE_ADDR']) ? sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'])) : '127.0.0.1';
|
|
37
55
|
$rawUri = isset($_SERVER['REQUEST_URI']) ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) : '/';
|
|
38
56
|
// phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- Uses wp_parse_url when available
|
|
39
57
|
$path = function_exists('wp_parse_url') ? (string)wp_parse_url($rawUri, PHP_URL_PATH) : (string)parse_url($rawUri, PHP_URL_PATH);
|
|
40
58
|
$serverProtocol = isset($_SERVER['SERVER_PROTOCOL']) ? sanitize_text_field(wp_unslash($_SERVER['SERVER_PROTOCOL'])) : '1.1';
|
|
59
|
+
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
|
60
|
+
$query = isset($_GET) && is_array($_GET) ? $sanitizeDeep($_GET) : [];
|
|
61
|
+
$cookies = isset($_COOKIE) && is_array($_COOKIE) ? $sanitizeDeep($_COOKIE) : [];
|
|
41
62
|
|
|
42
63
|
return new RequestContext(
|
|
43
64
|
$remoteAddr,
|
|
44
65
|
!empty($path) ? $path : '/',
|
|
45
66
|
$headers,
|
|
46
|
-
|
|
47
|
-
$_GET,
|
|
67
|
+
$query,
|
|
48
68
|
$body,
|
|
49
|
-
$
|
|
69
|
+
$cookies,
|
|
50
70
|
$serverProtocol
|
|
51
71
|
);
|
|
52
72
|
}
|