@anonympins/fingerprint 0.3.4 → 0.3.6
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 +225 -192
- package/README.md +1087 -1080
- package/package.json +1 -1
- package/src/js/fingerprint.js +403 -63
- package/src/php/FingerprintEngine.php +133 -6
- package/src/php/Ja3AnomalyDetector.php +228 -0
- package/src/php/Tests/Ja3AnomalyDetectorTest.php +180 -0
- package/src/php/Tests/RequestUtilsTest.php +65 -0
- package/src/php/Utils/RequestUtils.php +187 -6
- package/src/php/bin/auto-tune.php +118 -0
|
@@ -186,7 +186,7 @@ class RequestUtils
|
|
|
186
186
|
public static function getHeaderAnomalies(RequestContext $context): array
|
|
187
187
|
{
|
|
188
188
|
$anomalyScore = 0;
|
|
189
|
-
$ua = $context->getHeader('user-agent');
|
|
189
|
+
$ua = $context->getHeader('user-agent') ?? '';
|
|
190
190
|
if (empty($ua) || strlen($ua) < 10) {
|
|
191
191
|
$anomalyScore += 60;
|
|
192
192
|
}
|
|
@@ -197,6 +197,17 @@ class RequestUtils
|
|
|
197
197
|
$anomalyScore += 15;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
// TE: trailers check for Firefox on Desktop
|
|
201
|
+
$uaParts = self::parseUserAgent($ua);
|
|
202
|
+
$isFirefoxDesktop = isset($uaParts['browser']) && str_starts_with($uaParts['browser'], 'Firefox') && ($uaParts['device'] ?? 'desktop') === 'desktop';
|
|
203
|
+
$te = strtolower($context->getHeader('te') ?? '');
|
|
204
|
+
|
|
205
|
+
if ($isFirefoxDesktop && $te !== 'trailers') {
|
|
206
|
+
$anomalyScore += 30;
|
|
207
|
+
} elseif (!$isFirefoxDesktop && ($uaParts['device'] ?? 'desktop') === 'desktop' && $te === 'trailers') {
|
|
208
|
+
$anomalyScore += 30;
|
|
209
|
+
}
|
|
210
|
+
|
|
200
211
|
return ['headerAnomalyScore' => min(100.0, $anomalyScore)];
|
|
201
212
|
}
|
|
202
213
|
|
|
@@ -520,7 +531,7 @@ class RequestUtils
|
|
|
520
531
|
$deviceData['lastChangeTimestamp'] = $now;
|
|
521
532
|
}
|
|
522
533
|
$deviceData['lastFpHash'] = $currentFpHash;
|
|
523
|
-
|
|
534
|
+
|
|
524
535
|
// Enregistrement de l'IP
|
|
525
536
|
if (!in_array($clientIp, $deviceData['ips'])) {
|
|
526
537
|
$deviceData['ips'][] = $clientIp;
|
|
@@ -620,6 +631,25 @@ class RequestUtils
|
|
|
620
631
|
}
|
|
621
632
|
}
|
|
622
633
|
|
|
634
|
+
// Détection d'énumération de chemins (crawling/scraping de ressources séquentielles)
|
|
635
|
+
$enumerationScore = 0;
|
|
636
|
+
if (count($history) >= 3) {
|
|
637
|
+
$templates = array_map(function($h) {
|
|
638
|
+
return preg_replace('/\d+/', '{num}', $h['path']);
|
|
639
|
+
}, $history);
|
|
640
|
+
|
|
641
|
+
$uniquePaths = array_unique(array_map(function($h) {
|
|
642
|
+
return $h['path'];
|
|
643
|
+
}, $history));
|
|
644
|
+
|
|
645
|
+
$templateCounts = array_count_values($templates);
|
|
646
|
+
$maxTemplateRepetition = !empty($templateCounts) ? max($templateCounts) : 0;
|
|
647
|
+
|
|
648
|
+
if ($maxTemplateRepetition >= 3 && count($uniquePaths) === count($history)) {
|
|
649
|
+
$enumerationScore = $patternWeight * 0.8;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
623
653
|
// Logique de décroissance et de score final
|
|
624
654
|
$newPatternScore = $deviceData['lastPatternScore'] ?? 0;
|
|
625
655
|
|
|
@@ -630,7 +660,7 @@ class RequestUtils
|
|
|
630
660
|
}
|
|
631
661
|
$newPatternScore = max(0, $newPatternScore);
|
|
632
662
|
|
|
633
|
-
$deviceData['lastPatternScore'] = $newPatternScore + $instantScore;
|
|
663
|
+
$deviceData['lastPatternScore'] = $newPatternScore + $instantScore + $enumerationScore;
|
|
634
664
|
|
|
635
665
|
return ['requestPatternScore' => min(100.0, $deviceData['lastPatternScore'])];
|
|
636
666
|
}
|
|
@@ -749,7 +779,7 @@ class RequestUtils
|
|
|
749
779
|
* Parse une chaîne de requête GraphQL pour extraire le type et le nom de l'opération.
|
|
750
780
|
* @param array<string, mixed> $body Le corps de la requête.
|
|
751
781
|
* @return array{type: string, name: string}|null
|
|
752
|
-
*/
|
|
782
|
+
*/
|
|
753
783
|
public static function parseGraphQLQuery(array $body): ?array
|
|
754
784
|
{
|
|
755
785
|
$query = $body['query'] ?? null;
|
|
@@ -841,7 +871,7 @@ class RequestUtils
|
|
|
841
871
|
if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
|
842
872
|
$ipBinary = inet_pton($ip);
|
|
843
873
|
if ($ipBinary === false) return null;
|
|
844
|
-
|
|
874
|
+
|
|
845
875
|
$mask = self::generateMask($ipv4Prefix, 4);
|
|
846
876
|
if ($mask === null) return null;
|
|
847
877
|
|
|
@@ -850,7 +880,7 @@ class RequestUtils
|
|
|
850
880
|
} elseif (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
|
851
881
|
$ipBinary = inet_pton($ip);
|
|
852
882
|
if ($ipBinary === false) return null;
|
|
853
|
-
|
|
883
|
+
|
|
854
884
|
$mask = self::generateMask($ipv6Prefix, 16);
|
|
855
885
|
if ($mask === null) return null;
|
|
856
886
|
|
|
@@ -959,4 +989,155 @@ class RequestUtils
|
|
|
959
989
|
$newScore = min(100.0, max(0.0, $current + $change));
|
|
960
990
|
$store->set($key, ['score' => $newScore, 'lastUpdate' => time()], 86400 * 7); // TTL de 7 jours
|
|
961
991
|
}
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Assainit les données de trafic pour l'auto-tuner afin de prévenir les attaques par empoisonnement.
|
|
996
|
+
* Limite la contribution de chaque deviceId à un pourcentage maximum (ex: 2%) du jeu de données total.
|
|
997
|
+
*
|
|
998
|
+
* @param array<int, array<string, mixed>> $trafficData
|
|
999
|
+
* @return array<int, array<string, mixed>>
|
|
1000
|
+
*/
|
|
1001
|
+
public static function sanitizeTrafficData(array $trafficData): array
|
|
1002
|
+
{
|
|
1003
|
+
if (empty($trafficData)) {
|
|
1004
|
+
return [];
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
$tempSanitized = [];
|
|
1008
|
+
$deviceCounts = [];
|
|
1009
|
+
$maxLogsPerDevice = max(3, (int)floor(count($trafficData) * 0.02));
|
|
1010
|
+
|
|
1011
|
+
foreach ($trafficData as $log) {
|
|
1012
|
+
$deviceId = $log['deviceId'] ?? 'anonymous';
|
|
1013
|
+
if (!isset($deviceCounts[$deviceId])) {
|
|
1014
|
+
$deviceCounts[$deviceId] = 0;
|
|
1015
|
+
}
|
|
1016
|
+
if ($deviceCounts[$deviceId] < $maxLogsPerDevice) {
|
|
1017
|
+
$deviceCounts[$deviceId]++;
|
|
1018
|
+
$tempSanitized[] = $log;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
$passedLogs = [];
|
|
1023
|
+
$suspiciousLogs = [];
|
|
1024
|
+
foreach ($tempSanitized as $log) {
|
|
1025
|
+
if (($log['type'] ?? '') === 'request_passed') {
|
|
1026
|
+
$passedLogs[] = $log;
|
|
1027
|
+
} else {
|
|
1028
|
+
$suspiciousLogs[] = $log;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
$minDataPoints = 200; // Seuil par défaut
|
|
1033
|
+
$maxPassedAllowed = max($minDataPoints, count($suspiciousLogs) * 9);
|
|
1034
|
+
|
|
1035
|
+
if (count($passedLogs) > $maxPassedAllowed) {
|
|
1036
|
+
shuffle($passedLogs);
|
|
1037
|
+
$passedLogs = array_slice($passedLogs, 0, $maxPassedAllowed);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
return array_merge($suspiciousLogs, $passedLogs);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/**
|
|
1044
|
+
* Génère une signature HMAC-SHA256 pour sécuriser les données du challenge stockées.
|
|
1045
|
+
* @param string $secret Le secret global (POW_SECRET).
|
|
1046
|
+
* @param array<string, mixed> $payload Les données du challenge.
|
|
1047
|
+
* @param string $clientIp L'IP du client pour lier la signature.
|
|
1048
|
+
* @return string
|
|
1049
|
+
*/
|
|
1050
|
+
public static function signChallengePayload(string $secret, array $payload, string $clientIp): string
|
|
1051
|
+
{
|
|
1052
|
+
$dataToSign = implode(':', [
|
|
1053
|
+
$payload['clientSecret'] ?? '',
|
|
1054
|
+
$payload['cpuTarget'] ?? '',
|
|
1055
|
+
$payload['fingerprint'] ?? '',
|
|
1056
|
+
$payload['memDifficulty'] ?? '',
|
|
1057
|
+
$payload['originalPath'] ?? '',
|
|
1058
|
+
$clientIp
|
|
1059
|
+
]);
|
|
1060
|
+
|
|
1061
|
+
return hash_hmac('sha256', $dataToSign, $secret);
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* Vérifie la signature HMAC-SHA256 des données de challenge récupérées du store.
|
|
1066
|
+
* @param string $secret Le secret global (POW_SECRET).
|
|
1067
|
+
* @param array<string, mixed> $payload Les données du challenge contenant la signature.
|
|
1068
|
+
* @param string $clientIp L'IP du client.
|
|
1069
|
+
* @return bool True si la signature est valide, false sinon.
|
|
1070
|
+
*/
|
|
1071
|
+
public static function verifyChallengePayload(string $secret, array $payload, string $clientIp): bool
|
|
1072
|
+
{
|
|
1073
|
+
if (empty($payload['signature'])) {
|
|
1074
|
+
return false;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
$storedSignature = $payload['signature'];
|
|
1078
|
+
$payloadWithoutSig = $payload;
|
|
1079
|
+
unset($payloadWithoutSig['signature']);
|
|
1080
|
+
|
|
1081
|
+
$expectedSignature = self::signChallengePayload($secret, $payloadWithoutSig, $clientIp);
|
|
1082
|
+
|
|
1083
|
+
return hash_equals($expectedSignature, $storedSignature);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/**
|
|
1087
|
+
* Vérifie si un ticket de clearance (PoW) est valide, en supportant la tolérance au roaming.
|
|
1088
|
+
*
|
|
1089
|
+
* @param string $ip L'adresse IP de la requête courante.
|
|
1090
|
+
* @param string|null $ticket Le ticket de clearance extrait du cookie.
|
|
1091
|
+
* @param string $deviceId L'identifiant du cookie de l'appareil.
|
|
1092
|
+
* @param string $deviceHash L'empreinte matérielle calculée côté serveur.
|
|
1093
|
+
* @param string $secret La clé secrète (POW_SECRET).
|
|
1094
|
+
* @return bool True si le ticket est valide et correspond aux contraintes de sécurité.
|
|
1095
|
+
*/
|
|
1096
|
+
public static function isTicketValid(string $ip, ?string $ticket, string $deviceId = '', string $deviceHash = '', string $secret = ''): bool
|
|
1097
|
+
{
|
|
1098
|
+
if (empty($ticket)) {
|
|
1099
|
+
return false;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
if (str_contains($ticket, '|')) {
|
|
1103
|
+
$parts = explode('|', $ticket);
|
|
1104
|
+
if (count($parts) < 3) return false;
|
|
1105
|
+
[$expiry, $originalIp, $sig] = $parts;
|
|
1106
|
+
} elseif (str_contains($ticket, ':')) {
|
|
1107
|
+
// Fallback rétrocompatible pour les anciens tickets
|
|
1108
|
+
$parts = explode(':', $ticket);
|
|
1109
|
+
if (count($parts) < 2) return false;
|
|
1110
|
+
[$expiry, $sig] = $parts;
|
|
1111
|
+
$originalIp = $ip;
|
|
1112
|
+
} else {
|
|
1113
|
+
return false;
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
if (empty($expiry) || empty($sig) || (time() * 1000) > (int)$expiry) {
|
|
1117
|
+
return false;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
if (str_contains($ticket, '|')) {
|
|
1121
|
+
$expectedSig = hash_hmac('sha256', "{$expiry}:{$originalIp}:{$deviceId}:{$deviceHash}", $secret);
|
|
1122
|
+
} else {
|
|
1123
|
+
$expectedSig = hash_hmac('sha256', "{$ip}:{$expiry}", $secret);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
if (!hash_equals($expectedSig, $sig)) {
|
|
1127
|
+
return false;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
if (!str_contains($ticket, '|')) {
|
|
1131
|
+
return $ip === $originalIp;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
if ($ip === $originalIp) return true;
|
|
1135
|
+
$currentSubnet = self::getIpSubnet($ip);
|
|
1136
|
+
$originalSubnet = self::getIpSubnet($originalIp);
|
|
1137
|
+
if ($currentSubnet !== null && $originalSubnet !== null && $currentSubnet === $originalSubnet) {
|
|
1138
|
+
return true;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
return !empty($deviceId) && !empty($deviceHash); // Match d'identité matérielle stricte (deviceId + deviceHash validés par HMAC)
|
|
1142
|
+
}
|
|
962
1143
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
declare(strict_types=1);
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CLI Script d'auto-tuning périodique des configurations de sécurité.
|
|
7
|
+
* Ce script lit la configuration courante, récupère les logs, les assainit,
|
|
8
|
+
* exécute l'optimiseur génétique, et écrase le fichier JSON d'origine.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
$autoloaderPaths = [
|
|
12
|
+
__DIR__ . '/../../vendor/autoload.php',
|
|
13
|
+
__DIR__ . '/../vendor/autoload.php',
|
|
14
|
+
__DIR__ . '/../../../autoload.php',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
$autoloaded = false;
|
|
18
|
+
foreach ($autoloaderPaths as $path) {
|
|
19
|
+
if (file_exists($path)) {
|
|
20
|
+
require_once $path;
|
|
21
|
+
$autoloaded = true;
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!$autoloaded) {
|
|
27
|
+
fwrite(STDERR, "Erreur : Impossible de charger l'autoloader. Exécutez 'composer install'.\n");
|
|
28
|
+
exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
use Anonympins\Fingerprint\Utils\RequestUtils;
|
|
32
|
+
use Anonympins\Fingerprint\Store\StoreManager;
|
|
33
|
+
use Anonympins\Fingerprint\Optimization\Optimization;
|
|
34
|
+
|
|
35
|
+
// 1. Récupération des arguments CLI
|
|
36
|
+
$configPath = $argv[1] ?? null;
|
|
37
|
+
if (!$configPath) {
|
|
38
|
+
echo "Usage: php auto-tune.php [chemin_vers_security-config.json]\n";
|
|
39
|
+
exit(1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!file_exists($configPath)) {
|
|
43
|
+
fwrite(STDERR, "Erreur : Fichier de configuration introuvable : {$configPath}\n");
|
|
44
|
+
exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
$config = json_decode(file_get_contents($configPath), true);
|
|
48
|
+
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
49
|
+
fwrite(STDERR, "Erreur : Fichier de configuration JSON invalide.\n");
|
|
50
|
+
exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 2. Connexion au store pour récupérer les logs accumulés
|
|
54
|
+
$store = StoreManager::getStore();
|
|
55
|
+
if (!$store) {
|
|
56
|
+
fwrite(STDERR, "Erreur : Aucun store de persistance actif.\n");
|
|
57
|
+
exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
$rawTrafficLogs = $store->get('traffic_logs') ?? [];
|
|
61
|
+
if (empty($rawTrafficLogs)) {
|
|
62
|
+
echo "[AutoTuning] Aucun log de trafic disponible pour l'optimisation.\n";
|
|
63
|
+
exit(0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 3. Nettoyage des données pour prévenir l'empoisonnement (Sybil attacks)
|
|
67
|
+
$sanitizedLogs = RequestUtils::sanitizeTrafficData($rawTrafficLogs);
|
|
68
|
+
|
|
69
|
+
$minDataPoints = $config['autotuning']['minDataPoints'] ?? 200;
|
|
70
|
+
if (count($sanitizedLogs) < $minDataPoints) {
|
|
71
|
+
echo "[AutoTuning] Reporté : Pas assez de données assainies (" . count($sanitizedLogs) . "/{$minDataPoints}).\n";
|
|
72
|
+
exit(0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
echo "[AutoTuning] Lancement de l'optimisation sur " . count($sanitizedLogs) . " données de trafic...\n";
|
|
76
|
+
|
|
77
|
+
// 4. Résolution du Front de Pareto
|
|
78
|
+
$paretoFront = Optimization::solveFullSecurityTuning(['trafficData' => $sanitizedLogs]);
|
|
79
|
+
if (empty($paretoFront)) {
|
|
80
|
+
fwrite(STDERR, "[AutoTuning] L'optimisation n'a retourné aucun résultat.\n");
|
|
81
|
+
exit(1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 5. Sélection de la solution la plus équilibrée
|
|
85
|
+
$bestSolution = $paretoFront[0];
|
|
86
|
+
$minDistance = sqrt(pow($bestSolution['objectives'][0], 2) + pow($bestSolution['objectives'][1], 2));
|
|
87
|
+
foreach ($paretoFront as $candidate) {
|
|
88
|
+
$distance = sqrt(pow($candidate['objectives'][0], 2) + pow($candidate['objectives'][1], 2));
|
|
89
|
+
if ($distance < $minDistance) {
|
|
90
|
+
$minDistance = $distance;
|
|
91
|
+
$bestSolution = $candidate;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
$newConfig = $bestSolution['solution'];
|
|
96
|
+
$maxChangeVelocity = 0.15; // Inertie de 15% maximum par cycle
|
|
97
|
+
|
|
98
|
+
$applyInertialUpdate = function (array &$current, array $target) use ($maxChangeVelocity) {
|
|
99
|
+
$sumCurrent = array_sum($current);
|
|
100
|
+
if ($sumCurrent === 0) return;
|
|
101
|
+
$sumTarget = 0;
|
|
102
|
+
foreach ($current as $k => $v) {
|
|
103
|
+
if (isset($target[$k])) $sumTarget += $target[$k];
|
|
104
|
+
}
|
|
105
|
+
$ratio = ($sumTarget - $sumCurrent) / $sumCurrent;
|
|
106
|
+
$factor = 1 + max(-$maxChangeVelocity, min($maxChangeVelocity, $ratio));
|
|
107
|
+
foreach ($current as $k => &$v) {
|
|
108
|
+
if (isset($target[$k])) $v *= $factor;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
$applyInertialUpdate($config['thresholds'], $newConfig['thresholds']);
|
|
113
|
+
$applyInertialUpdate($config['weights'], $newConfig['weights']);
|
|
114
|
+
$applyInertialUpdate($config['patterns'], $newConfig['patterns']);
|
|
115
|
+
|
|
116
|
+
// 6. Écrasement propre du fichier de configuration original
|
|
117
|
+
file_put_contents($configPath, json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES));
|
|
118
|
+
echo "[AutoTuning] Succès : Fichier {$configPath} mis à jour avec les paramètres optimisés.\n";
|