@anonympins/fingerprint 0.7.4 → 0.8.0
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 +59 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/public/anonympins-bot-mitigation-pow.zip +0 -0
- package/src/js/dynamic-wasm.js +218 -218
- package/src/js/fingerprint.client.js +1784 -1738
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +489 -99
- package/src/js/fingerprint.utils.js +252 -2
- package/src/js/pow.worker.js +2 -2
- package/src/js/tests/cross_parity.test.js +3 -3
- package/src/js/tests/dns-circuit-breaker.test.js +6 -6
- package/src/js/tests/dynamic-wasm.test.js +77 -77
- package/src/js/tests/fingerprint.builder.test.js +76 -78
- package/src/js/tests/fingerprint.client.init.test.js +78 -0
- package/src/js/tests/fingerprint.client.test.js +16 -17
- package/src/js/tests/fingerprint.test.js +37 -12
- package/src/js/tests/ja3AnomalyDetector.test.js +135 -136
- package/src/js/tests/library.test.js +94 -95
- package/src/js/tests/obfuscation.test.js +8 -8
- package/src/js/tests/quicFingerprint.test.js +47 -0
- package/src/js/tests/tcpFingerprint.test.js +4 -4
- package/src/js/tests/threatIntel.test.js +40 -1
- package/src/js/upow-model-task.js +155 -112
- package/src/php/Challenge/ChallengeUtils.php +392 -86
- package/src/php/Config/SecurityProfiles.php +79 -61
- package/src/php/DirectFingerprint.php +55 -8
- package/src/php/FingerprintBuilder.php +13 -13
- package/src/php/FingerprintClient.php +6 -6
- package/src/php/FingerprintEngine.php +2122 -1690
- package/src/php/Ja3AnomalyDetector.php +22 -22
- package/src/php/Optimization/FunctionRegistry.php +4 -4
- package/src/php/Optimization/Optimization.php +6 -6
- package/src/php/RequestContext.php +11 -11
- package/src/php/Store/InMemoryStore.php +96 -66
- package/src/php/Store/MongoDbStore.php +2 -2
- package/src/php/Store/RedisStore.php +1 -1
- package/src/php/Store/StoreManager.php +48 -35
- package/src/php/Tests/ChallengeUtilsTest.php +31 -0
- package/src/php/Tests/DnsCircuitBreakerTest.php +105 -105
- package/src/php/Tests/FingerprintClientTest.php +70 -70
- package/src/php/Tests/PowTest.php +37 -39
- package/src/php/Tests/QuicFingerprintTest.php +115 -53
- package/src/php/Tests/ThreatIntelTest.php +34 -0
- package/src/php/Utils/BigInt.php +202 -202
- package/src/php/Utils/BlockList.php +13 -13
- package/src/php/Utils/Logger.php +19 -1
- package/src/php/Utils/MaliciousPatterns.php +6 -6
- package/src/php/Utils/MetricsManager.php +209 -209
- package/src/php/Utils/RequestUtils.php +362 -1
- package/src/php/Utils/TLSClientHelloParser.php +385 -369
- package/src/php/WordPress/WpDbStore.php +7 -7
- package/src/php/WordPress/anonympins-bot-mitigation-pow.php +1121 -0
- package/src/php/WordPress/fingerprint-anti-bot.php +0 -559
- package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.mo +0 -0
- package/src/php/WordPress/languages/anonympins-bot-mitigation-pow-fr_FR.po +232 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +297 -261
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +348 -267
- package/src/php/WordPress/package.php +80 -101
- package/src/php/WordPress/readme.txt +21 -5
- package/src/php/bin/auto-tune.php +1 -1
- package/public/fingerprint-anti-bot.zip +0 -0
|
@@ -116,6 +116,187 @@ class RequestUtils
|
|
|
116
116
|
return (string)$srv;
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Extracts Private Access Tokens (PAT / RFC 9578) from request headers.
|
|
121
|
+
*
|
|
122
|
+
* @param RequestContext $context
|
|
123
|
+
* @return array<int, string> Raw binary tokens.
|
|
124
|
+
*/
|
|
125
|
+
public static function extractPrivateAccessTokens(RequestContext $context): array
|
|
126
|
+
{
|
|
127
|
+
$tokens = [];
|
|
128
|
+
$authHeader = $context->getHeader('authorization');
|
|
129
|
+
if ($authHeader && stripos($authHeader, 'privatetoken ') === 0) {
|
|
130
|
+
if (preg_match('/token=(?:"([^"]+)"|([a-zA-Z0-9_\-+\/=]+))/i', $authHeader, $matches)) {
|
|
131
|
+
$rawB64 = !empty($matches[1]) ? $matches[1] : $matches[2];
|
|
132
|
+
$decoded = base64_decode(strtr($rawB64, '-_', '+/'), true);
|
|
133
|
+
if ($decoded !== false) {
|
|
134
|
+
$tokens[] = $decoded;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
$pstHeader = $context->getHeader('sec-private-state-token');
|
|
140
|
+
if ($pstHeader) {
|
|
141
|
+
foreach (explode(',', $pstHeader) as $part) {
|
|
142
|
+
$trimmed = trim($part);
|
|
143
|
+
if (!empty($trimmed)) {
|
|
144
|
+
$decoded = base64_decode(strtr($trimmed, '-_', '+/'), true);
|
|
145
|
+
if ($decoded !== false) {
|
|
146
|
+
$tokens[] = $decoded;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return $tokens;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Decodes an RFC 9578 Token structure:
|
|
156
|
+
* uint16_t token_type (2 bytes)
|
|
157
|
+
* uint8_t nonce[32] (32 bytes)
|
|
158
|
+
* uint8_t challenge_digest[32] (32 bytes)
|
|
159
|
+
* uint8_t token_key_id[32] (32 bytes)
|
|
160
|
+
* uint8_t authenticator[Nk] (>= 32 bytes)
|
|
161
|
+
*
|
|
162
|
+
* @param string $binary
|
|
163
|
+
* @return array<string, mixed>|null
|
|
164
|
+
*/
|
|
165
|
+
public static function parsePrivateAccessToken(string $binary): ?array
|
|
166
|
+
{
|
|
167
|
+
$len = strlen($binary);
|
|
168
|
+
if ($len < 98) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
$tokenType = unpack('n', substr($binary, 0, 2))[1];
|
|
173
|
+
$nonce = substr($binary, 2, 32);
|
|
174
|
+
$challengeDigest = substr($binary, 34, 32);
|
|
175
|
+
$tokenKeyId = substr($binary, 66, 32);
|
|
176
|
+
$authenticator = substr($binary, 98);
|
|
177
|
+
|
|
178
|
+
if ($authenticator === '') {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return [
|
|
183
|
+
'tokenType' => $tokenType,
|
|
184
|
+
'nonce' => bin2hex($nonce),
|
|
185
|
+
'challengeDigest' => bin2hex($challengeDigest),
|
|
186
|
+
'tokenKeyId' => bin2hex($tokenKeyId),
|
|
187
|
+
'authenticator' => $authenticator,
|
|
188
|
+
'signedData' => substr($binary, 0, 98),
|
|
189
|
+
];
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Cryptographically validates a Private Access Token signature.
|
|
194
|
+
*
|
|
195
|
+
* @param array<string, mixed> $parsedToken
|
|
196
|
+
* @param array<string, mixed> $patConfig
|
|
197
|
+
* @return bool
|
|
198
|
+
*/
|
|
199
|
+
public static function verifyPatSignature(array $parsedToken, array $patConfig = []): bool
|
|
200
|
+
{
|
|
201
|
+
$trustedKeys = $patConfig['trustedKeys'] ?? [];
|
|
202
|
+
$keyId = $parsedToken['tokenKeyId'];
|
|
203
|
+
$publicKey = $trustedKeys[$keyId] ?? $trustedKeys[strtolower($keyId)] ?? ($patConfig['defaultPublicKey'] ?? null);
|
|
204
|
+
|
|
205
|
+
if (!$publicKey) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
$tokenType = (int)$parsedToken['tokenType'];
|
|
210
|
+
$signedData = $parsedToken['signedData'];
|
|
211
|
+
$authenticator = $parsedToken['authenticator'];
|
|
212
|
+
|
|
213
|
+
$pubKeyObj = openssl_pkey_get_public($publicKey);
|
|
214
|
+
if (!$pubKeyObj) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Type 0x0001 (Blind RSA 2048) / 0x0002 (Rate-Limited Blind RSA)
|
|
219
|
+
if ($tokenType === 1 || $tokenType === 2) {
|
|
220
|
+
// Try RSA-PSS with SHA-384, then SHA-256, then PKCS#1 v1.5
|
|
221
|
+
if (defined('OPENSSL_PKCS1_PSS_PADDING') && @openssl_verify($signedData, $authenticator, $pubKeyObj, OPENSSL_ALGO_SHA384)) {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
if (defined('OPENSSL_PKCS1_PSS_PADDING') && @openssl_verify($signedData, $authenticator, $pubKeyObj, OPENSSL_ALGO_SHA256)) {
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
if (@openssl_verify($signedData, $authenticator, $pubKeyObj, OPENSSL_ALGO_SHA256) === 1) {
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
} elseif ($tokenType === 3) {
|
|
231
|
+
return @openssl_verify($signedData, $authenticator, $pubKeyObj, OPENSSL_ALGO_SHA256) === 1;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Analyzes TCP MTU and fragmentation flags to detect network tunnels (VPN/Proxy).
|
|
239
|
+
* @param RequestContext $context The request context.
|
|
240
|
+
* @return array{'mtuAnomalyScore': float}
|
|
241
|
+
*/
|
|
242
|
+
public static function getMtuAnomalyScore(RequestContext $context): array
|
|
243
|
+
{
|
|
244
|
+
$mtuHeader = $context->getHeader('x-tcp-mtu-info');
|
|
245
|
+
$mssHeader = $context->getHeader('x-tcp-mss') ?? $context->getHeader('x-mss') ?? $context->getHeader('x-forwarded-mss');
|
|
246
|
+
|
|
247
|
+
$mtu = null;
|
|
248
|
+
$df = null;
|
|
249
|
+
|
|
250
|
+
if (!empty($mtuHeader) && is_string($mtuHeader)) {
|
|
251
|
+
$parts = explode(':', $mtuHeader);
|
|
252
|
+
if (count($parts) >= 2) {
|
|
253
|
+
$parsedMtu = filter_var($parts[0], FILTER_VALIDATE_INT);
|
|
254
|
+
$parsedDf = filter_var($parts[1], FILTER_VALIDATE_INT);
|
|
255
|
+
$mtu = $parsedMtu !== false ? $parsedMtu : null;
|
|
256
|
+
$df = $parsedDf !== false ? $parsedDf : null;
|
|
257
|
+
} elseif (count($parts) === 1 && is_numeric($parts[0])) {
|
|
258
|
+
$mtu = (int)$parts[0];
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Reconstitution depuis MSS (ex: FastCGI Nginx $tcpinfo_rcv_mss)
|
|
263
|
+
if ($mtu === null && !empty($mssHeader) && is_numeric($mssHeader)) {
|
|
264
|
+
$mss = (int)$mssHeader;
|
|
265
|
+
if ($mss > 0) {
|
|
266
|
+
$isV6 = filter_var($context->clientIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false;
|
|
267
|
+
$mtu = $mss + ($isV6 ? 60 : 40);
|
|
268
|
+
$df = 1;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if ($mtu === null) {
|
|
273
|
+
return ['mtuAnomalyScore' => 0.0];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
$score = 0.0;
|
|
277
|
+
|
|
278
|
+
// 1. Détection des tunnels (VPN WireGuard/OpenVPN, GRE, proxies résidentiels encapsulés)
|
|
279
|
+
if ($mtu > 500 && $mtu <= 1420) {
|
|
280
|
+
$score += 55.0;
|
|
281
|
+
} elseif ($mtu > 1420 && $mtu < 1492) {
|
|
282
|
+
$score += 30.0;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// 2. OS vs. Network Stack Inconsistency
|
|
286
|
+
$ua = $context->getHeader('user-agent') ?? '';
|
|
287
|
+
$uaParts = self::parseUserAgent($ua);
|
|
288
|
+
$os = $uaParts['os'] ?? null;
|
|
289
|
+
|
|
290
|
+
// Pénalités additionnelles en cas d'incohérence OS vs signature réseau.
|
|
291
|
+
if ($os) {
|
|
292
|
+
if (str_starts_with($os, 'Windows') && $mtu < 1492) $score += 20.0;
|
|
293
|
+
if ((str_starts_with($os, 'Android') || str_starts_with($os, 'iOS')) && $mtu < 1480) $score += 15.0;
|
|
294
|
+
if ($df === 0 && (str_starts_with($os, 'Windows') || str_starts_with($os, 'Mac') || str_starts_with($os, 'Linux'))) $score += 40.0;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return ['mtuAnomalyScore' => min(100.0, $score)];
|
|
298
|
+
}
|
|
299
|
+
|
|
119
300
|
/**
|
|
120
301
|
* Calcule un score d'incohérence entre la signature TLS (JA3) et le User-Agent.
|
|
121
302
|
* @return array{'tlsSpoofingScore': float}
|
|
@@ -177,6 +358,25 @@ class RequestUtils
|
|
|
177
358
|
}
|
|
178
359
|
}
|
|
179
360
|
|
|
361
|
+
// ECH & SNI Discrepancy Checks
|
|
362
|
+
$outerSni = $context->getHeader('x-ech-outer-sni');
|
|
363
|
+
$host = explode(':', $context->getHeader('host') ?? '')[0];
|
|
364
|
+
$hasEch = $context->getHeader('x-ech-present') === 'true' || $context->getHeader('x-has-ech') === 'true';
|
|
365
|
+
$echExpected = $context->getHeader('x-ech-expected') === 'true' || !empty($outerSni);
|
|
366
|
+
|
|
367
|
+
if (!empty($outerSni) && !empty($host) && strcasecmp($outerSni, $host) !== 0 && !$hasEch) {
|
|
368
|
+
return ['tlsSpoofingScore' => max($tlsSpoofingScore ?? 0.0, 60.0)];
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
$browserVer = (int)($claimedBrowserInfo['version'] ?? 0);
|
|
372
|
+
$isModernEchBrowser = (
|
|
373
|
+
(in_array($claimedBrowser, ['Chrome', 'Edge'], true) && $browserVer >= 119) ||
|
|
374
|
+
($claimedBrowser === 'Firefox' && $browserVer >= 118)
|
|
375
|
+
);
|
|
376
|
+
if ($context->isHttps && $isModernEchBrowser && $echExpected && !$hasEch) {
|
|
377
|
+
return ['tlsSpoofingScore' => max($tlsSpoofingScore ?? 0.0, 55.0)];
|
|
378
|
+
}
|
|
379
|
+
|
|
180
380
|
return ['tlsSpoofingScore' => 0.0];
|
|
181
381
|
}
|
|
182
382
|
|
|
@@ -585,8 +785,14 @@ class RequestUtils
|
|
|
585
785
|
|
|
586
786
|
if (str_contains($ua, 'Chrome') && !str_contains($ua, 'Edg')) {
|
|
587
787
|
$result['browser'] = 'Chrome';
|
|
788
|
+
if (preg_match('/Chrome\/(\d+)/', $ua, $matches)) {
|
|
789
|
+
$result['version'] = (int)$matches[1];
|
|
790
|
+
}
|
|
588
791
|
} elseif (str_contains($ua, 'Firefox')) {
|
|
589
792
|
$result['browser'] = 'Firefox';
|
|
793
|
+
if (preg_match('/Firefox\/(\d+)/', $ua, $matches)) {
|
|
794
|
+
$result['version'] = (int)$matches[1];
|
|
795
|
+
}
|
|
590
796
|
} elseif (str_contains($ua, 'Safari') && !str_contains($ua, 'Chrome')) {
|
|
591
797
|
$result['browser'] = 'Safari';
|
|
592
798
|
} elseif (str_contains($ua, 'Edg')) {
|
|
@@ -2058,6 +2264,52 @@ class RequestUtils
|
|
|
2058
2264
|
$uaParts = self::parseUserAgent($ua);
|
|
2059
2265
|
$browser = $uaParts['browser'] ?? null;
|
|
2060
2266
|
|
|
2267
|
+
$serverProtocol = strtoupper($context->httpVersion ?? '');
|
|
2268
|
+
$isH2 = str_starts_with($serverProtocol, 'HTTP/2') || str_starts_with($serverProtocol, '2');
|
|
2269
|
+
$isH3 = str_starts_with($serverProtocol, 'HTTP/3') || str_starts_with($serverProtocol, '3');
|
|
2270
|
+
$isH1 = str_starts_with($serverProtocol, 'HTTP/1') || str_starts_with($serverProtocol, '1');
|
|
2271
|
+
|
|
2272
|
+
// 1. Violation RFC 7540 (HTTP/2) & RFC 9114 (HTTP/3) : en-têtes hop-by-hop interdits
|
|
2273
|
+
$hasIllegalHopByHop = $context->getHeader('connection') !== null
|
|
2274
|
+
|| $context->getHeader('keep-alive') !== null
|
|
2275
|
+
|| $context->getHeader('proxy-connection') !== null;
|
|
2276
|
+
|
|
2277
|
+
if ($isH2 && $hasIllegalHopByHop) {
|
|
2278
|
+
$http2Anomaly += 70.0;
|
|
2279
|
+
}
|
|
2280
|
+
if ($isH3 && $hasIllegalHopByHop) {
|
|
2281
|
+
$quicAnomaly += 75.0;
|
|
2282
|
+
}
|
|
2283
|
+
|
|
2284
|
+
// 2. Corrélation avec la télémétrie client W3C Navigation Timing
|
|
2285
|
+
$clientProtocol = null;
|
|
2286
|
+
$behaviorHeader = $context->getHeader('x-behavior-metrics');
|
|
2287
|
+
if ($behaviorHeader && is_string($behaviorHeader) && str_starts_with($behaviorHeader, '{')) {
|
|
2288
|
+
$metrics = json_decode($behaviorHeader, true);
|
|
2289
|
+
if (is_array($metrics)) {
|
|
2290
|
+
$clientProtocol = $metrics['protocol'] ?? $metrics['nextHopProtocol'] ?? null;
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
if ($clientProtocol && is_string($clientProtocol)) {
|
|
2294
|
+
$clientProtocol = strtolower(trim($clientProtocol));
|
|
2295
|
+
$hasProxy = $context->getHeader('via') || $context->getHeader('forwarded') || $context->getHeader('x-forwarded-for');
|
|
2296
|
+
if ($clientProtocol === 'h3' && $isH1 && !$hasProxy) {
|
|
2297
|
+
$quicAnomaly += 80.0;
|
|
2298
|
+
} elseif ($clientProtocol === 'h2' && $isH1 && !$hasProxy) {
|
|
2299
|
+
$http2Anomaly += 75.0;
|
|
2300
|
+
} elseif ($clientProtocol === 'http/1.1' && ($isH2 || $isH3)) {
|
|
2301
|
+
$http2Anomaly += 60.0;
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
// 3. Rétrogradation protocolaire (Navigateur moderne naviguant en HTTP/1.1 sur HTTPS)
|
|
2306
|
+
if ($context->isHttps && $browser && $isH1) {
|
|
2307
|
+
$hasProxy = $context->getHeader('via') || $context->getHeader('forwarded') || $context->getHeader('x-forwarded-proto') || $context->getHeader('x-forwarded-for');
|
|
2308
|
+
if (!$hasProxy) {
|
|
2309
|
+
$http2Anomaly += 50.0;
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
|
|
2061
2313
|
if ($browser) {
|
|
2062
2314
|
// HTTP/2 Anomaly logic
|
|
2063
2315
|
$h2Fp = $context->getHeader('x-http2-fingerprint') ?? $context->http2Fingerprint ?? null;
|
|
@@ -2075,11 +2327,40 @@ class RequestUtils
|
|
|
2075
2327
|
if ($headerOrder && $headerOrder !== 'm,a,s,p') $http2Anomaly += 60.0;
|
|
2076
2328
|
if ($connWindow === 65535 || $connWindow === 65536) $http2Anomaly += 40.0;
|
|
2077
2329
|
if ($streamPriority === '0' || $streamPriority === '') $http2Anomaly += 50.0;
|
|
2330
|
+
|
|
2331
|
+
$priorityDeps = substr_count($streamPriority, ',');
|
|
2332
|
+
if (str_contains($streamPriority, ',') && $priorityDeps < 2 && !empty($streamPriority) && $streamPriority !== '0') {
|
|
2333
|
+
$http2Anomaly += 35.0;
|
|
2334
|
+
}
|
|
2078
2335
|
} elseif ($isFirefox) {
|
|
2079
2336
|
if ($headerOrder && $headerOrder !== 'm,s,p,a') $http2Anomaly += 60.0;
|
|
2080
2337
|
} elseif ($isSafari) {
|
|
2081
2338
|
if ($headerOrder && $headerOrder !== 'm,s,p,a') $http2Anomaly += 60.0;
|
|
2082
2339
|
}
|
|
2340
|
+
|
|
2341
|
+
// Analyse de la structure des dépendances de flux HTTP/2 (RFC 7540)
|
|
2342
|
+
if (!empty($streamPriority)) {
|
|
2343
|
+
$priorityEntries = array_filter(array_map('trim', explode(',', $streamPriority)));
|
|
2344
|
+
foreach ($priorityEntries as $entry) {
|
|
2345
|
+
$sub = explode(':', $entry);
|
|
2346
|
+
if (count($sub) >= 4) {
|
|
2347
|
+
$streamId = (int)$sub[0];
|
|
2348
|
+
$exclusive = (int)$sub[1];
|
|
2349
|
+
$depStreamId = (int)$sub[2];
|
|
2350
|
+
$weight = (int)$sub[3];
|
|
2351
|
+
|
|
2352
|
+
if ($streamId === $depStreamId && $streamId > 0) {
|
|
2353
|
+
$http2Anomaly += 60.0;
|
|
2354
|
+
}
|
|
2355
|
+
if ($weight < 1 || $weight > 256) {
|
|
2356
|
+
$http2Anomaly += 45.0;
|
|
2357
|
+
}
|
|
2358
|
+
if ($isChromium && $depStreamId === 0 && $weight === 16 && $exclusive === 0) {
|
|
2359
|
+
$http2Anomaly += 55.0;
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2083
2364
|
}
|
|
2084
2365
|
}
|
|
2085
2366
|
|
|
@@ -2111,7 +2392,32 @@ class RequestUtils
|
|
|
2111
2392
|
if ($isChromium) {
|
|
2112
2393
|
if ($maxData > 0 && $maxData < 1048576) $quicAnomaly += 40.0;
|
|
2113
2394
|
if ($maxStreams > 0 && $maxStreams !== 100) $quicAnomaly += 30.0;
|
|
2114
|
-
|
|
2395
|
+
|
|
2396
|
+
if (!empty($priorityOrder)) {
|
|
2397
|
+
if (preg_match('/\bu=(-?\d+)\b/', $priorityOrder, $uMatch)) {
|
|
2398
|
+
$urgency = (int)$uMatch[1];
|
|
2399
|
+
if ($urgency < 0 || $urgency > 7) {
|
|
2400
|
+
$quicAnomaly += 50.0;
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
if (preg_match('/\bi=\d+\b/', $priorityOrder)) {
|
|
2404
|
+
$quicAnomaly += 40.0;
|
|
2405
|
+
}
|
|
2406
|
+
if (preg_match('/\bw=\d+\b/', $priorityOrder)) {
|
|
2407
|
+
$quicAnomaly += 45.0;
|
|
2408
|
+
}
|
|
2409
|
+
}
|
|
2410
|
+
|
|
2411
|
+
if (!empty($priorityOrder)) {
|
|
2412
|
+
if (str_contains($priorityOrder, 'u=') && !str_contains($priorityOrder, 'i')) {
|
|
2413
|
+
$quicAnomaly += 40.0;
|
|
2414
|
+
} elseif (!str_contains($priorityOrder, 'u=')) {
|
|
2415
|
+
$quicAnomaly += 30.0;
|
|
2416
|
+
}
|
|
2417
|
+
if ($priorityOrder === 'p' || $priorityOrder === 'i') {
|
|
2418
|
+
$quicAnomaly += 50.0;
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2115
2421
|
if ($bidiLocal > 0 && ($bidiLocal < 524288 || $bidiLocal === 262144)) $quicAnomaly += 40.0;
|
|
2116
2422
|
if ($bidiRemote > 0 && ($bidiRemote < 524288 || $bidiRemote === 262144)) $quicAnomaly += 30.0;
|
|
2117
2423
|
|
|
@@ -2123,6 +2429,11 @@ class RequestUtils
|
|
|
2123
2429
|
if ($sIdx !== null && $sIdx !== 0) $quicAnomaly += 50.0;
|
|
2124
2430
|
if ($mIdx !== null && $sIdx !== null && $mIdx < $sIdx) $quicAnomaly += 60.0;
|
|
2125
2431
|
if ($pIdx !== null && $sIdx !== null && $pIdx < $sIdx) $quicAnomaly += 60.0;
|
|
2432
|
+
|
|
2433
|
+
$sCount = count(array_filter($frameOrder, fn($f) => in_array($f, ['s', 'settings', '4'], true)));
|
|
2434
|
+
if ($sCount > 1) {
|
|
2435
|
+
$quicAnomaly += 50.0;
|
|
2436
|
+
}
|
|
2126
2437
|
}
|
|
2127
2438
|
} elseif ($isFirefox) {
|
|
2128
2439
|
if ($maxData > 0 && $maxData > 5000000) $quicAnomaly += 40.0;
|
|
@@ -2151,6 +2462,56 @@ class RequestUtils
|
|
|
2151
2462
|
}
|
|
2152
2463
|
}
|
|
2153
2464
|
|
|
2465
|
+
// HPACK / QPACK Compression Ratio Analysis
|
|
2466
|
+
$compressionInfo = $context->getHeader('x-compression-info');
|
|
2467
|
+
if (!empty($compressionInfo)) {
|
|
2468
|
+
$info = [];
|
|
2469
|
+
foreach (explode(',', $compressionInfo) as $item) {
|
|
2470
|
+
$kv = explode(':', $item, 2);
|
|
2471
|
+
if (count($kv) === 2) {
|
|
2472
|
+
$info[trim($kv[0])] = (float)trim($kv[1]);
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
$reqCount = $info['req_count'] ?? 0;
|
|
2476
|
+
$hpackRatio = $info['hpack_ratio'] ?? null;
|
|
2477
|
+
$qpackRatio = $info['qpack_ratio'] ?? null;
|
|
2478
|
+
$dynamicEntries = isset($info['dynamic_table_entries']) ? (int)$info['dynamic_table_entries'] : (isset($info['table_entries']) ? (int)$info['table_entries'] : null);
|
|
2479
|
+
$dynamicHits = isset($info['dynamic_hits']) ? (int)$info['dynamic_hits'] : null;
|
|
2480
|
+
$dynamicTableSize = isset($info['dynamic_table_size']) ? (int)$info['dynamic_table_size'] : null;
|
|
2481
|
+
|
|
2482
|
+
$claimedFamily = explode('/', $browser ?? '')[0];
|
|
2483
|
+
$isHuman = in_array($claimedFamily, ['Chrome', 'Firefox', 'Safari', 'Edge'], true);
|
|
2484
|
+
|
|
2485
|
+
if ($reqCount > 3 && $isHuman) {
|
|
2486
|
+
if ($hpackRatio !== null) {
|
|
2487
|
+
if ($hpackRatio < 0.4) {
|
|
2488
|
+
$http2Anomaly += (1.0 - $hpackRatio) * 50.0;
|
|
2489
|
+
} elseif ($hpackRatio > 0.75) {
|
|
2490
|
+
$http2Anomaly += min(50.0, ($hpackRatio - 0.5) * 100.0);
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
if ($qpackRatio !== null) {
|
|
2494
|
+
if ($qpackRatio < 0.4) {
|
|
2495
|
+
$quicAnomaly += (1.0 - $qpackRatio) * 50.0;
|
|
2496
|
+
} elseif ($qpackRatio > 0.75) {
|
|
2497
|
+
$quicAnomaly += min(50.0, ($qpackRatio - 0.5) * 100.0);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
if ($dynamicEntries !== null && $dynamicEntries === 0) {
|
|
2501
|
+
$http2Anomaly += 45.0;
|
|
2502
|
+
$quicAnomaly += 45.0;
|
|
2503
|
+
}
|
|
2504
|
+
if ($dynamicHits !== null && $dynamicHits === 0) {
|
|
2505
|
+
$http2Anomaly += 35.0;
|
|
2506
|
+
$quicAnomaly += 35.0;
|
|
2507
|
+
}
|
|
2508
|
+
if ($dynamicTableSize !== null && $dynamicTableSize === 0) {
|
|
2509
|
+
$http2Anomaly += 40.0;
|
|
2510
|
+
$quicAnomaly += 40.0;
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2154
2515
|
return [
|
|
2155
2516
|
'protocolAnomalyScore' => max(
|
|
2156
2517
|
0.0,
|