@anonympins/fingerprint 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +1 -1
- package/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 +190 -16
- 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 +124 -10
- 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 +57 -11
- 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 +44 -17
- 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 +38 -0
- 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/BlockList.php +99 -99
- 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 +554 -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 +218 -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
|
}
|
|
@@ -179,6 +179,12 @@
|
|
|
179
179
|
}
|
|
180
180
|
}
|
|
181
181
|
}
|
|
182
|
+
|
|
183
|
+
// Si aucune liste blanche n'est explicitement fournie, appliquer la liste par défaut.
|
|
184
|
+
if (!isset($securityConfig['whitelist'])) {
|
|
185
|
+
$securityConfig['whitelist'] = self::default_whitelist();
|
|
186
|
+
}
|
|
187
|
+
|
|
182
188
|
$this->securityConfig = $securityConfig;
|
|
183
189
|
$this->verbose = $securityConfig['verbose'] ?? false;
|
|
184
190
|
$this->allowlist = $this->buildAllowlist();
|
|
@@ -229,7 +235,8 @@
|
|
|
229
235
|
'autotuning', 'enableUsefulWork', 'usefulWorkConfigPath', 'challengeNewDevices', 'graphql_operation_allowlist', 'dryRun',
|
|
230
236
|
'similarityThreshold', 'summary', 'description',
|
|
231
237
|
'ed25519_private_key', 'ed25519_public_key',
|
|
232
|
-
'wasm', 'enableProofOfSpace', 'pospace', 'federatedPeers', 'federationSecret', 'reset'
|
|
238
|
+
'wasm', 'enableProofOfSpace', 'pospace', 'federatedPeers', 'federationSecret', 'reset',
|
|
239
|
+
'filterWhitelist'
|
|
233
240
|
];
|
|
234
241
|
|
|
235
242
|
if (empty($config['weights'])) {
|
|
@@ -550,10 +557,8 @@
|
|
|
550
557
|
|
|
551
558
|
$deviceId = bin2hex(random_bytes(16)); // UUID-like
|
|
552
559
|
|
|
553
|
-
$isHttps = (
|
|
554
|
-
|
|
555
|
-
($context->getHeader('x-forwarded-proto') === 'https');
|
|
556
|
-
$secureOption = $isHttps || $this->isProduction;
|
|
560
|
+
$isHttps = !empty($context->isHttps);
|
|
561
|
+
$secureOption = $isHttps;
|
|
557
562
|
|
|
558
563
|
// Préparer le cookie à envoyer
|
|
559
564
|
$newCookie = [
|
|
@@ -562,7 +567,6 @@
|
|
|
562
567
|
'options' => [
|
|
563
568
|
'httponly' => true,
|
|
564
569
|
'secure' => $secureOption,
|
|
565
|
-
'partitioned' => $secureOption,
|
|
566
570
|
'samesite' => 'Strict',
|
|
567
571
|
'path' => '/',
|
|
568
572
|
]
|
|
@@ -745,7 +749,7 @@
|
|
|
745
749
|
$botnetCluster = RequestUtils::getBotnetClusterScore($context, $stableFpHash);
|
|
746
750
|
|
|
747
751
|
// NOUVEAU: Score de réputation du sous-réseau IP
|
|
748
|
-
$subnetScore = RequestUtils::getSubnetScore($context, $deviceId);
|
|
752
|
+
$subnetScore = RequestUtils::getSubnetScore($context, $deviceId, $this->securityConfig);
|
|
749
753
|
|
|
750
754
|
// Score d'anomalie de pile TCP/IP
|
|
751
755
|
$tcpAnomaly = RequestUtils::getTcpAnomalyScore($context);
|
|
@@ -985,13 +989,36 @@
|
|
|
985
989
|
// Le fingerprint est cohérent, on peut valider la solution
|
|
986
990
|
if ($powType === 'cpu_target' || $powType === 'cpu_mem') {
|
|
987
991
|
$cpuSolution = $context->query['pow_solution_cpu'] ?? $context->query['pow_solution'] ?? null;
|
|
988
|
-
if ($cpuSolution) {
|
|
989
|
-
$
|
|
992
|
+
if ($cpuSolution !== null && $cpuSolution !== '') {
|
|
993
|
+
$identity = $this->resolveRequestIdentity($context, $suspicionVector);
|
|
994
|
+
$deviceId = (string)($identity['deviceId'] ?? '');
|
|
995
|
+
$currentDeviceHash = (string)($identity['currentDeviceHash'] ?? RequestUtils::getCompositeDeviceHash($context));
|
|
996
|
+
|
|
997
|
+
$ticket = ChallengeUtils::verifyCpuTargetPoWAndGenerateTicket(
|
|
998
|
+
$context->clientIp,
|
|
999
|
+
3600000,
|
|
1000
|
+
$powNonce,
|
|
1001
|
+
(string)$cpuSolution,
|
|
1002
|
+
$challengeContext,
|
|
1003
|
+
$deviceId,
|
|
1004
|
+
$currentDeviceHash
|
|
1005
|
+
);
|
|
990
1006
|
$isValid = $ticket !== null;
|
|
991
1007
|
|
|
992
1008
|
if ($powType === 'cpu_mem') {
|
|
993
|
-
|
|
994
|
-
|
|
1009
|
+
if (!empty($challengeContext['isHttp'])) {
|
|
1010
|
+
$isMemValid = true;
|
|
1011
|
+
} else {
|
|
1012
|
+
$memSolution = $context->query['pow_solution_mem'] ?? null;
|
|
1013
|
+
$isMemValid = ($memSolution !== null && $memSolution !== '')
|
|
1014
|
+
? ChallengeUtils::verifyMemoryPoW(
|
|
1015
|
+
$powNonce,
|
|
1016
|
+
(string)$memSolution,
|
|
1017
|
+
$challengeContext['memDifficulty'] ?? 0,
|
|
1018
|
+
$challengeContext['clientSecret'] ?? ''
|
|
1019
|
+
)
|
|
1020
|
+
: false;
|
|
1021
|
+
}
|
|
995
1022
|
$isValid = $isValid && $isMemValid;
|
|
996
1023
|
}
|
|
997
1024
|
}
|
|
@@ -1073,10 +1100,8 @@
|
|
|
1073
1100
|
MetricsManager::incrementCounter('challenges_solved_total');
|
|
1074
1101
|
$this->log('Challenge solution valid - issuing ticket', ['ticketMaxAge' => $ticketTtl]);
|
|
1075
1102
|
|
|
1076
|
-
$isHttps = (
|
|
1077
|
-
|
|
1078
|
-
($context->getHeader('x-forwarded-proto') === 'https');
|
|
1079
|
-
$secureOption = $isHttps || $this->isProduction;
|
|
1103
|
+
$isHttps = !empty($context->isHttps);
|
|
1104
|
+
$secureOption = $isHttps;
|
|
1080
1105
|
|
|
1081
1106
|
return [
|
|
1082
1107
|
'action' => 'redirect',
|
|
@@ -1092,7 +1117,6 @@
|
|
|
1092
1117
|
'samesite' => 'Strict',
|
|
1093
1118
|
'expires' => time() + ($ticketTtl / 1000),
|
|
1094
1119
|
'path' => '/',
|
|
1095
|
-
'partitioned' => $secureOption,
|
|
1096
1120
|
]
|
|
1097
1121
|
]
|
|
1098
1122
|
];
|
|
@@ -1399,6 +1423,7 @@
|
|
|
1399
1423
|
$tlsSessionId = $context->tlsSessionId ?? '';
|
|
1400
1424
|
$baseBlock = ChallengeUtils::createCpuChallengeBaseBlock($nonce, $clientSecret, $originalFingerprint, $context->clientIp, $tlsSessionId);
|
|
1401
1425
|
|
|
1426
|
+
$isHttps = !empty($context->isHttps);
|
|
1402
1427
|
$challengeContext = [
|
|
1403
1428
|
'clientSecret' => $clientSecret,
|
|
1404
1429
|
'cpuTarget' => $cpuChallengeDetails['target'],
|
|
@@ -1407,6 +1432,7 @@
|
|
|
1407
1432
|
'memDifficulty' => $memDifficulty,
|
|
1408
1433
|
'baseBlock' => $baseBlock,
|
|
1409
1434
|
'originalPath' => $context->path,
|
|
1435
|
+
'isHttp' => !$isHttps,
|
|
1410
1436
|
];
|
|
1411
1437
|
|
|
1412
1438
|
$store->set("secret:{$nonce}", $challengeContext, $this->securityConfig['challengeTtl'] ?? 300);
|
|
@@ -1441,7 +1467,8 @@
|
|
|
1441
1467
|
// Pour les navigateurs, retourner une page HTML
|
|
1442
1468
|
$pageBody = ChallengeUtils::generateCombinedPoWChallengePage(
|
|
1443
1469
|
$cpuChallengeDetails, $memDifficulty, $clientSecret,
|
|
1444
|
-
$this->securityConfig, $trapUrls, $originalFingerprint
|
|
1470
|
+
$this->securityConfig, $trapUrls, $originalFingerprint,
|
|
1471
|
+
$context->clientIp, $tlsSessionId, $baseBlock, $isHttps
|
|
1445
1472
|
);
|
|
1446
1473
|
$decision['body'] = $pageBody;
|
|
1447
1474
|
}
|
|
@@ -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
|
{
|