@anonympins/fingerprint 0.7.0 → 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 +17 -0
- package/README.md +1 -1
- package/composer.json +8 -1
- package/package.json +1 -1
- package/public/fingerprint-wordpress.zip +0 -0
- package/src/js/build-client.js +8 -10
- package/src/js/fingerprint.builder.js +37 -38
- package/src/js/fingerprint.client.js +1531 -1463
- package/src/js/fingerprint.client.obfuscated.js +1 -0
- package/src/js/fingerprint.js +152 -10
- package/src/js/fingerprint.utils.js +213 -213
- package/src/js/library.js +340 -376
- package/src/js/mongodb-store.js +1 -1
- package/src/js/optimization.worker.js +27 -27
- package/src/js/pow.solver.inline.js +154 -56
- package/src/js/pow.solver.js +180 -92
- package/src/js/pow.worker.js +48 -48
- 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 +34 -7
- 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 +56 -10
- package/src/php/Config/SecurityProfiles.php +20 -0
- package/src/php/DirectFingerprint.php +190 -145
- package/src/php/FingerprintBuilder.php +29 -30
- package/src/php/FingerprintClient.php +147 -148
- package/src/php/FingerprintEngine.php +7 -5
- package/src/php/Ja3AnomalyDetector.php +33 -32
- package/src/php/Optimization/FunctionRegistry.php +7 -7
- package/src/php/Optimization/Optimization.php +24 -24
- package/src/php/Optimization/OptimizationOperators.php +26 -25
- 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 -662
- 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 +129 -7
- package/src/php/Utils/TLSClientHelloParser.php +369 -369
- 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,149 +1,148 @@
|
|
|
1
|
-
<?php
|
|
2
|
-
|
|
3
|
-
declare(strict_types=1);
|
|
4
|
-
|
|
5
|
-
namespace Anonympins\Fingerprint;
|
|
6
|
-
use Anonympins\Fingerprint\Config\SecurityProfiles;
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* FingerprintClient -
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* @param
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
'
|
|
44
|
-
'
|
|
45
|
-
'
|
|
46
|
-
'
|
|
47
|
-
|
|
48
|
-
'
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
'
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* @
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
'position:
|
|
83
|
-
'
|
|
84
|
-
|
|
85
|
-
];
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
$
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
$nestingType
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
. '<
|
|
95
|
-
. '
|
|
96
|
-
. '</
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
. '<
|
|
102
|
-
. '
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
$
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
wasmScript.
|
|
124
|
-
wasmScript.
|
|
125
|
-
wasmScript
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
*
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
namespace Anonympins\Fingerprint;
|
|
6
|
+
use Anonympins\Fingerprint\Config\SecurityProfiles;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* FingerprintClient - PHP wrapper for the client-side fingerprinting library.
|
|
10
|
+
*
|
|
11
|
+
* Handles script injection and generation of polymorphic honeypot fields
|
|
12
|
+
* inside HTML forms.
|
|
13
|
+
*/
|
|
14
|
+
class FingerprintClient
|
|
15
|
+
{
|
|
16
|
+
/**
|
|
17
|
+
* @var string Path to client script asset.
|
|
18
|
+
*/
|
|
19
|
+
private string $clientScriptPath;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @var array Configuration payload passed to `initializeClient`.
|
|
23
|
+
*/
|
|
24
|
+
private array $clientConfig;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @var string|null Cryptographic nonce for Content Security Policy (CSP).
|
|
28
|
+
*/
|
|
29
|
+
private ?string $nonce;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Constructor.
|
|
33
|
+
*
|
|
34
|
+
* @param string $clientScriptPath Web-accessible path to `fingerprint.client.js`.
|
|
35
|
+
* @param array $clientConfig Client initialization options.
|
|
36
|
+
*/
|
|
37
|
+
public function __construct(string $clientScriptPath, array $clientConfig = [])
|
|
38
|
+
{
|
|
39
|
+
$this->clientScriptPath = $clientScriptPath;
|
|
40
|
+
|
|
41
|
+
$defaultConfig = [
|
|
42
|
+
'mouse' => true,
|
|
43
|
+
'keystrokes' => true,
|
|
44
|
+
'clicks' => true,
|
|
45
|
+
'honeypots' => [],
|
|
46
|
+
'fetch' => [
|
|
47
|
+
'handleChallenges' => true,
|
|
48
|
+
'probationaryTtl' => 30000, // 30 seconds
|
|
49
|
+
],
|
|
50
|
+
'wasm' => true, // Attempt to load WASM module
|
|
51
|
+
'wasmPath' => '/fp.js' // Path to WASM loader script
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// Deep merge configuration overrides
|
|
55
|
+
$this->clientConfig = SecurityProfiles::deepMerge($defaultConfig, $clientConfig);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
// Generate CSP nonce when possible
|
|
59
|
+
$this->nonce = bin2hex(random_bytes(16));
|
|
60
|
+
} catch (\Exception $e) {
|
|
61
|
+
$this->nonce = null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Generates a hidden honeypot form field.
|
|
67
|
+
* Traps automated bots while remaining invisible to human users.
|
|
68
|
+
*
|
|
69
|
+
* @param string $fieldName Field name (must match client honeypot registration).
|
|
70
|
+
* @return string Generated HTML markup.
|
|
71
|
+
*/
|
|
72
|
+
public function generateHoneypotField(string $fieldName): string
|
|
73
|
+
{
|
|
74
|
+
// Register field in client configuration for monitoring
|
|
75
|
+
if (!in_array($fieldName, $this->clientConfig['honeypots'])) {
|
|
76
|
+
$this->clientConfig['honeypots'][] = $fieldName;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Polymorphic CSS styling to robustly obscure honeypots
|
|
80
|
+
$styleOptions = [
|
|
81
|
+
'position:absolute; left:-9999px; top:-9999px; transform:scale(0); opacity:0; pointer-events:none;',
|
|
82
|
+
'position:fixed; left:-8888px; top:-8888px; width:0; height:0; overflow:hidden; opacity:0; pointer-events:none;',
|
|
83
|
+
'display:none; visibility:hidden; pointer-events:none;'
|
|
84
|
+
];
|
|
85
|
+
$styles = $styleOptions[array_rand($styleOptions)];
|
|
86
|
+
|
|
87
|
+
$containerTags = ['div', 'span', 'p', 'section'];
|
|
88
|
+
$tag = $containerTags[array_rand($containerTags)];
|
|
89
|
+
|
|
90
|
+
$nestingType = rand(0, 1);
|
|
91
|
+
if ($nestingType === 1) {
|
|
92
|
+
return '<' . $tag . ' style="' . $styles . '" aria-hidden="true">'
|
|
93
|
+
. '<label for="' . htmlspecialchars($fieldName) . '">' . htmlspecialchars($fieldName)
|
|
94
|
+
. '<input type="text" id="' . htmlspecialchars($fieldName) . '" name="' . htmlspecialchars($fieldName) . '" tabindex="-1" autocomplete="off">'
|
|
95
|
+
. '</label>'
|
|
96
|
+
. '</' . $tag . '>';
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return '<' . $tag . ' style="' . $styles . '" aria-hidden="true">'
|
|
100
|
+
. '<label for="' . htmlspecialchars($fieldName) . '">' . htmlspecialchars($fieldName) . '</label>'
|
|
101
|
+
. '<input type="text" id="' . htmlspecialchars($fieldName) . '" name="' . htmlspecialchars($fieldName) . '" tabindex="-1" autocomplete="off">'
|
|
102
|
+
. '</' . $tag . '>';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Generates HTML script tags to embed the client library on a page.
|
|
107
|
+
*
|
|
108
|
+
* @return string HTML <script> markup.
|
|
109
|
+
*/
|
|
110
|
+
public function getScriptTag(): string
|
|
111
|
+
{
|
|
112
|
+
$configJson = json_encode($this->clientConfig);
|
|
113
|
+
$nonceAttr = $this->nonce ? ' nonce="' . $this->nonce . '"' : '';
|
|
114
|
+
|
|
115
|
+
// Inline initialization script embedded in HTML
|
|
116
|
+
$initScript = <<<JS
|
|
117
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
118
|
+
const config = {$configJson};
|
|
119
|
+
if (window.ClientLibrary) {
|
|
120
|
+
if (config.wasmPath) {
|
|
121
|
+
const wasmScript = document.createElement('script');
|
|
122
|
+
wasmScript.src = config.wasmPath;
|
|
123
|
+
wasmScript.async = true;
|
|
124
|
+
wasmScript.nonce = '{$this->nonce}';
|
|
125
|
+
document.head.appendChild(wasmScript);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
window.ClientLibrary.initializeClient(config);
|
|
129
|
+
} else {
|
|
130
|
+
console.error('Fingerprint client library not loaded.');
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
JS;
|
|
134
|
+
|
|
135
|
+
// Combine library loader script and inline initialization
|
|
136
|
+
return '<script src="' . htmlspecialchars($this->clientScriptPath) . '"' . $nonceAttr . '></script>'
|
|
137
|
+
. '<script' . $nonceAttr . '>' . $initScript . '</script>';
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Returns the generated CSP nonce.
|
|
142
|
+
* @return string|null
|
|
143
|
+
*/
|
|
144
|
+
public function getNonce(): ?string
|
|
145
|
+
{
|
|
146
|
+
return $this->nonce;
|
|
147
|
+
}
|
|
149
148
|
}
|
|
@@ -229,7 +229,8 @@
|
|
|
229
229
|
'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist', 'dryRun',
|
|
230
230
|
'similarityThreshold', 'summary', 'description',
|
|
231
231
|
'ed25519_private_key', 'ed25519_public_key',
|
|
232
|
-
'wasm', 'enableProofOfSpace', 'pospace', 'federatedPeers', 'federationSecret', 'reset'
|
|
232
|
+
'wasm', 'enableProofOfSpace', 'pospace', 'federatedPeers', 'federationSecret', 'reset',
|
|
233
|
+
'filterWhitelist'
|
|
233
234
|
];
|
|
234
235
|
|
|
235
236
|
if (empty($config['weights'])) {
|
|
@@ -562,7 +563,6 @@
|
|
|
562
563
|
'options' => [
|
|
563
564
|
'httponly' => true,
|
|
564
565
|
'secure' => $secureOption,
|
|
565
|
-
'partitioned' => $secureOption,
|
|
566
566
|
'samesite' => 'Strict',
|
|
567
567
|
'path' => '/',
|
|
568
568
|
]
|
|
@@ -745,7 +745,7 @@
|
|
|
745
745
|
$botnetCluster = RequestUtils::getBotnetClusterScore($context, $stableFpHash);
|
|
746
746
|
|
|
747
747
|
// NOUVEAU: Score de réputation du sous-réseau IP
|
|
748
|
-
$subnetScore = RequestUtils::getSubnetScore($context, $deviceId);
|
|
748
|
+
$subnetScore = RequestUtils::getSubnetScore($context, $deviceId, $this->securityConfig);
|
|
749
749
|
|
|
750
750
|
// Score d'anomalie de pile TCP/IP
|
|
751
751
|
$tcpAnomaly = RequestUtils::getTcpAnomalyScore($context);
|
|
@@ -1092,7 +1092,6 @@
|
|
|
1092
1092
|
'samesite' => 'Strict',
|
|
1093
1093
|
'expires' => time() + ($ticketTtl / 1000),
|
|
1094
1094
|
'path' => '/',
|
|
1095
|
-
'partitioned' => $secureOption,
|
|
1096
1095
|
]
|
|
1097
1096
|
]
|
|
1098
1097
|
];
|
|
@@ -1399,6 +1398,7 @@
|
|
|
1399
1398
|
$tlsSessionId = $context->tlsSessionId ?? '';
|
|
1400
1399
|
$baseBlock = ChallengeUtils::createCpuChallengeBaseBlock($nonce, $clientSecret, $originalFingerprint, $context->clientIp, $tlsSessionId);
|
|
1401
1400
|
|
|
1401
|
+
$isHttps = !empty($context->isHttps);
|
|
1402
1402
|
$challengeContext = [
|
|
1403
1403
|
'clientSecret' => $clientSecret,
|
|
1404
1404
|
'cpuTarget' => $cpuChallengeDetails['target'],
|
|
@@ -1407,6 +1407,7 @@
|
|
|
1407
1407
|
'memDifficulty' => $memDifficulty,
|
|
1408
1408
|
'baseBlock' => $baseBlock,
|
|
1409
1409
|
'originalPath' => $context->path,
|
|
1410
|
+
'isHttp' => !$isHttps,
|
|
1410
1411
|
];
|
|
1411
1412
|
|
|
1412
1413
|
$store->set("secret:{$nonce}", $challengeContext, $this->securityConfig['challengeTtl'] ?? 300);
|
|
@@ -1441,7 +1442,8 @@
|
|
|
1441
1442
|
// Pour les navigateurs, retourner une page HTML
|
|
1442
1443
|
$pageBody = ChallengeUtils::generateCombinedPoWChallengePage(
|
|
1443
1444
|
$cpuChallengeDetails, $memDifficulty, $clientSecret,
|
|
1444
|
-
$this->securityConfig, $trapUrls, $originalFingerprint
|
|
1445
|
+
$this->securityConfig, $trapUrls, $originalFingerprint,
|
|
1446
|
+
$context->clientIp, $tlsSessionId, $baseBlock, $isHttps
|
|
1445
1447
|
);
|
|
1446
1448
|
$decision['body'] = $pageBody;
|
|
1447
1449
|
}
|
|
@@ -5,17 +5,17 @@ declare(strict_types=1);
|
|
|
5
5
|
namespace Anonympins\Fingerprint;
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* TLS (JA3) fingerprint anomaly and spoofing detector in PHP.
|
|
9
9
|
*/
|
|
10
10
|
class Ja3AnomalyDetector
|
|
11
11
|
{
|
|
12
|
-
//
|
|
12
|
+
// Decimal GREASE values (RFC 8701) used by Chromium and Safari TLS stacks
|
|
13
13
|
private const GREASE_VALUES = [
|
|
14
14
|
2570, 6682, 10794, 14906, 19018, 23130, 27242, 31354,
|
|
15
15
|
35466, 39578, 43690, 47802, 51914, 55926, 60038, 64150
|
|
16
16
|
];
|
|
17
17
|
|
|
18
|
-
//
|
|
18
|
+
// Known JA3 MD5 signatures for baseline cross-layer verification
|
|
19
19
|
private const TLS_FINGERPRINT_DB = [
|
|
20
20
|
'e188a442b87f422c5a1e80b05399435b' => ['Chrome'],
|
|
21
21
|
'd8e35855049321c6042a4325c697858f' => ['Chrome'],
|
|
@@ -29,7 +29,7 @@ class Ja3AnomalyDetector
|
|
|
29
29
|
'b633f21d532d35967c8753c38536b4d3' => ['Safari'],
|
|
30
30
|
'4d7a28d5f55b359b69100a311013f03e' => ['Safari', 'Chrome', 'Firefox'],
|
|
31
31
|
'8dd3d7532873575314df23c447543001' => ['Safari', 'Chrome', 'Firefox'],
|
|
32
|
-
//
|
|
32
|
+
// Automated scraper and library fingerprints
|
|
33
33
|
'47344a349b75c4e82333475553b5f358' => ['Python'],
|
|
34
34
|
'b29587b8a143c42546133ad7704b3310' => ['Go'],
|
|
35
35
|
'd435b5223b2884c5a832b842637e245f' => ['Java'],
|
|
@@ -37,8 +37,8 @@ class Ja3AnomalyDetector
|
|
|
37
37
|
];
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
40
|
+
* Parses a raw JA3 string.
|
|
41
|
+
* Expected format: "TLSVersion,Ciphers,Extensions,EllipticCurves,EllipticCurveFormats"
|
|
42
42
|
*/
|
|
43
43
|
public static function parseJa3(string $ja3String): ?array
|
|
44
44
|
{
|
|
@@ -61,7 +61,7 @@ class Ja3AnomalyDetector
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/**
|
|
64
|
-
*
|
|
64
|
+
* Checks if a collection contains any GREASE value.
|
|
65
65
|
*/
|
|
66
66
|
public static function hasGrease(array $values): bool
|
|
67
67
|
{
|
|
@@ -74,7 +74,7 @@ class Ja3AnomalyDetector
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
/**
|
|
77
|
-
*
|
|
77
|
+
* Extracts browser family from User-Agent string.
|
|
78
78
|
*/
|
|
79
79
|
public static function getBrowserFamily(string $userAgent): ?string
|
|
80
80
|
{
|
|
@@ -95,14 +95,14 @@ class Ja3AnomalyDetector
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
/**
|
|
98
|
-
*
|
|
98
|
+
* Evaluates comprehensive JA3 spoofing and anomaly suspicion score.
|
|
99
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
|
|
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
106
|
*/
|
|
107
107
|
public static function getJa3AnomalyScore(
|
|
108
108
|
?string $ja3Hash,
|
|
@@ -115,18 +115,18 @@ class Ja3AnomalyDetector
|
|
|
115
115
|
$claimedBrowser = self::getBrowserFamily($userAgent);
|
|
116
116
|
$isHumanBrowser = in_array($claimedBrowser, ['Chrome', 'Firefox', 'Safari', 'Edge'], true);
|
|
117
117
|
|
|
118
|
-
// ---
|
|
118
|
+
// --- ANALYSIS 1: MD5 JA3 HASH VERIFICATION ---
|
|
119
119
|
if ($ja3Hash && strlen($ja3Hash) === 32) {
|
|
120
120
|
if (isset(self::TLS_FINGERPRINT_DB[$ja3Hash])) {
|
|
121
121
|
$expectedBrowsers = self::TLS_FINGERPRINT_DB[$ja3Hash];
|
|
122
122
|
|
|
123
|
-
//
|
|
123
|
+
// Case A: TLS matches known automated library but UA claims standard browser
|
|
124
124
|
$isLibrary = array_intersect($expectedBrowsers, ['Python', 'Go', 'Java', 'curl']);
|
|
125
125
|
if (!empty($isLibrary) && $isHumanBrowser) {
|
|
126
|
-
$score = max($score, 90); //
|
|
126
|
+
$score = max($score, 90); // High confidence spoofing
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
-
//
|
|
129
|
+
// Case B: Direct discrepancy between claimed browser and expected TLS stack
|
|
130
130
|
if ($claimedBrowser !== null) {
|
|
131
131
|
$matched = false;
|
|
132
132
|
foreach ($expectedBrowsers as $expected) {
|
|
@@ -136,12 +136,12 @@ class Ja3AnomalyDetector
|
|
|
136
136
|
}
|
|
137
137
|
}
|
|
138
138
|
if (!$matched) {
|
|
139
|
-
$score = max($score, 80); //
|
|
139
|
+
$score = max($score, 80); // Browser family mismatch
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
-
//
|
|
144
|
+
// Case C: Stateful multi-UA stagnation detection
|
|
145
145
|
if ($cacheInstance && $claimedBrowser !== null && method_exists($cacheInstance, 'get') && method_exists($cacheInstance, 'set')) {
|
|
146
146
|
$cacheKey = "ja3-browsers:" . $ja3Hash;
|
|
147
147
|
|
|
@@ -154,7 +154,7 @@ class Ja3AnomalyDetector
|
|
|
154
154
|
|
|
155
155
|
if (!in_array($claimedBrowser, $seenBrowsers, true)) {
|
|
156
156
|
$seenBrowsers[] = $claimedBrowser;
|
|
157
|
-
// Cache
|
|
157
|
+
// Cache for 24 hours (86400 seconds)
|
|
158
158
|
if (method_exists($cacheInstance, 'setex')) {
|
|
159
159
|
$cacheInstance->setex($cacheKey, 86400, json_encode($seenBrowsers));
|
|
160
160
|
} else {
|
|
@@ -162,32 +162,32 @@ class Ja3AnomalyDetector
|
|
|
162
162
|
}
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
//
|
|
165
|
+
// Multiple rotating browser UAs emitted from identical TLS stack
|
|
166
166
|
if (count($seenBrowsers) > 1) {
|
|
167
167
|
$score = max($score, 85);
|
|
168
168
|
}
|
|
169
169
|
} catch (\Throwable $e) {
|
|
170
|
-
//
|
|
170
|
+
// Fault-tolerant fallback on cache error
|
|
171
171
|
}
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
-
// ---
|
|
175
|
+
// --- ANALYSIS 2: DEEP INSPECTION ON RAW JA3 STRING ---
|
|
176
176
|
if ($ja3Raw) {
|
|
177
177
|
$parsed = self::parseJa3($ja3Raw);
|
|
178
178
|
if ($parsed) {
|
|
179
|
-
//
|
|
179
|
+
// Check A: GREASE presence for Chrome / Edge
|
|
180
180
|
if ($claimedBrowser === 'Chrome' || $claimedBrowser === 'Edge') {
|
|
181
181
|
$hasCiphersGrease = self::hasGrease($parsed['ciphers']);
|
|
182
182
|
$hasExtensionsGrease = self::hasGrease($parsed['extensions']);
|
|
183
183
|
|
|
184
184
|
if (!$hasCiphersGrease && !$hasExtensionsGrease) {
|
|
185
|
-
//
|
|
185
|
+
// Modern Chromium missing GREASE indicates spoofing
|
|
186
186
|
$score = max($score, 75);
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
//
|
|
190
|
+
// Check B: HTTP/2 or HTTP/3 without ALPN extension (Extension 16)
|
|
191
191
|
$isH2OrHigher = (
|
|
192
192
|
strpos($httpVersion, '2.0') !== false ||
|
|
193
193
|
strpos($httpVersion, 'HTTP/2') !== false ||
|
|
@@ -196,11 +196,11 @@ class Ja3AnomalyDetector
|
|
|
196
196
|
$hasAlpnExtension = in_array(16, $parsed['extensions'], true);
|
|
197
197
|
|
|
198
198
|
if ($isH2OrHigher && !$hasAlpnExtension) {
|
|
199
|
-
//
|
|
199
|
+
// HTTP/2 active at server level but missing from client ALPN extension
|
|
200
200
|
$score = max($score, 70);
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
-
//
|
|
203
|
+
// Check C: Deprecated TLS version negotiated by claimed modern browser
|
|
204
204
|
if ($isHumanBrowser && $parsed['tlsVersion'] < 771) {
|
|
205
205
|
$score = max($score, 80);
|
|
206
206
|
}
|
|
@@ -211,8 +211,9 @@ class Ja3AnomalyDetector
|
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
/**
|
|
214
|
-
*
|
|
215
|
-
*
|
|
214
|
+
* Correlates low-level TCP RTT with application layer latency to detect residential proxy hops.
|
|
215
|
+
* @param RequestContext $context
|
|
216
|
+
* @return int Suspicion score
|
|
216
217
|
*/
|
|
217
218
|
public static function getRttProxyScore(RequestContext $context): int
|
|
218
219
|
{
|
|
@@ -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
|