@anonympins/fingerprint 0.7.1 → 0.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -3
- package/README.md +1 -1
- package/package.json +1 -1
- package/public/fingerprint-anti-bot.zip +0 -0
- package/src/js/fingerprint.js +106 -29
- package/src/js/fingerprint.utils.js +32 -5
- package/src/js/tests/fingerprint.test.js +91 -4
- package/src/js/tests/threatIntel.test.js +76 -0
- package/src/php/AutoTuner.php +36 -18
- package/src/php/Challenge/ChallengeUtils.php +107 -225
- package/src/php/DirectFingerprint.php +16 -4
- package/src/php/FingerprintClient.php +17 -19
- package/src/php/FingerprintEngine.php +114 -51
- package/src/php/Ja3AnomalyDetector.php +20 -12
- package/src/php/Optimization/Optimization.php +5 -1
- package/src/php/Optimization/OptimizationOperators.php +5 -1
- package/src/php/Optimization/ProblemInitializers.php +16 -4
- package/src/php/ProblemManager.php +20 -14
- package/src/php/Tests/FingerprintEngineTest.php +705 -662
- package/src/php/Tests/ThreatIntelTest.php +64 -0
- package/src/php/Utils/BlockList.php +99 -99
- package/src/php/Utils/Env.php +51 -49
- package/src/php/Utils/MaliciousPatterns.php +74 -74
- package/src/php/Utils/RequestUtils.php +22 -5
- package/src/php/WordPress/WpDbStore.php +4 -0
- package/src/php/WordPress/fingerprint-anti-bot.php +559 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +262 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +268 -0
- package/src/php/WordPress/package.php +102 -4
- package/src/php/WordPress/readme.txt +36 -0
- package/public/fingerprint-wordpress.zip +0 -0
- package/src/js/fingerprint.client.obfuscated.js +0 -1
- package/src/php/WordPress/fingerprint-wordpress.php +0 -217
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint\Tests;
|
|
6
|
+
|
|
7
|
+
use Anonympins\Fingerprint\Ja3AnomalyDetector;
|
|
8
|
+
use Anonympins\Fingerprint\RequestContext;
|
|
9
|
+
use Anonympins\Fingerprint\Store\InMemoryStore;
|
|
10
|
+
use Anonympins\Fingerprint\Store\StoreManager;
|
|
11
|
+
use Anonympins\Fingerprint\Utils\RequestUtils;
|
|
12
|
+
use PHPUnit\Framework\TestCase;
|
|
13
|
+
|
|
14
|
+
class ThreatIntelTest extends TestCase
|
|
15
|
+
{
|
|
16
|
+
protected function setUp(): void
|
|
17
|
+
{
|
|
18
|
+
parent::setUp();
|
|
19
|
+
StoreManager::configureStore(new InMemoryStore());
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
public function testRttProxyScoreReturnsZeroForCoherentTurnaround(): void
|
|
23
|
+
{
|
|
24
|
+
$reqTime = 1700000000000;
|
|
25
|
+
$context = new RequestContext(
|
|
26
|
+
'127.0.0.1', '/',
|
|
27
|
+
[
|
|
28
|
+
'x-tcp-rtt' => '15',
|
|
29
|
+
'x-behavior-metrics' => json_encode(['clientTimestamp' => $reqTime - 30])
|
|
30
|
+
],
|
|
31
|
+
[], null, [], '1.1', $reqTime
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
$score = Ja3AnomalyDetector::getRttProxyScore($context);
|
|
35
|
+
$this->assertEquals(0.0, $score);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public function testRttProxyScoreIgnoresSlowConnectionWithoutTcpRtt(): void
|
|
39
|
+
{
|
|
40
|
+
$reqTime = 1700000000000;
|
|
41
|
+
$context = new RequestContext(
|
|
42
|
+
'127.0.0.1', '/',
|
|
43
|
+
['x-behavior-metrics' => json_encode(['clientTimestamp' => $reqTime - 450])],
|
|
44
|
+
[], null, [], '1.1', $reqTime
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
$score = Ja3AnomalyDetector::getRttProxyScore($context);
|
|
48
|
+
$this->assertEquals(0.0, $score, 'A slow connection without TCP RTT context should not trigger a false positive.');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public function testRttProxyScoreContinuouslyPenalizesResidentialTunneling(): void
|
|
52
|
+
{
|
|
53
|
+
$reqTime = 1700000000000;
|
|
54
|
+
$context = new RequestContext(
|
|
55
|
+
'127.0.0.1', '/',
|
|
56
|
+
['x-tcp-rtt' => '10', 'x-behavior-metrics' => json_encode(['clientTimestamp' => $reqTime - 300])],
|
|
57
|
+
[], null, [], '1.1', $reqTime
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
$score = Ja3AnomalyDetector::getRttProxyScore($context);
|
|
61
|
+
$this->assertGreaterThan(75.0, $score);
|
|
62
|
+
$this->assertLessThanOrEqual(95.0, $score);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -1,100 +1,100 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
namespace Anonympins\Fingerprint\Utils;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Implémentation PHP d'une liste de blocage IP/CIDR, similaire à Node.js `net.BlockList`.
|
|
9
|
-
*/
|
|
10
|
-
class BlockList
|
|
11
|
-
{
|
|
12
|
-
/**
|
|
13
|
-
* @var array<string> Liste des IPs ou CIDR à bloquer.
|
|
14
|
-
*/
|
|
15
|
-
private array $entries = [];
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Ajoute une adresse IP ou une plage CIDR à la liste.
|
|
19
|
-
* @param string $entry Une adresse IP (ex: '192.168.1.1') ou une plage CIDR (ex: '192.168.1.0/24').
|
|
20
|
-
* @return void
|
|
21
|
-
*/
|
|
22
|
-
public function add(string $entry): void
|
|
23
|
-
{
|
|
24
|
-
$this->entries[] = $entry;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Vérifie si une adresse IP est présente dans la liste de blocage.
|
|
29
|
-
* @param string $ip L'adresse IP à vérifier.
|
|
30
|
-
* @return bool True si l'IP est bloquée, false sinon.
|
|
31
|
-
*/
|
|
32
|
-
public function check(string $ip): bool
|
|
33
|
-
{
|
|
34
|
-
foreach ($this->entries as $entry) {
|
|
35
|
-
if (str_contains($entry, '/')) {
|
|
36
|
-
// C'est une plage CIDR
|
|
37
|
-
if ($this->ipInCidr($ip, $entry)) {
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
} else {
|
|
41
|
-
// C'est une IP directe
|
|
42
|
-
if ($ip === $entry) {
|
|
43
|
-
return true;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Vérifie si une adresse IP se trouve dans une plage CIDR donnée.
|
|
52
|
-
* @param string $ip L'adresse IP à vérifier.
|
|
53
|
-
* @param string $cidr La plage CIDR (ex: '192.168.1.0/24').
|
|
54
|
-
* @return bool True si l'IP est dans la plage, false sinon.
|
|
55
|
-
*/
|
|
56
|
-
private function ipInCidr(string $ip, string $cidr): bool
|
|
57
|
-
{
|
|
58
|
-
|
|
59
|
-
$
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
$
|
|
71
|
-
$
|
|
72
|
-
|
|
73
|
-
$
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
return
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint\Utils;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Implémentation PHP d'une liste de blocage IP/CIDR, similaire à Node.js `net.BlockList`.
|
|
9
|
+
*/
|
|
10
|
+
class BlockList
|
|
11
|
+
{
|
|
12
|
+
/**
|
|
13
|
+
* @var array<string> Liste des IPs ou CIDR à bloquer.
|
|
14
|
+
*/
|
|
15
|
+
private array $entries = [];
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Ajoute une adresse IP ou une plage CIDR à la liste.
|
|
19
|
+
* @param string $entry Une adresse IP (ex: '192.168.1.1') ou une plage CIDR (ex: '192.168.1.0/24').
|
|
20
|
+
* @return void
|
|
21
|
+
*/
|
|
22
|
+
public function add(string $entry): void
|
|
23
|
+
{
|
|
24
|
+
$this->entries[] = $entry;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Vérifie si une adresse IP est présente dans la liste de blocage.
|
|
29
|
+
* @param string $ip L'adresse IP à vérifier.
|
|
30
|
+
* @return bool True si l'IP est bloquée, false sinon.
|
|
31
|
+
*/
|
|
32
|
+
public function check(string $ip): bool
|
|
33
|
+
{
|
|
34
|
+
foreach ($this->entries as $entry) {
|
|
35
|
+
if (str_contains($entry, '/')) {
|
|
36
|
+
// C'est une plage CIDR
|
|
37
|
+
if ($this->ipInCidr($ip, $entry)) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
} else {
|
|
41
|
+
// C'est une IP directe
|
|
42
|
+
if ($ip === $entry) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Vérifie si une adresse IP se trouve dans une plage CIDR donnée.
|
|
52
|
+
* @param string $ip L'adresse IP à vérifier.
|
|
53
|
+
* @param string $cidr La plage CIDR (ex: '192.168.1.0/24').
|
|
54
|
+
* @return bool True si l'IP est dans la plage, false sinon.
|
|
55
|
+
*/
|
|
56
|
+
private function ipInCidr(string $ip, string $cidr): bool
|
|
57
|
+
{
|
|
58
|
+
// Handle IPv4-mapped IPv6 addresses by converting them to IPv4
|
|
59
|
+
if (str_starts_with(strtolower($ip), '::ffff:') && filter_var(substr($ip, 7), FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
|
60
|
+
$ip = substr($ip, 7);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
[$network, $mask] = explode('/', $cidr, 2);
|
|
64
|
+
$mask = (int)$mask;
|
|
65
|
+
|
|
66
|
+
// Now, both $ip and $network should be in the same family for a valid comparison
|
|
67
|
+
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) && filter_var($network, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
|
68
|
+
if ($mask < 0 || $mask > 32) { return false; }
|
|
69
|
+
$ipLong = ip2long($ip);
|
|
70
|
+
$networkLong = ip2long($network);
|
|
71
|
+
if ($ipLong === false || $networkLong === false) return false;
|
|
72
|
+
$netmask = -1 << (32 - $mask);
|
|
73
|
+
return ($ipLong & $netmask) === ($networkLong & $netmask);
|
|
74
|
+
} elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) && filter_var($network, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
|
75
|
+
// IPv6 vs IPv6 CIDR
|
|
76
|
+
if ($mask < 0 || $mask > 128) { return false; }
|
|
77
|
+
$ipBinary = inet_pton($ip);
|
|
78
|
+
$networkBinary = inet_pton($network);
|
|
79
|
+
if ($ipBinary === false || $networkBinary === false) return false;
|
|
80
|
+
|
|
81
|
+
$bytesToCompare = (int)floor($mask / 8);
|
|
82
|
+
if (strncmp($ipBinary, $networkBinary, $bytesToCompare) !== 0) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
$bitsToCompare = $mask % 8;
|
|
87
|
+
if ($bitsToCompare > 0) {
|
|
88
|
+
$byteIndex = $bytesToCompare;
|
|
89
|
+
$bitmask = (0xFF << (8 - $bitsToCompare)) & 0xFF;
|
|
90
|
+
if ((ord($ipBinary[$byteIndex]) & $bitmask) !== (ord($networkBinary[$byteIndex]) & $bitmask)) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
100
|
}
|
package/src/php/Utils/Env.php
CHANGED
|
@@ -1,50 +1,52 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
namespace Anonympins\Fingerprint\Utils;
|
|
6
|
-
|
|
7
|
-
class Env
|
|
8
|
-
{
|
|
9
|
-
/** @var array<string, mixed> */
|
|
10
|
-
private static array $localCache = [];
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* @param string $key
|
|
14
|
-
* @param mixed $default
|
|
15
|
-
* @return mixed
|
|
16
|
-
*/
|
|
17
|
-
public static function get(string $key, $default = null)
|
|
18
|
-
{
|
|
19
|
-
if (array_key_exists($key, self::$localCache)) {
|
|
20
|
-
return self::$localCache[$key];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
unset(
|
|
48
|
-
|
|
49
|
-
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint\Utils;
|
|
6
|
+
|
|
7
|
+
class Env
|
|
8
|
+
{
|
|
9
|
+
/** @var array<string, mixed> */
|
|
10
|
+
private static array $localCache = [];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param string $key
|
|
14
|
+
* @param mixed $default
|
|
15
|
+
* @return mixed
|
|
16
|
+
*/
|
|
17
|
+
public static function get(string $key, $default = null)
|
|
18
|
+
{
|
|
19
|
+
if (array_key_exists($key, self::$localCache)) {
|
|
20
|
+
return self::$localCache[$key];
|
|
21
|
+
}
|
|
22
|
+
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Environment and server variables may contain raw cryptographic keys.
|
|
23
|
+
if (isset($_ENV[$key]) && $_ENV[$key] !== '') {
|
|
24
|
+
return $_ENV[$key];
|
|
25
|
+
}
|
|
26
|
+
if (isset($_SERVER[$key]) && $_SERVER[$key] !== '') {
|
|
27
|
+
return $_SERVER[$key];
|
|
28
|
+
}
|
|
29
|
+
// phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized,WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
|
30
|
+
$val = getenv($key);
|
|
31
|
+
if ($val !== false && $val !== '') {
|
|
32
|
+
return $val;
|
|
33
|
+
}
|
|
34
|
+
return $default;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public static function set(string $key, $value): void
|
|
38
|
+
{
|
|
39
|
+
self::$localCache[$key] = $value;
|
|
40
|
+
$_ENV[$key] = $value;
|
|
41
|
+
$_SERVER[$key] = $value;
|
|
42
|
+
@putenv("{$key}={$value}");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public static function clear(string $key): void
|
|
46
|
+
{
|
|
47
|
+
unset(self::$localCache[$key]);
|
|
48
|
+
unset($_ENV[$key]);
|
|
49
|
+
unset($_SERVER[$key]);
|
|
50
|
+
@putenv($key);
|
|
51
|
+
}
|
|
50
52
|
}
|
|
@@ -1,75 +1,75 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
namespace Anonympins\Fingerprint\Utils;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Fournit des fonctions pour détecter les patterns d'injection malveillants.
|
|
9
|
-
*/
|
|
10
|
-
class MaliciousPatterns
|
|
11
|
-
{
|
|
12
|
-
/**
|
|
13
|
-
* @var array<string, string> Map des patterns malveillants regroupés par type.
|
|
14
|
-
*/
|
|
15
|
-
private const INJECTION_PATTERNS = [
|
|
16
|
-
// SQL/NoSQL injections, including time-based attacks
|
|
17
|
-
'sql' => '/(\$ne|\' *OR *\'1\'=\'1|[\'";]\s*--|; ?(DROP|TRUNCATE|DELETE)|UNION SELECT|(?:SLEEP|BENCHMARK)\s*\(|WAITFOR DELAY)/i',
|
|
18
|
-
// Log4Shell (JNDI injection)
|
|
19
|
-
'log4shell' => '/\$\{jndi:(ldap|rmi|dns):/i',
|
|
20
|
-
// Server-Side Template Injection (SSTI) for engines like Jinja2, Twig, etc.
|
|
21
|
-
'ssti' => '/\{\{.*\}\}|\{%.*%\}/',
|
|
22
|
-
// XML External Entity (XXE) injection
|
|
23
|
-
'xxe' => '/<!ENTITY\s+.*SYSTEM/i',
|
|
24
|
-
// Path Traversal
|
|
25
|
-
'traversal' => '/(\.\.\/|\.\.)/',
|
|
26
|
-
// Remote Command Execution (RCE)
|
|
27
|
-
// The original regex had an issue with `|cmd` being interpreted as a modifier in some PCRE versions.
|
|
28
|
-
// Using a non-capturing group (?:...) makes it more robust and fixes the compilation error.
|
|
29
|
-
'rce' => '/`.*`|(?:^|[\n;&|]\s*)(?:ping|ls|whoami|cat|rm|ncat|nc|bash|sh|powershell|cmd)\b/i',
|
|
30
|
-
// Server-Side Request Forgery (SSRF)
|
|
31
|
-
'ssrf' => '/(?:https?:\/\/)?(?:127\.\d+\.\d+\.\d+|169\.254\.169\.254|10\.\d+\.\d+\.\d+|172\.(?:1[6-9]|2\d|3[01])\.\d+\.\d+|192\.168\.\d+\.\d+|localhost|0\.0\.0\.0|\[[0:]+1\])\b/i',
|
|
32
|
-
// Carriage Return Line Feed (CRLF) Injection
|
|
33
|
-
'crlf' => '/[\r\n]|%0[ad]/i',
|
|
34
|
-
// Cross-Site Scripting (XSS)
|
|
35
|
-
'xss' => '/(<script|javascript:|on\w+\s*=|alert\s*\(|confirm\s*\(|prompt\s*\(|<img\s+src[^>]+onerror|<iframe)/i',
|
|
36
|
-
// Open Redirect
|
|
37
|
-
'openRedirect' => '/^(https?:)?\/\/(?![^\/]*?(localhost|127\.0\.0\.1))[^\s\/]+/i',
|
|
38
|
-
// Local/Remote File Inclusion (LFI/RFI)
|
|
39
|
-
'lfi' => '/(?:etc\/passwd|win\.ini|boot\.ini|php:\/\/filter|data:\/\/|zip:\/\/)/i',
|
|
40
|
-
// Shellshock (CVE-2014-6271)
|
|
41
|
-
'shellshock' => '/\(\)\s*\{\s*:\s*;\s*\}\s*/i',
|
|
42
|
-
// NoSQL Injection (MongoDB query operators)
|
|
43
|
-
'nosql' => '/\$(?:eq|ne|gt|gte|lt|lte|in|nin|and|or|nor|not|expr|jsonSchema|mod|regex|text|where|elemMatch)/i',
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Vérifie si une chaîne de caractères contient des patterns d'injection connus.
|
|
48
|
-
* @param string $str La chaîne à vérifier.
|
|
49
|
-
* @param array<string> $typesToDetect Les types d'injections à détecter (par défaut, tous).
|
|
50
|
-
* @return bool True si un pattern malveillant est détecté, false sinon.
|
|
51
|
-
*/
|
|
52
|
-
public static function isMalicious(string $str, array $typesToDetect = []): bool
|
|
53
|
-
{
|
|
54
|
-
if (empty($typesToDetect)) {
|
|
55
|
-
$typesToDetect = array_filter(array_keys(self::INJECTION_PATTERNS), function ($key) {
|
|
56
|
-
return $key !== 'openRedirect';
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
foreach ($typesToDetect as $type) {
|
|
61
|
-
if (isset(self::INJECTION_PATTERNS[$type])) {
|
|
62
|
-
try {
|
|
63
|
-
if (preg_match(self::INJECTION_PATTERNS[$type], $str)) {
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
}catch (\Exception $e){
|
|
67
|
-
|
|
68
|
-
error_log($e->getMessage());
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint\Utils;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Fournit des fonctions pour détecter les patterns d'injection malveillants.
|
|
9
|
+
*/
|
|
10
|
+
class MaliciousPatterns
|
|
11
|
+
{
|
|
12
|
+
/**
|
|
13
|
+
* @var array<string, string> Map des patterns malveillants regroupés par type.
|
|
14
|
+
*/
|
|
15
|
+
private const INJECTION_PATTERNS = [
|
|
16
|
+
// SQL/NoSQL injections, including time-based attacks
|
|
17
|
+
'sql' => '/(\$ne|\' *OR *\'1\'=\'1|[\'";]\s*--|; ?(DROP|TRUNCATE|DELETE)|UNION SELECT|(?:SLEEP|BENCHMARK)\s*\(|WAITFOR DELAY)/i',
|
|
18
|
+
// Log4Shell (JNDI injection)
|
|
19
|
+
'log4shell' => '/\$\{jndi:(ldap|rmi|dns):/i',
|
|
20
|
+
// Server-Side Template Injection (SSTI) for engines like Jinja2, Twig, etc.
|
|
21
|
+
'ssti' => '/\{\{.*\}\}|\{%.*%\}/',
|
|
22
|
+
// XML External Entity (XXE) injection
|
|
23
|
+
'xxe' => '/<!ENTITY\s+.*SYSTEM/i',
|
|
24
|
+
// Path Traversal
|
|
25
|
+
'traversal' => '/(\.\.\/|\.\.)/',
|
|
26
|
+
// Remote Command Execution (RCE)
|
|
27
|
+
// The original regex had an issue with `|cmd` being interpreted as a modifier in some PCRE versions.
|
|
28
|
+
// Using a non-capturing group (?:...) makes it more robust and fixes the compilation error.
|
|
29
|
+
'rce' => '/`.*`|(?:^|[\n;&|]\s*)(?:ping|ls|whoami|cat|rm|ncat|nc|bash|sh|powershell|cmd)\b/i',
|
|
30
|
+
// Server-Side Request Forgery (SSRF)
|
|
31
|
+
'ssrf' => '/(?:https?:\/\/)?(?:127\.\d+\.\d+\.\d+|169\.254\.169\.254|10\.\d+\.\d+\.\d+|172\.(?:1[6-9]|2\d|3[01])\.\d+\.\d+|192\.168\.\d+\.\d+|localhost|0\.0\.0\.0|\[[0:]+1\])\b/i',
|
|
32
|
+
// Carriage Return Line Feed (CRLF) Injection
|
|
33
|
+
'crlf' => '/[\r\n]|%0[ad]/i',
|
|
34
|
+
// Cross-Site Scripting (XSS)
|
|
35
|
+
'xss' => '/(<script|javascript:|on\w+\s*=|alert\s*\(|confirm\s*\(|prompt\s*\(|<img\s+src[^>]+onerror|<iframe)/i',
|
|
36
|
+
// Open Redirect
|
|
37
|
+
'openRedirect' => '/^(https?:)?\/\/(?![^\/]*?(localhost|127\.0\.0\.1))[^\s\/]+/i',
|
|
38
|
+
// Local/Remote File Inclusion (LFI/RFI)
|
|
39
|
+
'lfi' => '/(?:etc\/passwd|win\.ini|boot\.ini|php:\/\/filter|data:\/\/|zip:\/\/)/i',
|
|
40
|
+
// Shellshock (CVE-2014-6271)
|
|
41
|
+
'shellshock' => '/\(\)\s*\{\s*:\s*;\s*\}\s*/i',
|
|
42
|
+
// NoSQL Injection (MongoDB query operators)
|
|
43
|
+
'nosql' => '/\$(?:eq|ne|gt|gte|lt|lte|in|nin|and|or|nor|not|expr|jsonSchema|mod|regex|text|where|elemMatch)/i',
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Vérifie si une chaîne de caractères contient des patterns d'injection connus.
|
|
48
|
+
* @param string $str La chaîne à vérifier.
|
|
49
|
+
* @param array<string> $typesToDetect Les types d'injections à détecter (par défaut, tous).
|
|
50
|
+
* @return bool True si un pattern malveillant est détecté, false sinon.
|
|
51
|
+
*/
|
|
52
|
+
public static function isMalicious(string $str, array $typesToDetect = []): bool
|
|
53
|
+
{
|
|
54
|
+
if (empty($typesToDetect)) {
|
|
55
|
+
$typesToDetect = array_filter(array_keys(self::INJECTION_PATTERNS), function ($key) {
|
|
56
|
+
return $key !== 'openRedirect';
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
foreach ($typesToDetect as $type) {
|
|
61
|
+
if (isset(self::INJECTION_PATTERNS[$type])) {
|
|
62
|
+
try {
|
|
63
|
+
if (preg_match(self::INJECTION_PATTERNS[$type], $str)) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
} catch (\Exception $e) {
|
|
67
|
+
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- WAF regex validation diagnostic
|
|
68
|
+
error_log("Pattern inspection failed for {$type}: " . $e->getMessage());
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
75
|
}
|
|
@@ -1038,19 +1038,34 @@ class RequestUtils
|
|
|
1038
1038
|
public static function getThreatIntelScore(RequestContext $context, array $threatIntelConfig): array
|
|
1039
1039
|
{
|
|
1040
1040
|
$score = 0.0;
|
|
1041
|
+
$signals = [];
|
|
1042
|
+
|
|
1041
1043
|
$zkpY = $context->zkpY;
|
|
1042
1044
|
if (!empty($zkpY)) {
|
|
1043
1045
|
$store = StoreManager::getStore();
|
|
1044
1046
|
$isBanned = $store->has("banned-zkp-y:{$zkpY}");
|
|
1045
1047
|
if ($isBanned) {
|
|
1046
1048
|
$score = 100.0;
|
|
1049
|
+
$signals[] = [
|
|
1050
|
+
'ruleId' => 'FEDERATED_ZKP_BANNED',
|
|
1051
|
+
'confidence' => 1.0,
|
|
1052
|
+
'score' => 100.0,
|
|
1053
|
+
'rationale' => 'Cryptographic identity matched federated banned list consensus'
|
|
1054
|
+
];
|
|
1047
1055
|
}
|
|
1048
1056
|
}
|
|
1049
1057
|
|
|
1050
|
-
$rttProxyScore = \Anonympins\Fingerprint\Ja3AnomalyDetector::getRttProxyScore($context);
|
|
1051
|
-
|
|
1058
|
+
$rttProxyScore = (float)\Anonympins\Fingerprint\Ja3AnomalyDetector::getRttProxyScore($context);
|
|
1059
|
+
if ($rttProxyScore > 0.0) {
|
|
1060
|
+
$score = max($score, $rttProxyScore);
|
|
1061
|
+
$signals[] = [
|
|
1062
|
+
'ruleId' => 'RESIDENTIAL_PROXY_RTT_DISCREPANCY',
|
|
1063
|
+
'score' => $rttProxyScore,
|
|
1064
|
+
'rationale' => 'Physical discrepancy between TCP edge RTT and application transit latency'
|
|
1065
|
+
];
|
|
1066
|
+
}
|
|
1052
1067
|
|
|
1053
|
-
return ['threatIntelScore' => $score];
|
|
1068
|
+
return ['threatIntelScore' => $score, 'threatIntelSignals' => $signals];
|
|
1054
1069
|
}
|
|
1055
1070
|
|
|
1056
1071
|
/**
|
|
@@ -1145,7 +1160,8 @@ class RequestUtils
|
|
|
1145
1160
|
*/
|
|
1146
1161
|
public static function cleanUrlFromPowParams(string $originalPath, array $incomingQuery): string
|
|
1147
1162
|
{
|
|
1148
|
-
|
|
1163
|
+
// phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url -- Uses wp_parse_url when running inside WordPress
|
|
1164
|
+
$urlParts = function_exists('wp_parse_url') ? wp_parse_url($originalPath) : parse_url($originalPath);
|
|
1149
1165
|
$path = $urlParts['path'] ?? '/';
|
|
1150
1166
|
$finalQuery = $incomingQuery;
|
|
1151
1167
|
|
|
@@ -1326,7 +1342,8 @@ class RequestUtils
|
|
|
1326
1342
|
|
|
1327
1343
|
// Utilisation de la partie stable du fingerprint matériel plutôt que l'ID de cookie volatil
|
|
1328
1344
|
$currentDeviceHash = self::getCompositeDeviceHash($context);
|
|
1329
|
-
$
|
|
1345
|
+
$stablePart = self::extractStablePart($currentDeviceHash);
|
|
1346
|
+
$stableFpId = !empty($stablePart) ? (string)FingerprintBuilder::cyrb53($stablePart) : (!empty($deviceId) ? $deviceId : (string)FingerprintBuilder::cyrb53($currentDeviceHash));
|
|
1330
1347
|
|
|
1331
1348
|
$currentDeviceContributions = $subnetData['highScoreDevices'][$stableFpId] ?? 0;
|
|
1332
1349
|
if ($currentDeviceContributions < 1 && $finalScore < 95) {
|