@anonympins/fingerprint 0.7.2 → 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 +22 -9
- package/README.md +1 -1
- package/package.json +1 -1
- package/public/fingerprint-anti-bot.zip +0 -0
- package/src/js/fingerprint.js +68 -23
- package/src/js/fingerprint.utils.js +32 -5
- package/src/js/tests/fingerprint.test.js +1 -1
- package/src/js/tests/threatIntel.test.js +76 -0
- package/src/php/AutoTuner.php +36 -18
- package/src/php/Challenge/ChallengeUtils.php +106 -224
- package/src/php/DirectFingerprint.php +16 -4
- package/src/php/FingerprintClient.php +17 -19
- package/src/php/FingerprintEngine.php +77 -39
- 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 +6 -1
- package/src/php/Tests/ThreatIntelTest.php +64 -0
- 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-wordpress.php → fingerprint-anti-bot.php} +131 -126
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +1 -1
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +1 -1
- package/src/php/WordPress/package.php +6 -3
- package/src/php/WordPress/readme.txt +36 -0
- package/public/fingerprint-wordpress.zip +0 -0
- package/src/js/fingerprint.client.obfuscated.js +0 -1
|
@@ -178,7 +178,8 @@ class ChallengeUtils
|
|
|
178
178
|
$peers = $config['federatedPeers'] ?? [];
|
|
179
179
|
if (!empty($peers)) {
|
|
180
180
|
$allowedHosts = array_map(function ($url) {
|
|
181
|
-
|
|
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'] ??
|
|
192
|
-
$sigHmac = $params['signature'] ?? $headers['x-federation-signature'] ??
|
|
193
|
-
$timestamp = (int)($params['timestamp'] ?? $headers['x-federation-timestamp'] ??
|
|
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'] ??
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,7 +889,7 @@ class ChallengeUtils
|
|
|
888
889
|
}
|
|
889
890
|
$maxAllowedMemDifficulty = 128; // 128MB
|
|
890
891
|
if ($difficulty > $maxAllowedMemDifficulty) {
|
|
891
|
-
|
|
892
|
+
self::logError("[Security] Memory PoW verification attempt with excessive difficulty: {$difficulty}MB. Denied.");
|
|
892
893
|
return false;
|
|
893
894
|
}
|
|
894
895
|
|
|
@@ -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-
|
|
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
|
-
|
|
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 =
|
|
1056
|
-
|
|
1057
|
-
const
|
|
1058
|
-
const
|
|
1059
|
-
const
|
|
1060
|
-
const
|
|
1061
|
-
const
|
|
1062
|
-
const
|
|
1063
|
-
const
|
|
1064
|
-
const
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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 =
|
|
1126
|
+
$nestingType = function_exists('wp_rand') ? wp_rand(0, 2) : random_int(0, 2);
|
|
1271
1127
|
$innerHtml = ($nestingType === 1) ? "<b>> " . ($index + 1) . "</b>" : (($nestingType === 2) ? "<i>> " . ($index + 1) . "</i>" : "<span>> " . ($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 =
|
|
1283
|
-
|
|
1284
|
-
const
|
|
1285
|
-
const
|
|
1286
|
-
const
|
|
1287
|
-
const
|
|
1288
|
-
const
|
|
1289
|
-
const
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
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
|
}
|
|
@@ -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
|
}
|