@anonympins/fingerprint 0.6.7 → 0.7.1
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 +33 -0
- package/README.md +3 -7
- package/composer.json +8 -1
- package/package.json +1 -1
- package/public/fingerprint-wordpress.zip +0 -0
- package/public/fp.wasm +0 -0
- package/src/js/build-client.js +9 -11
- package/src/js/dynamic-wasm.js +7 -7
- package/src/js/fingerprint.builder.js +37 -38
- package/src/js/fingerprint.client.js +236 -152
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +517 -101
- package/src/js/fingerprint.utils.js +213 -213
- package/src/js/library.js +355 -384
- package/src/js/mongodb-store.js +1 -1
- package/src/js/optimization.worker.js +27 -27
- package/src/js/pow.solver.inline.js +206 -62
- package/src/js/pow.solver.js +235 -101
- package/src/js/pow.worker.js +48 -26
- package/src/js/redis-store.js +1 -1
- package/src/js/tests/fingerprint.client.init.test.js +146 -143
- package/src/js/tests/fingerprint.test.js +101 -40
- package/src/js/tests/ip-reputation.test.js +1 -0
- package/src/js/upow-model-task.js +14 -15
- package/src/php/AutoTuner.php +34 -34
- package/src/php/Challenge/ChallengeUtils.php +204 -32
- package/src/php/Config/SecurityProfiles.php +21 -0
- package/src/php/DirectFingerprint.php +190 -145
- package/src/php/FingerprintBuilder.php +29 -30
- package/src/php/FingerprintClient.php +24 -25
- package/src/php/FingerprintEngine.php +1836 -1880
- package/src/php/Ja3AnomalyDetector.php +258 -227
- package/src/php/Optimization/FunctionRegistry.php +7 -7
- package/src/php/Optimization/Optimization.php +24 -24
- package/src/php/Optimization/OptimizationOperators.php +28 -27
- package/src/php/Optimization/ProblemInitializers.php +52 -52
- package/src/php/ProblemManager.php +31 -34
- package/src/php/RequestContext.php +8 -1
- package/src/php/Store/IStore.php +7 -8
- package/src/php/Store/InMemoryStore.php +66 -66
- package/src/php/Store/MongoDbStore.php +5 -5
- package/src/php/Store/RedisStore.php +5 -5
- package/src/php/Store/StoreManager.php +35 -35
- package/src/php/Tests/ChallengeUtilsTest.php +453 -453
- package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
- package/src/php/Tests/FingerprintEngineTest.php +662 -693
- package/src/php/Tests/IpReputationTest.php +10 -0
- package/src/php/Tests/MaliciousPatternsTest.php +103 -103
- package/src/php/Tests/MetricsTest.php +97 -97
- package/src/php/Tests/ProblemManagerTest.php +376 -376
- package/src/php/Tests/QuicFingerprintTest.php +53 -53
- package/src/php/Tests/RequestUtilsTest.php +471 -471
- package/src/php/Tests/TLSClientHelloParserTest.php +177 -177
- package/src/php/Tests/config/ed25519_key.json +3 -3
- package/src/php/Utils/Env.php +49 -49
- package/src/php/Utils/MaliciousPatterns.php +74 -74
- package/src/php/Utils/MetricsManager.php +209 -209
- package/src/php/Utils/RequestUtils.php +236 -19
- package/src/php/Utils/TLSClientHelloParser.php +369 -212
- package/src/php/WordPress/WpDbStore.php +155 -0
- package/src/php/WordPress/fingerprint-wordpress.php +217 -0
- package/src/php/WordPress/package.php +123 -0
- package/src/php/bin/auto-tune.php +118 -118
|
@@ -1,228 +1,259 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
namespace Anonympins\Fingerprint;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
*
|
|
9
|
-
*/
|
|
10
|
-
class Ja3AnomalyDetector
|
|
11
|
-
{
|
|
12
|
-
//
|
|
13
|
-
private const GREASE_VALUES = [
|
|
14
|
-
2570, 6682, 10794, 14906, 19018, 23130, 27242, 31354,
|
|
15
|
-
35466, 39578, 43690, 47802, 51914, 55926, 60038, 64150
|
|
16
|
-
];
|
|
17
|
-
|
|
18
|
-
//
|
|
19
|
-
private const TLS_FINGERPRINT_DB = [
|
|
20
|
-
'e188a442b87f422c5a1e80b05399435b' => ['Chrome'],
|
|
21
|
-
'd8e35855049321c6042a4325c697858f' => ['Chrome'],
|
|
22
|
-
'a9f90958d44533748c139a5d1895b925' => ['Chrome'],
|
|
23
|
-
'3b5379916d2b3882253c42885956a350' => ['Chrome'],
|
|
24
|
-
'59822058c95c33d2d06e52f410855c8c' => ['Chrome'],
|
|
25
|
-
'b386946a5a586163c7c533636b45c355' => ['Firefox'],
|
|
26
|
-
'66236495a523c1785f8f3a105b248b11' => ['Firefox'],
|
|
27
|
-
'b73d470006575b5e35167a0b5a8540e2' => ['Firefox'],
|
|
28
|
-
'8443d7562933834333943465d52363cf' => ['Firefox'],
|
|
29
|
-
'b633f21d532d35967c8753c38536b4d3' => ['Safari'],
|
|
30
|
-
'4d7a28d5f55b359b69100a311013f03e' => ['Safari', 'Chrome', 'Firefox'],
|
|
31
|
-
'8dd3d7532873575314df23c447543001' => ['Safari', 'Chrome', 'Firefox'],
|
|
32
|
-
//
|
|
33
|
-
'47344a349b75c4e82333475553b5f358' => ['Python'],
|
|
34
|
-
'b29587b8a143c42546133ad7704b3310' => ['Go'],
|
|
35
|
-
'd435b5223b2884c5a832b842637e245f' => ['Java'],
|
|
36
|
-
'c72366b9551263d990b7fa574225332c' => ['curl'],
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*/
|
|
43
|
-
public static function parseJa3(string $ja3String): ?array
|
|
44
|
-
{
|
|
45
|
-
if (empty($ja3String)) {
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
$parts = explode(',', $ja3String);
|
|
50
|
-
if (count($parts) !== 5) {
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return [
|
|
55
|
-
'tlsVersion' => (int)$parts[0],
|
|
56
|
-
'ciphers' => $parts[1] !== '' ? array_map('intval', explode('-', $parts[1])) : [],
|
|
57
|
-
'extensions' => $parts[2] !== '' ? array_map('intval', explode('-', $parts[2])) : [],
|
|
58
|
-
'curves' => $parts[3] !== '' ? array_map('intval', explode('-', $parts[3])) : [],
|
|
59
|
-
'points' => $parts[4] !== '' ? array_map('intval', explode('-', $parts[4])) : []
|
|
60
|
-
];
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
*
|
|
65
|
-
*/
|
|
66
|
-
public static function hasGrease(array $values): bool
|
|
67
|
-
{
|
|
68
|
-
foreach ($values as $val) {
|
|
69
|
-
if (in_array($val, self::GREASE_VALUES, true)) {
|
|
70
|
-
return true;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
*
|
|
78
|
-
*/
|
|
79
|
-
public static function getBrowserFamily(string $userAgent): ?string
|
|
80
|
-
{
|
|
81
|
-
$ua = strtolower($userAgent);
|
|
82
|
-
if (strpos($ua, 'edg') !== false) {
|
|
83
|
-
return 'Edge';
|
|
84
|
-
}
|
|
85
|
-
if (strpos($ua, 'chrome') !== false) {
|
|
86
|
-
return 'Chrome';
|
|
87
|
-
}
|
|
88
|
-
if (strpos($ua, 'firefox') !== false) {
|
|
89
|
-
return 'Firefox';
|
|
90
|
-
}
|
|
91
|
-
if (strpos($ua, 'safari') !== false) {
|
|
92
|
-
return 'Safari';
|
|
93
|
-
}
|
|
94
|
-
return null;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* @param string|null $ja3Hash
|
|
101
|
-
* @param string|null $ja3Raw
|
|
102
|
-
* @param string $userAgent
|
|
103
|
-
* @param string $httpVersion
|
|
104
|
-
* @param object|null $cacheInstance
|
|
105
|
-
* @return int
|
|
106
|
-
*/
|
|
107
|
-
public static function getJa3AnomalyScore(
|
|
108
|
-
?string $ja3Hash,
|
|
109
|
-
?string $ja3Raw,
|
|
110
|
-
string $userAgent,
|
|
111
|
-
string $httpVersion,
|
|
112
|
-
?object $cacheInstance = null
|
|
113
|
-
): int {
|
|
114
|
-
$score = 0;
|
|
115
|
-
$claimedBrowser = self::getBrowserFamily($userAgent);
|
|
116
|
-
$isHumanBrowser = in_array($claimedBrowser, ['Chrome', 'Firefox', 'Safari', 'Edge'], true);
|
|
117
|
-
|
|
118
|
-
// ---
|
|
119
|
-
if ($ja3Hash && strlen($ja3Hash) === 32) {
|
|
120
|
-
if (isset(self::TLS_FINGERPRINT_DB[$ja3Hash])) {
|
|
121
|
-
$expectedBrowsers = self::TLS_FINGERPRINT_DB[$ja3Hash];
|
|
122
|
-
|
|
123
|
-
//
|
|
124
|
-
$isLibrary = array_intersect($expectedBrowsers, ['Python', 'Go', 'Java', 'curl']);
|
|
125
|
-
if (!empty($isLibrary) && $isHumanBrowser) {
|
|
126
|
-
$score = max($score, 90); //
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
//
|
|
130
|
-
if ($claimedBrowser !== null) {
|
|
131
|
-
$matched = false;
|
|
132
|
-
foreach ($expectedBrowsers as $expected) {
|
|
133
|
-
if (stripos($claimedBrowser, $expected) === 0) {
|
|
134
|
-
$matched = true;
|
|
135
|
-
break;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
if (!$matched) {
|
|
139
|
-
$score = max($score, 80); //
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
//
|
|
145
|
-
if ($cacheInstance && $claimedBrowser !== null && method_exists($cacheInstance, 'get') && method_exists($cacheInstance, 'set')) {
|
|
146
|
-
$cacheKey = "ja3-browsers:" . $ja3Hash;
|
|
147
|
-
|
|
148
|
-
try {
|
|
149
|
-
$rawCached = $cacheInstance->get($cacheKey);
|
|
150
|
-
$seenBrowsers = $rawCached ? json_decode((string)$rawCached, true) : [];
|
|
151
|
-
if (!is_array($seenBrowsers)) {
|
|
152
|
-
$seenBrowsers = [];
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
if (!in_array($claimedBrowser, $seenBrowsers, true)) {
|
|
156
|
-
$seenBrowsers[] = $claimedBrowser;
|
|
157
|
-
// Cache
|
|
158
|
-
if (method_exists($cacheInstance, 'setex')) {
|
|
159
|
-
$cacheInstance->setex($cacheKey, 86400, json_encode($seenBrowsers));
|
|
160
|
-
} else {
|
|
161
|
-
$cacheInstance->set($cacheKey, json_encode($seenBrowsers), 86400);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
//
|
|
166
|
-
if (count($seenBrowsers) > 1) {
|
|
167
|
-
$score = max($score, 85);
|
|
168
|
-
}
|
|
169
|
-
} catch (\Throwable $e) {
|
|
170
|
-
//
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// ---
|
|
176
|
-
if ($ja3Raw) {
|
|
177
|
-
$parsed = self::parseJa3($ja3Raw);
|
|
178
|
-
if ($parsed) {
|
|
179
|
-
//
|
|
180
|
-
if ($claimedBrowser === 'Chrome' || $claimedBrowser === 'Edge') {
|
|
181
|
-
$hasCiphersGrease = self::hasGrease($parsed['ciphers']);
|
|
182
|
-
$hasExtensionsGrease = self::hasGrease($parsed['extensions']);
|
|
183
|
-
|
|
184
|
-
if (!$hasCiphersGrease && !$hasExtensionsGrease) {
|
|
185
|
-
//
|
|
186
|
-
$score = max($score, 75);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
//
|
|
191
|
-
$isH2OrHigher = (
|
|
192
|
-
strpos($httpVersion, '2.0') !== false ||
|
|
193
|
-
strpos($httpVersion, 'HTTP/2') !== false ||
|
|
194
|
-
strpos($httpVersion, 'HTTP/3') !== false
|
|
195
|
-
);
|
|
196
|
-
$hasAlpnExtension = in_array(16, $parsed['extensions'], true);
|
|
197
|
-
|
|
198
|
-
if ($isH2OrHigher && !$hasAlpnExtension) {
|
|
199
|
-
//
|
|
200
|
-
$score = max($score, 70);
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
//
|
|
204
|
-
if ($isHumanBrowser && $parsed['tlsVersion'] < 771) {
|
|
205
|
-
$score = max($score, 80);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
return $score;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
$
|
|
221
|
-
$
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
$
|
|
226
|
-
|
|
227
|
-
$
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* TLS (JA3) fingerprint anomaly and spoofing detector in PHP.
|
|
9
|
+
*/
|
|
10
|
+
class Ja3AnomalyDetector
|
|
11
|
+
{
|
|
12
|
+
// Decimal GREASE values (RFC 8701) used by Chromium and Safari TLS stacks
|
|
13
|
+
private const GREASE_VALUES = [
|
|
14
|
+
2570, 6682, 10794, 14906, 19018, 23130, 27242, 31354,
|
|
15
|
+
35466, 39578, 43690, 47802, 51914, 55926, 60038, 64150
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
// Known JA3 MD5 signatures for baseline cross-layer verification
|
|
19
|
+
private const TLS_FINGERPRINT_DB = [
|
|
20
|
+
'e188a442b87f422c5a1e80b05399435b' => ['Chrome'],
|
|
21
|
+
'd8e35855049321c6042a4325c697858f' => ['Chrome'],
|
|
22
|
+
'a9f90958d44533748c139a5d1895b925' => ['Chrome'],
|
|
23
|
+
'3b5379916d2b3882253c42885956a350' => ['Chrome'],
|
|
24
|
+
'59822058c95c33d2d06e52f410855c8c' => ['Chrome'],
|
|
25
|
+
'b386946a5a586163c7c533636b45c355' => ['Firefox'],
|
|
26
|
+
'66236495a523c1785f8f3a105b248b11' => ['Firefox'],
|
|
27
|
+
'b73d470006575b5e35167a0b5a8540e2' => ['Firefox'],
|
|
28
|
+
'8443d7562933834333943465d52363cf' => ['Firefox'],
|
|
29
|
+
'b633f21d532d35967c8753c38536b4d3' => ['Safari'],
|
|
30
|
+
'4d7a28d5f55b359b69100a311013f03e' => ['Safari', 'Chrome', 'Firefox'],
|
|
31
|
+
'8dd3d7532873575314df23c447543001' => ['Safari', 'Chrome', 'Firefox'],
|
|
32
|
+
// Automated scraper and library fingerprints
|
|
33
|
+
'47344a349b75c4e82333475553b5f358' => ['Python'],
|
|
34
|
+
'b29587b8a143c42546133ad7704b3310' => ['Go'],
|
|
35
|
+
'd435b5223b2884c5a832b842637e245f' => ['Java'],
|
|
36
|
+
'c72366b9551263d990b7fa574225332c' => ['curl'],
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Parses a raw JA3 string.
|
|
41
|
+
* Expected format: "TLSVersion,Ciphers,Extensions,EllipticCurves,EllipticCurveFormats"
|
|
42
|
+
*/
|
|
43
|
+
public static function parseJa3(string $ja3String): ?array
|
|
44
|
+
{
|
|
45
|
+
if (empty($ja3String)) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
$parts = explode(',', $ja3String);
|
|
50
|
+
if (count($parts) !== 5) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return [
|
|
55
|
+
'tlsVersion' => (int)$parts[0],
|
|
56
|
+
'ciphers' => $parts[1] !== '' ? array_map('intval', explode('-', $parts[1])) : [],
|
|
57
|
+
'extensions' => $parts[2] !== '' ? array_map('intval', explode('-', $parts[2])) : [],
|
|
58
|
+
'curves' => $parts[3] !== '' ? array_map('intval', explode('-', $parts[3])) : [],
|
|
59
|
+
'points' => $parts[4] !== '' ? array_map('intval', explode('-', $parts[4])) : []
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Checks if a collection contains any GREASE value.
|
|
65
|
+
*/
|
|
66
|
+
public static function hasGrease(array $values): bool
|
|
67
|
+
{
|
|
68
|
+
foreach ($values as $val) {
|
|
69
|
+
if (in_array($val, self::GREASE_VALUES, true)) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Extracts browser family from User-Agent string.
|
|
78
|
+
*/
|
|
79
|
+
public static function getBrowserFamily(string $userAgent): ?string
|
|
80
|
+
{
|
|
81
|
+
$ua = strtolower($userAgent);
|
|
82
|
+
if (strpos($ua, 'edg') !== false) {
|
|
83
|
+
return 'Edge';
|
|
84
|
+
}
|
|
85
|
+
if (strpos($ua, 'chrome') !== false) {
|
|
86
|
+
return 'Chrome';
|
|
87
|
+
}
|
|
88
|
+
if (strpos($ua, 'firefox') !== false) {
|
|
89
|
+
return 'Firefox';
|
|
90
|
+
}
|
|
91
|
+
if (strpos($ua, 'safari') !== false) {
|
|
92
|
+
return 'Safari';
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Evaluates comprehensive JA3 spoofing and anomaly suspicion score.
|
|
99
|
+
*
|
|
100
|
+
* @param string|null $ja3Hash MD5 hash of JA3 fingerprint (32 hex characters).
|
|
101
|
+
* @param string|null $ja3Raw Raw unhashed JA3 string if available.
|
|
102
|
+
* @param string $userAgent Request User-Agent header.
|
|
103
|
+
* @param string $httpVersion HTTP protocol version string.
|
|
104
|
+
* @param object|null $cacheInstance Cache store for multi-UA stagnation detection.
|
|
105
|
+
* @return int Suspicion score between 0 and 100.
|
|
106
|
+
*/
|
|
107
|
+
public static function getJa3AnomalyScore(
|
|
108
|
+
?string $ja3Hash,
|
|
109
|
+
?string $ja3Raw,
|
|
110
|
+
string $userAgent,
|
|
111
|
+
string $httpVersion,
|
|
112
|
+
?object $cacheInstance = null
|
|
113
|
+
): int {
|
|
114
|
+
$score = 0;
|
|
115
|
+
$claimedBrowser = self::getBrowserFamily($userAgent);
|
|
116
|
+
$isHumanBrowser = in_array($claimedBrowser, ['Chrome', 'Firefox', 'Safari', 'Edge'], true);
|
|
117
|
+
|
|
118
|
+
// --- ANALYSIS 1: MD5 JA3 HASH VERIFICATION ---
|
|
119
|
+
if ($ja3Hash && strlen($ja3Hash) === 32) {
|
|
120
|
+
if (isset(self::TLS_FINGERPRINT_DB[$ja3Hash])) {
|
|
121
|
+
$expectedBrowsers = self::TLS_FINGERPRINT_DB[$ja3Hash];
|
|
122
|
+
|
|
123
|
+
// Case A: TLS matches known automated library but UA claims standard browser
|
|
124
|
+
$isLibrary = array_intersect($expectedBrowsers, ['Python', 'Go', 'Java', 'curl']);
|
|
125
|
+
if (!empty($isLibrary) && $isHumanBrowser) {
|
|
126
|
+
$score = max($score, 90); // High confidence spoofing
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Case B: Direct discrepancy between claimed browser and expected TLS stack
|
|
130
|
+
if ($claimedBrowser !== null) {
|
|
131
|
+
$matched = false;
|
|
132
|
+
foreach ($expectedBrowsers as $expected) {
|
|
133
|
+
if (stripos($claimedBrowser, $expected) === 0) {
|
|
134
|
+
$matched = true;
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (!$matched) {
|
|
139
|
+
$score = max($score, 80); // Browser family mismatch
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Case C: Stateful multi-UA stagnation detection
|
|
145
|
+
if ($cacheInstance && $claimedBrowser !== null && method_exists($cacheInstance, 'get') && method_exists($cacheInstance, 'set')) {
|
|
146
|
+
$cacheKey = "ja3-browsers:" . $ja3Hash;
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
$rawCached = $cacheInstance->get($cacheKey);
|
|
150
|
+
$seenBrowsers = $rawCached ? json_decode((string)$rawCached, true) : [];
|
|
151
|
+
if (!is_array($seenBrowsers)) {
|
|
152
|
+
$seenBrowsers = [];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!in_array($claimedBrowser, $seenBrowsers, true)) {
|
|
156
|
+
$seenBrowsers[] = $claimedBrowser;
|
|
157
|
+
// Cache for 24 hours (86400 seconds)
|
|
158
|
+
if (method_exists($cacheInstance, 'setex')) {
|
|
159
|
+
$cacheInstance->setex($cacheKey, 86400, json_encode($seenBrowsers));
|
|
160
|
+
} else {
|
|
161
|
+
$cacheInstance->set($cacheKey, json_encode($seenBrowsers), 86400);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Multiple rotating browser UAs emitted from identical TLS stack
|
|
166
|
+
if (count($seenBrowsers) > 1) {
|
|
167
|
+
$score = max($score, 85);
|
|
168
|
+
}
|
|
169
|
+
} catch (\Throwable $e) {
|
|
170
|
+
// Fault-tolerant fallback on cache error
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// --- ANALYSIS 2: DEEP INSPECTION ON RAW JA3 STRING ---
|
|
176
|
+
if ($ja3Raw) {
|
|
177
|
+
$parsed = self::parseJa3($ja3Raw);
|
|
178
|
+
if ($parsed) {
|
|
179
|
+
// Check A: GREASE presence for Chrome / Edge
|
|
180
|
+
if ($claimedBrowser === 'Chrome' || $claimedBrowser === 'Edge') {
|
|
181
|
+
$hasCiphersGrease = self::hasGrease($parsed['ciphers']);
|
|
182
|
+
$hasExtensionsGrease = self::hasGrease($parsed['extensions']);
|
|
183
|
+
|
|
184
|
+
if (!$hasCiphersGrease && !$hasExtensionsGrease) {
|
|
185
|
+
// Modern Chromium missing GREASE indicates spoofing
|
|
186
|
+
$score = max($score, 75);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check B: HTTP/2 or HTTP/3 without ALPN extension (Extension 16)
|
|
191
|
+
$isH2OrHigher = (
|
|
192
|
+
strpos($httpVersion, '2.0') !== false ||
|
|
193
|
+
strpos($httpVersion, 'HTTP/2') !== false ||
|
|
194
|
+
strpos($httpVersion, 'HTTP/3') !== false
|
|
195
|
+
);
|
|
196
|
+
$hasAlpnExtension = in_array(16, $parsed['extensions'], true);
|
|
197
|
+
|
|
198
|
+
if ($isH2OrHigher && !$hasAlpnExtension) {
|
|
199
|
+
// HTTP/2 active at server level but missing from client ALPN extension
|
|
200
|
+
$score = max($score, 70);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Check C: Deprecated TLS version negotiated by claimed modern browser
|
|
204
|
+
if ($isHumanBrowser && $parsed['tlsVersion'] < 771) {
|
|
205
|
+
$score = max($score, 80);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return $score;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Correlates low-level TCP RTT with application layer latency to detect residential proxy hops.
|
|
215
|
+
* @param RequestContext $context
|
|
216
|
+
* @return int Suspicion score
|
|
217
|
+
*/
|
|
218
|
+
public static function getRttProxyScore(RequestContext $context): int
|
|
219
|
+
{
|
|
220
|
+
$tcpRttHeader = $context->headers['x-tcp-rtt'] ?? $context->headers['x-real-rtt'] ?? null;
|
|
221
|
+
$tcpRtt = $tcpRttHeader !== null ? (int)$tcpRttHeader : null;
|
|
222
|
+
|
|
223
|
+
$behaviorHeader = $context->headers['x-behavior-metrics'] ?? null;
|
|
224
|
+
if ($behaviorHeader) {
|
|
225
|
+
$metrics = json_decode($behaviorHeader, true);
|
|
226
|
+
if (json_last_error() === JSON_ERROR_NONE && isset($metrics['clientTimestamp'])) {
|
|
227
|
+
$clientTimestamp = (int)$metrics['clientTimestamp'];
|
|
228
|
+
$appLatency = $context->requestTimestamp - $clientTimestamp;
|
|
229
|
+
if ($tcpRtt !== null && $tcpRtt > 0) {
|
|
230
|
+
$clientToProxyDelta = $appLatency - $tcpRtt;
|
|
231
|
+
if ($tcpRtt < 35 && $clientToProxyDelta > 150) {
|
|
232
|
+
return 85;
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
if ($appLatency > 350) {
|
|
236
|
+
return 40;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// --- EXEMPLE D'UTILISATION PRATIQUE ---
|
|
246
|
+
/*
|
|
247
|
+
$userAgent = $_SERVER['HTTP_USER_AGENT'] ?? '';
|
|
248
|
+
$httpVersion = $_SERVER['SERVER_PROTOCOL'] ?? '';
|
|
249
|
+
|
|
250
|
+
// Récupération des en-têtes injectés par votre Reverse-Proxy (Nginx, HAProxy, etc.)
|
|
251
|
+
$ja3Hash = $_SERVER['HTTP_X_JA3_HASH'] ?? null;
|
|
252
|
+
$ja3Raw = $_SERVER['HTTP_X_JA3_RAW'] ?? null;
|
|
253
|
+
|
|
254
|
+
// Redis facultatif pour la détection stateful de rotation UA
|
|
255
|
+
$redis = new \Redis();
|
|
256
|
+
$redis->connect('127.0.0.1', 6379);
|
|
257
|
+
|
|
258
|
+
$suspicionScore = Ja3AnomalyDetector::getJa3AnomalyScore($ja3Hash, $ja3Raw, $userAgent, $httpVersion, $redis);
|
|
228
259
|
*/
|
|
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
|
|
5
5
|
namespace Anonympins\Fingerprint\Optimization;
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* Registry to safely expose optimization library routines to task runners.
|
|
9
9
|
*/
|
|
10
10
|
class FunctionRegistry
|
|
11
11
|
{
|
|
@@ -13,17 +13,17 @@ class FunctionRegistry
|
|
|
13
13
|
private static array $functions = [];
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* Initializes registry with built-in solvers and evaluators.
|
|
17
17
|
*/
|
|
18
18
|
private static function initialize(): void
|
|
19
19
|
{
|
|
20
20
|
if (empty(self::$functions)) {
|
|
21
|
-
//
|
|
21
|
+
// Evaluators & Scoring Adapters
|
|
22
22
|
self::$functions['tsp.calculateEnergy'] = [OptimizationUtils::class, 'evaluatePathDistance'];
|
|
23
23
|
self::$functions['portfolio.calculateMetrics'] = [OptimizationOperators::class, 'calculatePortfolioMetrics'];
|
|
24
24
|
self::$functions['facility.calculateEnergy'] = [OptimizationOperators::class, 'evaluateFacilityLocation'];
|
|
25
25
|
|
|
26
|
-
//
|
|
26
|
+
// Full Optimization Solvers
|
|
27
27
|
self::$functions['tsp.solve'] = [OptimizationOperators::class, 'solveTSP'];
|
|
28
28
|
self::$functions['portfolio.solve'] = [OptimizationOperators::class, 'solvePortfolio'];
|
|
29
29
|
self::$functions['fraud.solve'] = [OptimizationOperators::class, 'solveFraudDetection'];
|
|
@@ -33,7 +33,7 @@ class FunctionRegistry
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
36
|
+
* Retrieves a function from the registry.
|
|
37
37
|
*/
|
|
38
38
|
public static function get(string $name): ?callable
|
|
39
39
|
{
|
|
@@ -41,7 +41,7 @@ class FunctionRegistry
|
|
|
41
41
|
return self::$functions[$name] ?? null;
|
|
42
42
|
}
|
|
43
43
|
/**
|
|
44
|
-
*
|
|
44
|
+
* Registers a custom function. Primarily intended for testing.
|
|
45
45
|
* @internal
|
|
46
46
|
* @param string $name
|
|
47
47
|
* @param callable $function
|
|
@@ -54,7 +54,7 @@ class FunctionRegistry
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
|
-
*
|
|
57
|
+
* Resets function registry. Intended for testing.
|
|
58
58
|
* @internal
|
|
59
59
|
*/
|
|
60
60
|
public static function __internal_resetRegistry(): void
|