@anonympins/fingerprint 0.8.1 → 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 +29 -0
- package/README.md +1 -1
- package/package.json +3 -2
- package/public/anonympins-bot-mitigation-pow.zip +0 -0
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +166 -7
- package/src/php/Challenge/ChallengeUtils.php +100 -19
- package/src/php/FingerprintEngine.php +75 -0
- package/src/php/Utils/RequestUtils.php +3 -2
- package/src/php/WordPress/anonympins-bot-mitigation-pow.php +198 -60
- 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/readme.txt +2 -2
package/src/js/fingerprint.js
CHANGED
|
@@ -3885,9 +3885,27 @@ let store = inMemoryStore;
|
|
|
3885
3885
|
* @param {IStore} externalStore - An implementation of the IStore interface.
|
|
3886
3886
|
*/
|
|
3887
3887
|
export const configureStore = (externalStore) => {
|
|
3888
|
+
if (externalStore && typeof externalStore.has !== 'function' && typeof externalStore.get === 'function') {
|
|
3889
|
+
externalStore.has = async function (key) {
|
|
3890
|
+
const val = await this.get(key);
|
|
3891
|
+
return val !== null && val !== undefined;
|
|
3892
|
+
};
|
|
3893
|
+
}
|
|
3888
3894
|
store = externalStore;
|
|
3889
3895
|
};
|
|
3890
3896
|
|
|
3897
|
+
async function storeHas(key) {
|
|
3898
|
+
if (!store) return false;
|
|
3899
|
+
if (typeof store.has === 'function') {
|
|
3900
|
+
return await store.has(key);
|
|
3901
|
+
}
|
|
3902
|
+
if (typeof store.get === 'function') {
|
|
3903
|
+
const val = await store.get(key);
|
|
3904
|
+
return val !== null && val !== undefined;
|
|
3905
|
+
}
|
|
3906
|
+
return false;
|
|
3907
|
+
}
|
|
3908
|
+
|
|
3891
3909
|
/**
|
|
3892
3910
|
* Orchestrates request identification using a persistent anchor (cookie)
|
|
3893
3911
|
* and fingerprint verification.
|
|
@@ -4706,7 +4724,7 @@ async function getThreatIntelScore(context, zkpY, threatIntelConfig = {}) {
|
|
|
4706
4724
|
|
|
4707
4725
|
// 1. Détection déterministe : Clé publique ZKP bannie par consensus fédéré
|
|
4708
4726
|
if (zkpY) {
|
|
4709
|
-
const isBanned = await
|
|
4727
|
+
const isBanned = await storeHas(`banned-zkp-y:${zkpY}`);
|
|
4710
4728
|
if (isBanned) {
|
|
4711
4729
|
score = 100.0;
|
|
4712
4730
|
signals.push({
|
|
@@ -5252,12 +5270,16 @@ export class FingerprintEngine {
|
|
|
5252
5270
|
|
|
5253
5271
|
const { clientIp = "unknown", path, cookies = {}, query = {}, isStatic, graphqlOperationType, graphqlOperationName } = requestContext;
|
|
5254
5272
|
|
|
5255
|
-
|
|
5273
|
+
const queryCoopOp = query.coop_op || (typeof requestContext.body === 'object' && requestContext.body ? requestContext.body.coop_op : null);
|
|
5274
|
+
if (queryCoopOp) {
|
|
5275
|
+
const bodyObj = typeof requestContext.body === 'object' && requestContext.body !== null ? requestContext.body : {};
|
|
5256
5276
|
const coopParams = {
|
|
5257
5277
|
...query,
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5278
|
+
...bodyObj,
|
|
5279
|
+
coop_op: queryCoopOp,
|
|
5280
|
+
signature: requestContext.headers?.['x-federation-signature'] || query.signature || bodyObj.signature,
|
|
5281
|
+
signature_ed25519: requestContext.headers?.['x-federation-signature-ed25519'] || query.signature_ed25519 || bodyObj.signature_ed25519,
|
|
5282
|
+
timestamp: requestContext.headers?.['x-federation-timestamp'] || query.timestamp || bodyObj.timestamp
|
|
5261
5283
|
};
|
|
5262
5284
|
const result = await handleCooperativeRequest(coopParams, clientIp, this.securityConfig);
|
|
5263
5285
|
return {
|
|
@@ -5387,7 +5409,7 @@ export class FingerprintEngine {
|
|
|
5387
5409
|
const parsedToken = parsePrivateAccessToken(rawToken);
|
|
5388
5410
|
if (parsedToken) {
|
|
5389
5411
|
const nonceKey = `pat-nonce:${parsedToken.nonce}`;
|
|
5390
|
-
const isReplayed = await
|
|
5412
|
+
const isReplayed = await storeHas(nonceKey);
|
|
5391
5413
|
if (!isReplayed) {
|
|
5392
5414
|
const isValid = verifyPatSignature(parsedToken, patConfig);
|
|
5393
5415
|
if (isValid) {
|
|
@@ -5415,6 +5437,41 @@ export class FingerprintEngine {
|
|
|
5415
5437
|
let whitelisted = false;
|
|
5416
5438
|
let whitelistType = '';
|
|
5417
5439
|
|
|
5440
|
+
// Auto-whitelisting des pairs fédérés
|
|
5441
|
+
const federatedPeers = this.securityConfig.federatedPeers || [];
|
|
5442
|
+
if (federatedPeers.length > 0) {
|
|
5443
|
+
for (const peerUrl of federatedPeers) {
|
|
5444
|
+
try {
|
|
5445
|
+
if (new URL(peerUrl).hostname === clientIp || peerUrl === clientIp) {
|
|
5446
|
+
whitelisted = true;
|
|
5447
|
+
whitelistType = 'federated_peer';
|
|
5448
|
+
break;
|
|
5449
|
+
}
|
|
5450
|
+
} catch (e) {
|
|
5451
|
+
if (peerUrl === clientIp) {
|
|
5452
|
+
whitelisted = true;
|
|
5453
|
+
whitelistType = 'federated_peer';
|
|
5454
|
+
break;
|
|
5455
|
+
}
|
|
5456
|
+
}
|
|
5457
|
+
}
|
|
5458
|
+
}
|
|
5459
|
+
|
|
5460
|
+
if (!whitelisted) {
|
|
5461
|
+
const subnet = getIpSubnet(clientIp);
|
|
5462
|
+
const ua = requestContext.headers?.['user-agent'] || '';
|
|
5463
|
+
if (await storeHas(`federated-whitelist:ip:${clientIp}`)) {
|
|
5464
|
+
whitelisted = true;
|
|
5465
|
+
whitelistType = 'federated_whitelist_ip';
|
|
5466
|
+
} else if (subnet && await storeHas(`federated-whitelist:subnet:${subnet}`)) {
|
|
5467
|
+
whitelisted = true;
|
|
5468
|
+
whitelistType = 'federated_whitelist_subnet';
|
|
5469
|
+
} else if (ua && await storeHas(`federated-whitelist:user_agent:${ua}`)) {
|
|
5470
|
+
whitelisted = true;
|
|
5471
|
+
whitelistType = 'federated_whitelist_user_agent';
|
|
5472
|
+
}
|
|
5473
|
+
}
|
|
5474
|
+
|
|
5418
5475
|
if (this._isIpInAllowlist(clientIp)) {
|
|
5419
5476
|
whitelisted = true;
|
|
5420
5477
|
whitelistType = 'allowlist';
|
|
@@ -5912,7 +5969,7 @@ export class FingerprintEngine {
|
|
|
5912
5969
|
|
|
5913
5970
|
const threshold = this.securityConfig.federationConsensusThreshold || 3;
|
|
5914
5971
|
if (reportedPeers.length >= threshold) {
|
|
5915
|
-
if (!(await
|
|
5972
|
+
if (!(await storeHas(`banned-zkp-y:${zkpY}`))) {
|
|
5916
5973
|
await store.set(`banned-zkp-y:${zkpY}`, true, 86400 * 30); // Banni pour 30 jours
|
|
5917
5974
|
}
|
|
5918
5975
|
}
|
|
@@ -6459,6 +6516,62 @@ export async function handleCooperativeRequest(params, clientIp = '127.0.0.1', c
|
|
|
6459
6516
|
return { status: 'synchronized', banned: false, reportsCount: reportedPeers.length };
|
|
6460
6517
|
}
|
|
6461
6518
|
|
|
6519
|
+
if (op === 'share_whitelist') {
|
|
6520
|
+
const peers = config.federatedPeers || [];
|
|
6521
|
+
if (peers.length > 0) {
|
|
6522
|
+
const allowedIPsAndHosts = peers.map(url => {
|
|
6523
|
+
try { return new URL(url).hostname; } catch(e) { return url; }
|
|
6524
|
+
});
|
|
6525
|
+
if (!allowedIPsAndHosts.includes(clientIp)) {
|
|
6526
|
+
return { error: 'Unauthorized federation sender IP' };
|
|
6527
|
+
}
|
|
6528
|
+
}
|
|
6529
|
+
const entry = params.entry;
|
|
6530
|
+
const entryType = params.entry_type || 'ip'; // ip, subnet, user_agent
|
|
6531
|
+
const sigEd25519 = params.signature_ed25519;
|
|
6532
|
+
const timestampStr = params.timestamp;
|
|
6533
|
+
const ttlStr = params.ttl || '86400';
|
|
6534
|
+
|
|
6535
|
+
if (!entry || !entry.trim() || !sigEd25519 || !timestampStr) {
|
|
6536
|
+
return { error: 'Missing required parameters for share_whitelist' };
|
|
6537
|
+
}
|
|
6538
|
+
|
|
6539
|
+
const trimmedEntry = entry.trim();
|
|
6540
|
+
if (trimmedEntry === '*' || trimmedEntry === '0.0.0.0/0' || trimmedEntry === '::/0') {
|
|
6541
|
+
return { error: 'Permissive wildcard entries are prohibited' };
|
|
6542
|
+
}
|
|
6543
|
+
|
|
6544
|
+
const timestamp = Number(timestampStr);
|
|
6545
|
+
if (isNaN(timestamp) || Math.abs(Date.now() - timestamp) > 300000) {
|
|
6546
|
+
return { error: 'Timestamp expired or clock skew too high' };
|
|
6547
|
+
}
|
|
6548
|
+
|
|
6549
|
+
const publicKey = config.ed25519_public_key || process.env.ED25519_PUBLIC_KEY;
|
|
6550
|
+
if (!publicKey) {
|
|
6551
|
+
return { error: 'Missing public key for asymmetric verification' };
|
|
6552
|
+
}
|
|
6553
|
+
|
|
6554
|
+
const msg = `${timestamp}:whitelist:${entryType}:${trimmedEntry}`;
|
|
6555
|
+
try {
|
|
6556
|
+
const cleanKey = publicKey.replace(/\\n/g, '\n');
|
|
6557
|
+
const isVerified = crypto.verify(
|
|
6558
|
+
null,
|
|
6559
|
+
Buffer.from(msg, 'utf8'),
|
|
6560
|
+
{ key: cleanKey, format: 'pem', type: 'spki' },
|
|
6561
|
+
Buffer.from(sigEd25519, 'hex')
|
|
6562
|
+
);
|
|
6563
|
+
if (!isVerified) {
|
|
6564
|
+
return { error: 'Invalid Ed25519 signature for whitelist synchronization' };
|
|
6565
|
+
}
|
|
6566
|
+
} catch (e) {
|
|
6567
|
+
return { error: 'Asymmetric signature verification failed: ' + e.message };
|
|
6568
|
+
}
|
|
6569
|
+
|
|
6570
|
+
const ttl = Math.min(604800, parseInt(ttlStr, 10) || 86400);
|
|
6571
|
+
await store.set(`federated-whitelist:${entryType}:${trimmedEntry}`, true, ttl);
|
|
6572
|
+
return { status: 'whitelist_synchronized', entry: trimmedEntry, entryType, ttl };
|
|
6573
|
+
}
|
|
6574
|
+
|
|
6462
6575
|
const nodeId = params.node_id || '';
|
|
6463
6576
|
if (!nodeId) {
|
|
6464
6577
|
return { error: 'Missing node_id' };
|
|
@@ -6600,6 +6713,51 @@ export async function handleCooperativeRequest(params, clientIp = '127.0.0.1', c
|
|
|
6600
6713
|
return null;
|
|
6601
6714
|
}
|
|
6602
6715
|
|
|
6716
|
+
export async function broadcastWhitelistedEntry(entry, entryType = 'ip', ttl = 86400, config = {}) {
|
|
6717
|
+
const peers = config.federatedPeers || [];
|
|
6718
|
+
if (peers.length === 0 || !entry) return;
|
|
6719
|
+
|
|
6720
|
+
const trimmedEntry = entry.trim();
|
|
6721
|
+
if (trimmedEntry === '*' || trimmedEntry === '0.0.0.0/0' || trimmedEntry === '::/0') {
|
|
6722
|
+
throw new Error('Permissive wildcard entries are prohibited');
|
|
6723
|
+
}
|
|
6724
|
+
|
|
6725
|
+
const ed25519Key = process.env.ED25519_PRIVATE_KEY || config.ed25519_private_key;
|
|
6726
|
+
if (!ed25519Key) {
|
|
6727
|
+
throw new Error('ED25519_PRIVATE_KEY is required for federated whitelist broadcast');
|
|
6728
|
+
}
|
|
6729
|
+
|
|
6730
|
+
const ts = Date.now();
|
|
6731
|
+
const msg = `${ts}:whitelist:${entryType}:${trimmedEntry}`;
|
|
6732
|
+
const cleanKey = ed25519Key.replace(/\\n/g, '\n');
|
|
6733
|
+
const signature = crypto.sign(null, Buffer.from(msg, 'utf8'), {
|
|
6734
|
+
key: cleanKey,
|
|
6735
|
+
format: 'pem',
|
|
6736
|
+
type: 'pkcs8'
|
|
6737
|
+
}).toString('hex');
|
|
6738
|
+
|
|
6739
|
+
const boundedTtl = Math.min(604800, parseInt(ttl, 10) || 86400);
|
|
6740
|
+
|
|
6741
|
+
peers.forEach(peerUrl => {
|
|
6742
|
+
const url = `${peerUrl}?coop_op=share_whitelist`;
|
|
6743
|
+
fetch(url, {
|
|
6744
|
+
method: 'POST',
|
|
6745
|
+
headers: {
|
|
6746
|
+
'Content-Type': 'application/json',
|
|
6747
|
+
'X-Federation-Signature-Ed25519': signature,
|
|
6748
|
+
'X-Federation-Timestamp': String(ts)
|
|
6749
|
+
},
|
|
6750
|
+
body: JSON.stringify({
|
|
6751
|
+
entry: trimmedEntry,
|
|
6752
|
+
entry_type: entryType,
|
|
6753
|
+
ttl: boundedTtl,
|
|
6754
|
+
signature_ed25519: signature,
|
|
6755
|
+
timestamp: ts
|
|
6756
|
+
})
|
|
6757
|
+
}).catch(() => {});
|
|
6758
|
+
});
|
|
6759
|
+
}
|
|
6760
|
+
|
|
6603
6761
|
|
|
6604
6762
|
/** @type {Map<number, number>} Cache des TTL optimisés par score de suspicion (clés de 0 à 100 par pas de 10) */
|
|
6605
6763
|
let optimizedTtlCache = new Map();
|
|
@@ -7464,6 +7622,7 @@ export const __internal = {
|
|
|
7464
7622
|
findPeerInSubnet,
|
|
7465
7623
|
handleCooperativeRequest,
|
|
7466
7624
|
broadcastBannedZkp,
|
|
7625
|
+
broadcastWhitelistedEntry,
|
|
7467
7626
|
generateIssuerPemKeys,
|
|
7468
7627
|
getMetric,
|
|
7469
7628
|
incrementCounter,
|
|
@@ -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'];
|
|
@@ -645,15 +705,32 @@ class ChallengeUtils
|
|
|
645
705
|
if (function_exists('wp_mkdir_p')) {
|
|
646
706
|
wp_mkdir_p($outDir);
|
|
647
707
|
} else {
|
|
708
|
+
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir -- Standalone filesystem fallback
|
|
648
709
|
mkdir($outDir, 0755, true);
|
|
649
710
|
}
|
|
650
711
|
}
|
|
651
712
|
$privateKeyPath = rtrim($outDir, '/\\') . '/' . $privName;
|
|
652
713
|
$publicKeyPath = rtrim($outDir, '/\\') . '/' . $pubName;
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
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
|
+
}
|
|
720
|
+
|
|
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
|
+
}
|
|
657
734
|
}
|
|
658
735
|
|
|
659
736
|
return [
|
|
@@ -1372,7 +1449,7 @@ class ChallengeUtils
|
|
|
1372
1449
|
. '}' . "\n"
|
|
1373
1450
|
. 'solve();';
|
|
1374
1451
|
|
|
1375
|
-
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>";
|
|
1376
1453
|
}
|
|
1377
1454
|
|
|
1378
1455
|
/**
|
|
@@ -1471,7 +1548,7 @@ class ChallengeUtils
|
|
|
1471
1548
|
. '}' . "\n"
|
|
1472
1549
|
. 'solve();';
|
|
1473
1550
|
|
|
1474
|
-
$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>';
|
|
1475
1552
|
$customTemplatePath = $securityConfig['challengePagePath'] ?? null;
|
|
1476
1553
|
|
|
1477
1554
|
if ($customTemplatePath && file_exists($customTemplatePath)) {
|
|
@@ -1515,6 +1592,10 @@ class ChallengeUtils
|
|
|
1515
1592
|
if (function_exists('sanitize_text_field')) {
|
|
1516
1593
|
return sanitize_text_field($val);
|
|
1517
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
|
|
1518
1599
|
return trim(strip_tags($val));
|
|
1519
1600
|
}
|
|
1520
1601
|
|
|
@@ -267,6 +267,7 @@ class FingerprintEngine
|
|
|
267
267
|
if (function_exists('wp_mkdir_p')) {
|
|
268
268
|
wp_mkdir_p($configDir);
|
|
269
269
|
} else {
|
|
270
|
+
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_mkdir -- Standalone filesystem fallback
|
|
270
271
|
@mkdir($configDir, 0700, true);
|
|
271
272
|
}
|
|
272
273
|
}
|
|
@@ -689,6 +690,21 @@ class FingerprintEngine
|
|
|
689
690
|
*/
|
|
690
691
|
private function checkAllowlists(RequestContext $context): bool
|
|
691
692
|
{
|
|
693
|
+
// Auto-whitelisting des pairs fédérés
|
|
694
|
+
$peers = $this->securityConfig['federatedPeers'] ?? [];
|
|
695
|
+
if (!empty($peers)) {
|
|
696
|
+
$allowedHosts = array_map(function ($url) {
|
|
697
|
+
// phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- Covered by wp_parse_url, Fallback for standalone PHP environments
|
|
698
|
+
$host = function_exists('wp_parse_url') ? wp_parse_url($url, PHP_URL_HOST) : parse_url($url, PHP_URL_HOST);
|
|
699
|
+
return !empty($host) ? $host : $url;
|
|
700
|
+
}, $peers);
|
|
701
|
+
|
|
702
|
+
if (in_array($context->clientIp, $allowedHosts, true)) {
|
|
703
|
+
$this->log("Federated peer auto-whitelisted", ['clientIp' => $context->clientIp]);
|
|
704
|
+
return true;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
692
708
|
$whitelisted = false;
|
|
693
709
|
$type = '';
|
|
694
710
|
|
|
@@ -701,6 +717,15 @@ class FingerprintEngine
|
|
|
701
717
|
} elseif ($this->isHostPathInAllowlist($context->getHeader('host'), $context->path)) {
|
|
702
718
|
$whitelisted = true;
|
|
703
719
|
$type = 'host_path_allowlist';
|
|
720
|
+
} elseif (StoreManager::getStore()->has("federated-whitelist:ip:{$context->clientIp}")) {
|
|
721
|
+
$whitelisted = true;
|
|
722
|
+
$type = 'federated_whitelist_ip';
|
|
723
|
+
} elseif (($subnet = RequestUtils::getIpSubnet($context->clientIp)) && StoreManager::getStore()->has("federated-whitelist:subnet:{$subnet}")) {
|
|
724
|
+
$whitelisted = true;
|
|
725
|
+
$type = 'federated_whitelist_subnet';
|
|
726
|
+
} elseif (($ua = $context->getHeader('user-agent')) && StoreManager::getStore()->has("federated-whitelist:user_agent:{$ua}")) {
|
|
727
|
+
$whitelisted = true;
|
|
728
|
+
$type = 'federated_whitelist_user_agent';
|
|
704
729
|
} elseif ($context->graphqlOperation && $this->isGraphqlOperationInAllowlist($context->graphqlOperation['type'], $context->graphqlOperation['name'])) {
|
|
705
730
|
$whitelisted = true;
|
|
706
731
|
$type = 'graphql_operation_allowlist';
|
|
@@ -2005,6 +2030,56 @@ class FingerprintEngine
|
|
|
2005
2030
|
}
|
|
2006
2031
|
}
|
|
2007
2032
|
|
|
2033
|
+
public function broadcastWhitelist(string $entry, string $entryType = 'ip', int $ttl = 86400): void
|
|
2034
|
+
{
|
|
2035
|
+
$peers = $this->securityConfig['federatedPeers'] ?? [];
|
|
2036
|
+
if (empty($peers)) return;
|
|
2037
|
+
|
|
2038
|
+
$cleanEntry = trim($entry);
|
|
2039
|
+
if ($cleanEntry === '*' || $cleanEntry === '0.0.0.0/0' || $cleanEntry === '::/0') {
|
|
2040
|
+
throw new \InvalidArgumentException('Permissive wildcard entries are prohibited');
|
|
2041
|
+
}
|
|
2042
|
+
if ($cleanEntry === '') {
|
|
2043
|
+
return;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
$boundedTtl = min(604800, max(60, $ttl));
|
|
2047
|
+
$ts = (int)(microtime(true) * 1000);
|
|
2048
|
+
$msg = "{$ts}:whitelist:{$entryType}:{$cleanEntry}";
|
|
2049
|
+
|
|
2050
|
+
$privateKey = Env::get('ED25519_PRIVATE_KEY');
|
|
2051
|
+
if (!$privateKey) {
|
|
2052
|
+
throw new \RuntimeException('ED25519_PRIVATE_KEY is required for federated whitelist broadcast');
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
$signature = '';
|
|
2056
|
+
try {
|
|
2057
|
+
$cleanKey = str_replace('\n', "\n", $privateKey);
|
|
2058
|
+
$pkeyObj = openssl_pkey_get_private($cleanKey);
|
|
2059
|
+
if ($pkeyObj && openssl_sign($msg, $sigBytes, $pkeyObj, null)) {
|
|
2060
|
+
$signature = bin2hex($sigBytes);
|
|
2061
|
+
}
|
|
2062
|
+
} catch (\Throwable $e) {
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
if (empty($signature)) return;
|
|
2066
|
+
|
|
2067
|
+
foreach ($peers as $peerUrl) {
|
|
2068
|
+
$this->asyncPost($peerUrl . '?coop_op=share_whitelist', [
|
|
2069
|
+
'entry' => $cleanEntry,
|
|
2070
|
+
'entry_type' => $entryType,
|
|
2071
|
+
'signature_ed25519' => $signature,
|
|
2072
|
+
'timestamp' => $ts,
|
|
2073
|
+
'ttl' => $boundedTtl,
|
|
2074
|
+
]);
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
|
|
2078
|
+
public function broadcastWhitelistedEntry(string $entry, string $entryType = 'ip', int $ttl = 86400): void
|
|
2079
|
+
{
|
|
2080
|
+
$this->broadcastWhitelist($entry, $entryType, $ttl);
|
|
2081
|
+
}
|
|
2082
|
+
|
|
2008
2083
|
/**
|
|
2009
2084
|
* Sends an asynchronous HTTP POST request to a given URL with the given
|
|
2010
2085
|
* parameters (JSON-encoded). Uses non-blocking stream sockets with a short
|
|
@@ -316,6 +316,7 @@ class RequestUtils
|
|
|
316
316
|
|
|
317
317
|
$claimedBrowserInfo = self::parseUserAgent($ua);
|
|
318
318
|
$claimedBrowser = $claimedBrowserInfo['browser'] ?? null;
|
|
319
|
+
$tlsSpoofingScore = 0.0;
|
|
319
320
|
|
|
320
321
|
if (empty($claimedBrowser) || empty($ua)) {
|
|
321
322
|
return ['tlsSpoofingScore' => 0.0];
|
|
@@ -367,7 +368,7 @@ class RequestUtils
|
|
|
367
368
|
$echExpected = $context->getHeader('x-ech-expected') === 'true' || !empty($outerSni);
|
|
368
369
|
|
|
369
370
|
if (!empty($outerSni) && !empty($host) && strcasecmp($outerSni, $host) !== 0 && !$hasEch) {
|
|
370
|
-
return ['tlsSpoofingScore' => max($tlsSpoofingScore
|
|
371
|
+
return ['tlsSpoofingScore' => max($tlsSpoofingScore, 60.0)];
|
|
371
372
|
}
|
|
372
373
|
|
|
373
374
|
$browserVer = (int)($claimedBrowserInfo['version'] ?? 0);
|
|
@@ -376,7 +377,7 @@ class RequestUtils
|
|
|
376
377
|
($claimedBrowser === 'Firefox' && $browserVer >= 118)
|
|
377
378
|
);
|
|
378
379
|
if ($context->isHttps && $isModernEchBrowser && $echExpected && !$hasEch) {
|
|
379
|
-
return ['tlsSpoofingScore' => max($tlsSpoofingScore
|
|
380
|
+
return ['tlsSpoofingScore' => max($tlsSpoofingScore, 55.0)];
|
|
380
381
|
}
|
|
381
382
|
|
|
382
383
|
return ['tlsSpoofingScore' => 0.0];
|