@anonympins/fingerprint 0.1.3 → 0.1.4
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/README.md +1 -5
- package/fingerprint.client.js +454 -458
- package/fingerprint.js +294 -156
- package/library.js +1613 -1577
- package/package.json +1 -1
- package/pow.solver.js +13 -1
package/fingerprint.js
CHANGED
|
@@ -35,8 +35,8 @@ const getPowSolverCode = () => {
|
|
|
35
35
|
console.warn('Could not load pow.solver.js for inlining, using fallback inline code');
|
|
36
36
|
// Fallback inline code if file cannot be loaded
|
|
37
37
|
return `(function(global){
|
|
38
|
-
async function solveCpuTargetInline(clientIp, nonce, target, clientSecret, progressCallback){
|
|
39
|
-
const cpuTarget = BigInt(target);
|
|
38
|
+
async function solveCpuTargetInline(clientIp, nonce, target, clientSecret, progressCallback){
|
|
39
|
+
const cpuTarget = typeof target === 'bigint' ? target : BigInt('0x' + target);
|
|
40
40
|
let cpuSolution = 0;
|
|
41
41
|
const ipPart = clientIp || '';
|
|
42
42
|
while(true){
|
|
@@ -191,15 +191,37 @@ function getHeaderSignature(context) {
|
|
|
191
191
|
}
|
|
192
192
|
return cyrb53(headerKeys.join(','));
|
|
193
193
|
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Returns the client-side fingerprint if available, otherwise computes a server-side hash.
|
|
197
|
+
* This aligns with the test's expectation for prioritization.
|
|
198
|
+
* @param {object} context The request context.
|
|
199
|
+
* @returns {string} The device fingerprint.
|
|
200
|
+
*/
|
|
194
201
|
export function getDeviceHash(context) {
|
|
195
|
-
// Prioritize the rich client-side fingerprint if provided.
|
|
196
202
|
const clientFp = context.headers['x-device-fingerprint'];
|
|
197
|
-
if (clientFp && typeof clientFp === 'string'
|
|
203
|
+
if (clientFp && typeof clientFp === 'string') {
|
|
198
204
|
return clientFp;
|
|
199
205
|
}
|
|
206
|
+
// Fallback to composite hash if client fingerprint is not available
|
|
207
|
+
return getCompositeDeviceHash(context);
|
|
208
|
+
}
|
|
200
209
|
|
|
210
|
+
export function getCompositeDeviceHash(context) {
|
|
201
211
|
const srv = new FingerprintBuilder();
|
|
202
212
|
|
|
213
|
+
// Si un fingerprint client est fourni, on l'intègre comme un signal fort,
|
|
214
|
+
// mais on ne lui fait pas aveuglément confiance. On continue de construire
|
|
215
|
+
// notre propre fingerprint serveur pour le comparer.
|
|
216
|
+
// Un attaquant qui forge un `clientFp` mais oublie de forger les en-têtes
|
|
217
|
+
// correspondants sera détecté par l'incohérence.
|
|
218
|
+
const clientFp = context.headers['x-device-fingerprint'];
|
|
219
|
+
if (clientFp && typeof clientFp === 'string' && clientFp.includes('cvs:')) {
|
|
220
|
+
// On ajoute le hash du fingerprint client comme un composant du fingerprint serveur.
|
|
221
|
+
// Si le clientFp change, le hash serveur changera aussi.
|
|
222
|
+
srv.add("client_fp_hash", clientFp);
|
|
223
|
+
}
|
|
224
|
+
|
|
203
225
|
// 1. SIGNAL FORT: User Agent (poids élevé)
|
|
204
226
|
const ua = context.headers["user-agent"];
|
|
205
227
|
if (ua) {
|
|
@@ -376,15 +398,15 @@ const generateTspChallenge = (
|
|
|
376
398
|
<div id="loader" style="margin:20px;">⚙️ Calculating route... (${numCities} cities)</div>
|
|
377
399
|
<script>${solverCode}</script>
|
|
378
400
|
<script>
|
|
379
|
-
const cities = ${citiesJson};
|
|
380
|
-
const nonce =
|
|
401
|
+
const cities = ${citiesJson}; // Safe, as it's JSON
|
|
402
|
+
const nonce = ${JSON.stringify(nonce)}; // Safe
|
|
381
403
|
const targetMaxDistance = ${targetMaxDistance};
|
|
382
404
|
|
|
383
405
|
async function solve() {
|
|
384
406
|
const result = await window.solveTspChallenge(cities, targetMaxDistance);
|
|
385
407
|
|
|
386
408
|
if (result.distance <= targetMaxDistance) {
|
|
387
|
-
window.location.href =
|
|
409
|
+
window.location.href = ${JSON.stringify(path)} + "?pow_type=tsp&pow_nonce=" + nonce + "&pow_solution=" + JSON.stringify(result.path);
|
|
388
410
|
} else {
|
|
389
411
|
document.getElementById('loader').innerText = "Error: Could not find a sufficient solution. Please try again.";
|
|
390
412
|
}
|
|
@@ -746,19 +768,111 @@ function getHoneypotScore(context, honeypotConfig = {}) {
|
|
|
746
768
|
* @returns {{behaviorScore: number}}
|
|
747
769
|
*/
|
|
748
770
|
function getBehaviorScore(context) {
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
771
|
+
const behaviorHeader = context.headers["x-behavior-metrics"];
|
|
772
|
+
if (!behaviorHeader) {
|
|
773
|
+
return { behaviorScore: 0 }; // Pas de données, pas de pénalité.
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
try {
|
|
777
|
+
const metrics = JSON.parse(behaviorHeader);
|
|
778
|
+
let score = 0;
|
|
779
|
+
|
|
780
|
+
// 1. Pénalité maximale si un honeypot client a été déclenché.
|
|
781
|
+
if (metrics.honeypotInteraction) {
|
|
782
|
+
return { behaviorScore: 100 };
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// 2. Pénalité pour absence totale d'interaction.
|
|
786
|
+
if (metrics.mouseEntropy === 0 && metrics.keystrokeLatency === 0) {
|
|
787
|
+
score += 40;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
// 3. Vérification de la plausibilité et de la distribution des métriques.
|
|
791
|
+
// Un bot pourrait envoyer des valeurs aléatoires, mais elles ne suivront probablement pas
|
|
792
|
+
// des distributions naturelles (comme la loi de Benford pour les premiers chiffres).
|
|
793
|
+
|
|
794
|
+
// Plausibilité de l'entropie de la souris
|
|
795
|
+
if (metrics.mouseEntropy > 0 && metrics.mouseEntropy < 0.1) score += 20; // Entropie très faible, suspect.
|
|
796
|
+
if (metrics.mouseEntropy > 500) score += 30; // Entropie irréalistement élevée.
|
|
797
|
+
|
|
798
|
+
// Plausibilité de la latence de frappe
|
|
799
|
+
if (metrics.keystrokeLatency > 0 && metrics.keystrokeLatency < 40) score += 25; // Frappe trop rapide pour un humain.
|
|
800
|
+
if (metrics.keystrokeLatency > 1000) score += 15; // Latence très élevée, peut être un script lent.
|
|
801
|
+
|
|
802
|
+
// 4. Analyse de la distribution avec la loi de Benford (si les valeurs sont non nulles).
|
|
803
|
+
// On utilise les décimales pour avoir plus de chiffres à analyser.
|
|
804
|
+
const mouseEntropyStr = String(metrics.mouseEntropy).replace(".", "");
|
|
805
|
+
const keystrokeLatencyStr = String(metrics.keystrokeLatency).replace(".", "");
|
|
806
|
+
|
|
807
|
+
if (mouseEntropyStr.length > 2) {
|
|
808
|
+
const mouseDeviation = Optimization.Operators.benfordTest(mouseEntropyStr);
|
|
809
|
+
// Une déviation > 0.15 est fortement suspecte.
|
|
810
|
+
if (mouseDeviation > 0.15) score += 40;
|
|
752
811
|
}
|
|
753
812
|
|
|
813
|
+
if (keystrokeLatencyStr.length > 2) {
|
|
814
|
+
const keystrokeDeviation = Optimization.Operators.benfordTest(keystrokeLatencyStr);
|
|
815
|
+
if (keystrokeDeviation > 0.15) score += 40;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
return { behaviorScore: Math.min(100, score) };
|
|
819
|
+
} catch (e) {
|
|
820
|
+
return { behaviorScore: 10 }; // En-tête malformé = légèrement suspect.
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/**
|
|
825
|
+
* Calcule un score d'incohérence entre les données du fingerprint client et les en-têtes serveur.
|
|
826
|
+
* @param {object} context - Le contexte de la requête.
|
|
827
|
+
* @returns {{crossLayerInconsistencyScore: number}}
|
|
828
|
+
*/
|
|
829
|
+
function getCrossLayerInconsistency(context) {
|
|
754
830
|
try {
|
|
755
|
-
const
|
|
831
|
+
const clientFpString = context.headers['x-device-fingerprint'];
|
|
832
|
+
if (!clientFpString) return { crossLayerInconsistencyScore: 0 };
|
|
833
|
+
|
|
834
|
+
const clientFpMap = new Map(clientFpString.split("|").map(part => part.split(":")));
|
|
835
|
+
const ua = context.headers["user-agent"] || '';
|
|
756
836
|
let score = 0;
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
837
|
+
|
|
838
|
+
// 1. Incohérence de l'OS
|
|
839
|
+
const clientOsHash = clientFpMap.get('os');
|
|
840
|
+
if (clientOsHash) {
|
|
841
|
+
const serverOsParts = parseUserAgent(ua);
|
|
842
|
+
if (serverOsParts.os && clientOsHash !== cyrb53(serverOsParts.os)) {
|
|
843
|
+
// Exemple: le client prétend être 'Windows' mais le UA est 'macOS'.
|
|
844
|
+
score += 50;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// 2. Incohérence de l'écran (si les Client Hints sont disponibles)
|
|
849
|
+
const clientScreenHash = clientFpMap.get('scr');
|
|
850
|
+
const viewportWidth = context.headers['sec-ch-viewport-width'];
|
|
851
|
+
if (clientScreenHash && viewportWidth) {
|
|
852
|
+
const clientWidth = clientFpMap.get('scr')?.split('x')[0];
|
|
853
|
+
// Ce n'est pas une comparaison directe, mais un bot pourrait oublier de forger les CH.
|
|
854
|
+
// Si le client FP a une largeur et que le CH en a une autre, c'est suspect.
|
|
855
|
+
// Cette vérification est basique et pourrait être affinée.
|
|
856
|
+
if (clientWidth && clientWidth !== viewportWidth) {
|
|
857
|
+
score += 20;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// 3. Incohérence du GPU/Canvas et JA3
|
|
862
|
+
// Un attaquant sophistiqué peut forger le canvas, mais il est très difficile de forger
|
|
863
|
+
// le JA3 qui dépend de la librairie TLS. Une forte incohérence ici est un signal fort.
|
|
864
|
+
const clientGpuHash = clientFpMap.get('gpu');
|
|
865
|
+
const ja3 = getJa3Hash(context);
|
|
866
|
+
if (clientGpuHash && ja3) {
|
|
867
|
+
// Une vraie implémentation nécessiterait une base de données mappant les GPU connus
|
|
868
|
+
// à des signatures JA3 typiques. Pour l'exemple, on simule une pénalité si les deux
|
|
869
|
+
// sont présents mais que le score de cohérence global est déjà faible.
|
|
870
|
+
// (Cette logique est déjà en partie couverte par le `consistencyScore`).
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
return { crossLayerInconsistencyScore: Math.min(100, score) };
|
|
760
874
|
} catch (e) {
|
|
761
|
-
return {
|
|
875
|
+
return { crossLayerInconsistencyScore: 10 }; // Erreur de parsing = suspect.
|
|
762
876
|
}
|
|
763
877
|
}
|
|
764
878
|
|
|
@@ -779,7 +893,9 @@ function getRequestPatternScore(context, deviceData, patternConfig = {}) {
|
|
|
779
893
|
scrapeThreshold = 1000, scrapeWeight = 20, scrapeBurstWeight = 40,
|
|
780
894
|
historySize = 10,
|
|
781
895
|
decayFactor = 0.9,
|
|
782
|
-
inactivityReset = 30000,
|
|
896
|
+
inactivityReset = 30000, // 30 secondes
|
|
897
|
+
// Nouveaux paramètres pour l'analyse de distribution
|
|
898
|
+
benfordMinSamples = 15, benfordWeight = 50,
|
|
783
899
|
// Nouveau paramètre pour la détection de séquences
|
|
784
900
|
sequenceLength = 3, sequenceWeight = 60
|
|
785
901
|
} = patternConfig;
|
|
@@ -793,9 +909,10 @@ function getRequestPatternScore(context, deviceData, patternConfig = {}) {
|
|
|
793
909
|
const currentQueryString = params.toString();
|
|
794
910
|
|
|
795
911
|
// Initialize request history if it doesn't exist
|
|
796
|
-
if (!deviceData.requestHistory)
|
|
797
|
-
|
|
798
|
-
|
|
912
|
+
if (!deviceData.requestHistory) deviceData.requestHistory = [];
|
|
913
|
+
// NOUVEAU: S'assurer que timingHistory est toujours initialisé.
|
|
914
|
+
// Cette vérification est séparée car deviceData peut exister avec requestHistory mais sans timingHistory.
|
|
915
|
+
if (!deviceData.timingHistory) deviceData.timingHistory = [];
|
|
799
916
|
|
|
800
917
|
const history = deviceData.requestHistory;
|
|
801
918
|
let score = 0;
|
|
@@ -803,7 +920,10 @@ function getRequestPatternScore(context, deviceData, patternConfig = {}) {
|
|
|
803
920
|
// --- Analyze patterns based on the last few requests ---
|
|
804
921
|
if (history.length > 0) {
|
|
805
922
|
const lastRequest = history[history.length - 1];
|
|
806
|
-
const timeSinceLast = now - lastRequest.timestamp;
|
|
923
|
+
const timeSinceLast = now - lastRequest.timestamp;
|
|
924
|
+
|
|
925
|
+
// Stocker le délai pour l'analyse de distribution
|
|
926
|
+
deviceData.timingHistory.push(timeSinceLast);
|
|
807
927
|
|
|
808
928
|
// 1. Velocity Check: Penalize requests that are too fast to be human.
|
|
809
929
|
if (timeSinceLast < velocityThreshold) { // 150 < 200 -> true
|
|
@@ -836,6 +956,17 @@ function getRequestPatternScore(context, deviceData, patternConfig = {}) {
|
|
|
836
956
|
);
|
|
837
957
|
if (isRepeating) score += sequenceWeight;
|
|
838
958
|
}
|
|
959
|
+
|
|
960
|
+
// 5. (NOUVEAU) Analyse de la distribution des délais avec la loi de Benford
|
|
961
|
+
if (deviceData.timingHistory.length >= benfordMinSamples) {
|
|
962
|
+
// On concatène tous les délais en une seule chaîne de chiffres.
|
|
963
|
+
const timingString = deviceData.timingHistory.join('');
|
|
964
|
+
const benfordDeviation = Optimization.Operators.benfordTest(timingString);
|
|
965
|
+
|
|
966
|
+
// Une déviation > 0.15 est suspecte. On peut pondérer la pénalité.
|
|
967
|
+
// Une déviation de 0.3 (très suspecte) donnerait un score de 100 (0.3 / 0.3 * 100).
|
|
968
|
+
score += Math.min(100, (benfordDeviation / 0.3) * benfordWeight);
|
|
969
|
+
}
|
|
839
970
|
}
|
|
840
971
|
|
|
841
972
|
// --- Update history ---
|
|
@@ -849,17 +980,20 @@ function getRequestPatternScore(context, deviceData, patternConfig = {}) {
|
|
|
849
980
|
if (history.length > historySize) {
|
|
850
981
|
history.shift();
|
|
851
982
|
}
|
|
983
|
+
if (deviceData.timingHistory.length > benfordMinSamples * 2) { // Garder un historique plus long pour Benford
|
|
984
|
+
deviceData.timingHistory.shift();
|
|
985
|
+
}
|
|
852
986
|
|
|
853
987
|
// Decay the score over time if behavior becomes normal again.
|
|
854
988
|
// We can store the score in deviceData and decay it.
|
|
855
|
-
deviceData.lastPatternScore = (deviceData.lastPatternScore || 0) * decayFactor + score; // Decay old score and add new
|
|
989
|
+
deviceData.lastPatternScore = Math.min(100, (deviceData.lastPatternScore || 0) * decayFactor + score); // Decay old score and add new, plafonné à 100
|
|
856
990
|
|
|
857
991
|
// If there hasn't been a request in a while, reset the pattern score.
|
|
858
992
|
if (history.length > 1 && (now - history[history.length - 2].timestamp > inactivityReset)) { // X ms inactivity
|
|
859
993
|
deviceData.lastPatternScore = 0;
|
|
860
994
|
}
|
|
861
995
|
|
|
862
|
-
return { requestPatternScore:
|
|
996
|
+
return { requestPatternScore: deviceData.lastPatternScore };
|
|
863
997
|
}
|
|
864
998
|
|
|
865
999
|
const trapUrlTemplates = [
|
|
@@ -957,7 +1091,7 @@ export const configureStore = (externalStore) => {
|
|
|
957
1091
|
*/
|
|
958
1092
|
async function resolveRequestIdentity(context, securityConfig = {}) {
|
|
959
1093
|
const existingDeviceId = context.cookies?.device_id;
|
|
960
|
-
const currentDeviceHash =
|
|
1094
|
+
const currentDeviceHash = getCompositeDeviceHash(context); // Use the composite hash for consistency checks
|
|
961
1095
|
let deviceId = existingDeviceId;
|
|
962
1096
|
let consistencyScore = 1.0; // 1.0 = perfectly consistent
|
|
963
1097
|
let deviceData = null;
|
|
@@ -1022,7 +1156,7 @@ async function getBehavioralIndicators(context, deviceData) {
|
|
|
1022
1156
|
const ipProfile = (await store.get(`ip:${clientIp}`)) || { type: "residential" };
|
|
1023
1157
|
const isSharedIp = ipProfile.type === "shared";
|
|
1024
1158
|
|
|
1025
|
-
const currentFpHash =
|
|
1159
|
+
const currentFpHash = getCompositeDeviceHash(context); // Use the composite hash for behavioral indicators
|
|
1026
1160
|
|
|
1027
1161
|
// --- Behavior analysis (Change frequency) ---
|
|
1028
1162
|
if (deviceData.lastFpHash && currentFpHash !== deviceData.lastFpHash) {
|
|
@@ -1108,6 +1242,9 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
1108
1242
|
// On appelle getHoneypotScore ici pour que son résultat soit inclus dans le vecteur.
|
|
1109
1243
|
const { honeypotScore } = getHoneypotScore(context, honeypotConfig);
|
|
1110
1244
|
|
|
1245
|
+
// NOUVEAU: On calcule le score d'incohérence entre les couches.
|
|
1246
|
+
const { crossLayerInconsistencyScore } = getCrossLayerInconsistency(context);
|
|
1247
|
+
|
|
1111
1248
|
const { requestPatternScore } = getRequestPatternScore(context, deviceData, securityConfig.patterns);
|
|
1112
1249
|
|
|
1113
1250
|
// Save the updated device state to the store
|
|
@@ -1120,7 +1257,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
1120
1257
|
deviceData.ips = new Set(deviceData.ips);
|
|
1121
1258
|
}
|
|
1122
1259
|
// Le vecteur de suspicion est maintenant complet.
|
|
1123
|
-
return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, requestPatternScore };
|
|
1260
|
+
return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, requestPatternScore, crossLayerInconsistencyScore };
|
|
1124
1261
|
};
|
|
1125
1262
|
|
|
1126
1263
|
// A residential user can change networks (home, 4G, public wifi).
|
|
@@ -1235,13 +1372,13 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
|
|
|
1235
1372
|
<script>${solverCode}</script>
|
|
1236
1373
|
<script>
|
|
1237
1374
|
async function solve() {
|
|
1238
|
-
const clientIp =
|
|
1239
|
-
const nonce =
|
|
1375
|
+
const clientIp = ${JSON.stringify(clientIp)};
|
|
1376
|
+
const nonce = ${JSON.stringify(nonce)};
|
|
1240
1377
|
const cpuTarget = BigInt("0x${target}");
|
|
1241
1378
|
const solution = await window.solveCpuChallengeInline(clientIp, nonce, cpuTarget, null, (progress) => {
|
|
1242
1379
|
// Optional progress callback
|
|
1243
1380
|
});
|
|
1244
|
-
window.location.href =
|
|
1381
|
+
window.location.href = ${JSON.stringify(path)} + "?pow_type=cpu_target&pow_nonce=" + nonce + "&pow_solution=" + solution;
|
|
1245
1382
|
}
|
|
1246
1383
|
solve();
|
|
1247
1384
|
</script>
|
|
@@ -1261,10 +1398,10 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
1261
1398
|
|
|
1262
1399
|
const challengeScript = `
|
|
1263
1400
|
async function solve() {
|
|
1264
|
-
const nonce =
|
|
1265
|
-
const path =
|
|
1266
|
-
const clientSecret =
|
|
1267
|
-
const clientIp =
|
|
1401
|
+
const nonce = ${JSON.stringify(nonce)};
|
|
1402
|
+
const path = ${JSON.stringify(path)};
|
|
1403
|
+
const clientSecret = ${JSON.stringify(clientSecret)};
|
|
1404
|
+
const clientIp = ${JSON.stringify(clientIp)};
|
|
1268
1405
|
const cpuTarget = BigInt("0x${target}");
|
|
1269
1406
|
const memDifficulty = ${memoryDifficulty};
|
|
1270
1407
|
|
|
@@ -1286,7 +1423,7 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
1286
1423
|
document.getElementById('loader').innerText = "Error: Insufficient memory. Please refresh.";
|
|
1287
1424
|
return;
|
|
1288
1425
|
}
|
|
1289
|
-
window.location.href = path + "?pow_type=cpu_mem&pow_nonce=" + nonce + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + memSolution;
|
|
1426
|
+
window.location.href = path + "?pow_type=cpu_mem&pow_nonce=" + ${JSON.stringify(nonce)} + "&pow_solution_cpu=" + cpuSolution + "&pow_solution_mem=" + memSolution;
|
|
1290
1427
|
}
|
|
1291
1428
|
solve();
|
|
1292
1429
|
`;
|
|
@@ -1317,11 +1454,11 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
1317
1454
|
*/
|
|
1318
1455
|
export function verifyCpuTargetPoWAndGenerateTicket(
|
|
1319
1456
|
clientIp, // This parameter is crucial and must be the actual client IP
|
|
1320
|
-
|
|
1457
|
+
ticketTtl, // NOUVEAU: Durée de validité du ticket (TTL en ms) configurable
|
|
1321
1458
|
nonce,
|
|
1322
1459
|
solution,
|
|
1323
1460
|
clientSecret, // Le secret est maintenant requis
|
|
1324
|
-
target, // La cible est maintenant passée directement
|
|
1461
|
+
target, // La cible est maintenant passée directement en hexadécimal
|
|
1325
1462
|
) {
|
|
1326
1463
|
const message = clientSecret
|
|
1327
1464
|
? `${nonce}:${solution}:${clientSecret}` // FIX: Ne pas inclure l'IP si un secret client est utilisé
|
|
@@ -1335,7 +1472,7 @@ export function verifyCpuTargetPoWAndGenerateTicket(
|
|
|
1335
1472
|
if (hashAsInt < BigInt("0x" + target)) {
|
|
1336
1473
|
// The comparison is direct with native BigInts
|
|
1337
1474
|
// The proof is valid, generate the ticket
|
|
1338
|
-
const expiry = Date.now() + (
|
|
1475
|
+
const expiry = Date.now() + (ticketTtl || 3600000); // Calcule l'expiration à partir du TTL
|
|
1339
1476
|
const signature = crypto
|
|
1340
1477
|
.createHmac("sha256", getPowSecret())
|
|
1341
1478
|
.update(`${clientIp}:${expiry}`)
|
|
@@ -1357,7 +1494,7 @@ const isStaticResource = (path) => staticExtensions.test(path);
|
|
|
1357
1494
|
* Détermine le TTL optimal pour un ticket en utilisant un algorithme génétique multi-objectifs.
|
|
1358
1495
|
* @param {number} suspicionScore - Le score de suspicion de la requête.
|
|
1359
1496
|
* @returns {number} Le TTL optimal calculé en millisecondes.
|
|
1360
|
-
|
|
1497
|
+
*/
|
|
1361
1498
|
function determineOptimalTicketTtl(suspicionScore) {
|
|
1362
1499
|
// Définir les bornes pour la durée de vie du ticket (5 minutes à 24 heures)
|
|
1363
1500
|
const MIN_TTL = 300000;
|
|
@@ -1414,7 +1551,7 @@ function determineOptimalTicketTtl(suspicionScore) {
|
|
|
1414
1551
|
|
|
1415
1552
|
// runMultiple choisit le meilleur résultat sur la base du score (ici, 0).
|
|
1416
1553
|
// La "meilleure" solution dépendra du cycle qui a trouvé le meilleur compromis.
|
|
1417
|
-
return bestResult.solution;
|
|
1554
|
+
return Math.round(bestResult.solution);
|
|
1418
1555
|
}
|
|
1419
1556
|
|
|
1420
1557
|
/**
|
|
@@ -1434,11 +1571,11 @@ function isMalicious(str) {
|
|
|
1434
1571
|
const xxeRegex = /<!ENTITY\s+.*SYSTEM/i;
|
|
1435
1572
|
const pathTraversalRegex = /(\.\.\/|\.\.\\)/;
|
|
1436
1573
|
// Regex pour les injections de commandes.
|
|
1437
|
-
// Elle détecte
|
|
1438
|
-
// 1. L'utilisation de backticks
|
|
1439
|
-
// 2. Des commandes dangereuses (
|
|
1440
|
-
//
|
|
1441
|
-
const commandInjectionRegex = /`.*`|[\n;&|]\s*(ping|ls|whoami|cat|rm|ncat|nc|bash|sh|powershell|cmd)\b/i;
|
|
1574
|
+
// Elle détecte :
|
|
1575
|
+
// 1. L'utilisation de backticks ``.
|
|
1576
|
+
// 2. Des commandes dangereuses (rm, whoami...) qui sont soit au début de la chaîne,
|
|
1577
|
+
// soit précédées par un séparateur de commande (;, &&, ||, |) suivi d'espaces.
|
|
1578
|
+
const commandInjectionRegex = /`.*`|(^|[\n;&|]\s*)(ping|ls|whoami|cat|rm|ncat|nc|bash|sh|powershell|cmd)\b/i;
|
|
1442
1579
|
|
|
1443
1580
|
return injectionRegex.test(str) || log4ShellRegex.test(str) || sstiRegex.test(str) || xxeRegex.test(str) || pathTraversalRegex.test(str) || commandInjectionRegex.test(str);
|
|
1444
1581
|
}
|
|
@@ -1471,7 +1608,8 @@ export class FingerprintEngine {
|
|
|
1471
1608
|
(suspicionVector.requestPatternScore || 0) * (weights.requestPatternScore || 0) +
|
|
1472
1609
|
(suspicionVector.inconsistencyScore || 0) * (weights.inconsistencyScore || 0) +
|
|
1473
1610
|
(suspicionVector.honeypotScore || 0) * (weights.honeypotScore || 0) +
|
|
1474
|
-
(suspicionVector.behaviorScore || 0) * (weights.behaviorScore || 0)
|
|
1611
|
+
(suspicionVector.behaviorScore || 0) * (weights.behaviorScore || 0) +
|
|
1612
|
+
(suspicionVector.crossLayerInconsistencyScore || 0) * (weights.crossLayerInconsistencyScore || 0);
|
|
1475
1613
|
|
|
1476
1614
|
return Math.min(100, score);
|
|
1477
1615
|
}
|
|
@@ -1609,6 +1747,72 @@ export class FingerprintEngine {
|
|
|
1609
1747
|
return { action: 'next', score: 0, vector: { whitelisted: 100, type: 'bot' } };
|
|
1610
1748
|
}
|
|
1611
1749
|
|
|
1750
|
+
// Resolve identity and check for persisted "condemned" status early.
|
|
1751
|
+
const { deviceId, deviceData, newCookie } = await resolveRequestIdentity(requestContext, this.securityConfig);
|
|
1752
|
+
const isNewDevice = !!newCookie;
|
|
1753
|
+
|
|
1754
|
+
this._log('Identity resolved', { deviceId, isNewDevice, hasDeviceData: !!deviceData });
|
|
1755
|
+
|
|
1756
|
+
if (deviceData?.condemned) {
|
|
1757
|
+
this._log('Device condemned - blocking request', { deviceId });
|
|
1758
|
+
if (onDeviceCompromised) {
|
|
1759
|
+
onDeviceCompromised({ deviceId: cookies?.device_id, clientIp, reason: 'Previously condemned', score: 100, vector: { honeypotScore: 100 } });
|
|
1760
|
+
}
|
|
1761
|
+
return { action: 'block', status: 404, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
// The engine now works with the context directly, no more rawReq dependency here.
|
|
1765
|
+
const suspicionVector = await __internal.getSuspicionVector(requestContext, this.securityConfig);
|
|
1766
|
+
// honeypotScore et behaviorScore sont maintenant inclus directement dans le vecteur de suspicion.
|
|
1767
|
+
|
|
1768
|
+
this._log('Suspicion vector calculated', {
|
|
1769
|
+
vector: suspicionVector,
|
|
1770
|
+
weights: this.securityConfig.weights
|
|
1771
|
+
});
|
|
1772
|
+
|
|
1773
|
+
let finalScore = this.calculateFinalScore(suspicionVector);
|
|
1774
|
+
|
|
1775
|
+
this._log('Final score calculated', { finalScore });
|
|
1776
|
+
|
|
1777
|
+
// Si c'est un nouvel appareil, on lui impose un challenge de base, même si son score est bas.
|
|
1778
|
+
// Cela augmente le coût pour les bots qui tentent de simplement supprimer leurs cookies.
|
|
1779
|
+
// NOUVEAU : Cette logique est maintenant configurable.
|
|
1780
|
+
const challengeNewDevices = this.securityConfig.challengeNewDevices === true;
|
|
1781
|
+
if (isNewDevice && finalScore < thresholds.low) {
|
|
1782
|
+
this._log('New device - enforcing minimum challenge score', {
|
|
1783
|
+
originalScore: finalScore,
|
|
1784
|
+
enforcedScore: thresholds.low
|
|
1785
|
+
});
|
|
1786
|
+
finalScore = thresholds.low;
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
const blockThreshold = thresholds.block ?? 95;
|
|
1790
|
+
const isBlocked = finalScore >= blockThreshold;
|
|
1791
|
+
|
|
1792
|
+
const isSuspiciousHigh = finalScore >= thresholds.high && !isBlocked;
|
|
1793
|
+
const isSuspiciousMedium = finalScore >= thresholds.medium;
|
|
1794
|
+
const isSuspicious = finalScore >= thresholds.low;
|
|
1795
|
+
|
|
1796
|
+
// Calculate an analog "suspicion factor" (0 to 1+) for progressive difficulty
|
|
1797
|
+
const suspicionFactor = isSuspicious
|
|
1798
|
+
? Math.min(
|
|
1799
|
+
1.5, // On autorise un dépassement pour rendre les challenges très difficiles si le score est très élevé
|
|
1800
|
+
(finalScore - thresholds.low) / (thresholds.high - thresholds.low),
|
|
1801
|
+
)
|
|
1802
|
+
: 0;
|
|
1803
|
+
|
|
1804
|
+
this._log('Suspicion levels evaluated', {
|
|
1805
|
+
finalScore,
|
|
1806
|
+
isBlocked,
|
|
1807
|
+
isSuspiciousHigh,
|
|
1808
|
+
isSuspiciousMedium,
|
|
1809
|
+
isSuspicious,
|
|
1810
|
+
suspicionFactor,
|
|
1811
|
+
thresholds: { low: thresholds.low, medium: thresholds.medium, high: thresholds.high, block: blockThreshold }
|
|
1812
|
+
});
|
|
1813
|
+
|
|
1814
|
+
const powCookie = cookies?.pow_clearance;
|
|
1815
|
+
|
|
1612
1816
|
// --- NOUVELLE LOGIQUE DE PRIORITÉ ---
|
|
1613
1817
|
// Si une solution de challenge est soumise, on la traite en priorité absolue,
|
|
1614
1818
|
// avant même de recalculer le score de suspicion.
|
|
@@ -1618,49 +1822,55 @@ export class FingerprintEngine {
|
|
|
1618
1822
|
|
|
1619
1823
|
// On doit calculer le score de suspicion *avant* de valider le ticket,
|
|
1620
1824
|
// car le TTL optimal en dépend.
|
|
1621
|
-
const preliminaryVector =
|
|
1622
|
-
const preliminaryScore =
|
|
1623
|
-
|
|
1624
|
-
this._log('Preliminary suspicion vector calculated', {
|
|
1625
|
-
vector: preliminaryVector,
|
|
1626
|
-
score: preliminaryScore
|
|
1825
|
+
const preliminaryVector = suspicionVector; // Use the already calculated vector
|
|
1826
|
+
const preliminaryScore = finalScore; // Use the already calculated score
|
|
1827
|
+
|
|
1828
|
+
this._log('Preliminary suspicion vector calculated', {
|
|
1829
|
+
vector: preliminaryVector,
|
|
1830
|
+
score: preliminaryScore
|
|
1627
1831
|
});
|
|
1628
|
-
|
|
1832
|
+
|
|
1629
1833
|
let isValid = false;
|
|
1630
1834
|
const challengeContext = await store.get(`secret:${pow_nonce}`);
|
|
1631
1835
|
let ticket = null;
|
|
1632
1836
|
// Déclarer optimalTtl ici avec une valeur par défaut
|
|
1633
1837
|
let optimalTtl = this.securityConfig.ticketMaxAge || 3600000;
|
|
1838
|
+
// NOUVEAU: Logique de ticket probatoire
|
|
1839
|
+
|
|
1840
|
+
let finalTtl; // Déclarer finalTtl ici pour qu'il soit accessible dans la portée
|
|
1841
|
+
const isProbationary = preliminaryScore >= thresholds.low;
|
|
1842
|
+
const probationaryTtl = 30000; // 30 secondes
|
|
1634
1843
|
|
|
1635
1844
|
if (challengeContext) {
|
|
1636
1845
|
optimalTtl = determineOptimalTicketTtl(preliminaryScore);
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1846
|
+
finalTtl = isProbationary ? probationaryTtl : optimalTtl;
|
|
1847
|
+
this._log('Challenge context found, verifying solution', { optimalTtl, finalTtl });
|
|
1848
|
+
|
|
1849
|
+
if (pow_type === "cpu_target" && pow_solution) {
|
|
1640
1850
|
// On passe la durée de vie du ticket configurée
|
|
1641
|
-
ticket
|
|
1851
|
+
console.log("ticket");
|
|
1852
|
+
ticket = verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, pow_solution, challengeContext.clientSecret, challengeContext.cpuTarget);
|
|
1853
|
+
console.log(ticket !== null)
|
|
1642
1854
|
isValid = ticket !== null;
|
|
1643
1855
|
this._log('CPU target challenge verification', { isValid });
|
|
1644
|
-
} else if (pow_type === "cpu_mem") {
|
|
1645
|
-
const cpuTicket = verifyCpuTargetPoWAndGenerateTicket(clientIp,
|
|
1856
|
+
} else if (pow_type === "cpu_mem" && pow_solution_cpu && pow_solution_mem) {
|
|
1857
|
+
const cpuTicket = verifyCpuTargetPoWAndGenerateTicket(clientIp, finalTtl, pow_nonce, pow_solution_cpu, challengeContext.clientSecret, challengeContext.cpuTarget);
|
|
1646
1858
|
const isMemValid = verifyMemoryPoW(pow_nonce, pow_solution_mem, challengeContext.memDifficulty, challengeContext.clientSecret);
|
|
1647
1859
|
isValid = cpuTicket !== null && isMemValid;
|
|
1648
1860
|
if (isValid) ticket = cpuTicket; // Le ticket est le même, on le réutilise
|
|
1649
|
-
this._log('Combined CPU+Memory challenge verification', {
|
|
1650
|
-
cpuValid: cpuTicket !== null,
|
|
1651
|
-
memValid: isMemValid,
|
|
1652
|
-
isValid
|
|
1861
|
+
this._log('Combined CPU+Memory challenge verification', {
|
|
1862
|
+
cpuValid: cpuTicket !== null,
|
|
1863
|
+
memValid: isMemValid,
|
|
1864
|
+
isValid
|
|
1653
1865
|
});
|
|
1654
1866
|
}
|
|
1655
1867
|
} else {
|
|
1656
1868
|
this._log('Challenge context not found or expired', { pow_nonce });
|
|
1657
1869
|
}
|
|
1658
|
-
|
|
1659
|
-
console.log({isValid})
|
|
1660
1870
|
if (isValid) {
|
|
1661
1871
|
// La solution est valide. On supprime le secret et on redirige.
|
|
1662
1872
|
await store.delete(`secret:${pow_nonce}`);
|
|
1663
|
-
this._log('Challenge solution valid - issuing ticket', { ticketMaxAge:
|
|
1873
|
+
this._log('Challenge solution valid - issuing ticket', { ticketMaxAge: finalTtl, isProbationary });
|
|
1664
1874
|
|
|
1665
1875
|
if (logger) {
|
|
1666
1876
|
logger({ type: 'challenge_solved', deviceId: cookies?.device_id, score: preliminaryScore, challengeType: pow_type, timestamp: Date.now() });
|
|
@@ -1669,8 +1879,6 @@ export class FingerprintEngine {
|
|
|
1669
1879
|
// NOUVELLE LOGIQUE DE REDIRECTION (plus robuste)
|
|
1670
1880
|
// 1. On part du chemin original stocké, qui peut contenir des query params.
|
|
1671
1881
|
const originalUrl = new URL(challengeContext?.originalPath || path, `http://${requestContext.headers.host || 'localhost'}`);
|
|
1672
|
-
|
|
1673
|
-
console.log({originalUrl})
|
|
1674
1882
|
// 2. On crée un nouvel objet de paramètres à partir de la requête entrante (qui contient les solutions ET les params originaux).
|
|
1675
1883
|
const finalSearchParams = new URLSearchParams(requestContext.query);
|
|
1676
1884
|
|
|
@@ -1685,8 +1893,6 @@ export class FingerprintEngine {
|
|
|
1685
1893
|
const finalQueryString = finalSearchParams.toString();
|
|
1686
1894
|
const finalRedirectPath = finalQueryString ? `${originalUrl.pathname}?${finalQueryString}` : originalUrl.pathname;
|
|
1687
1895
|
this._log('Redirecting to clean path', { finalRedirectPath });
|
|
1688
|
-
|
|
1689
|
-
console.log({finalRedirectPath})
|
|
1690
1896
|
return {
|
|
1691
1897
|
action: 'redirect',
|
|
1692
1898
|
path: finalRedirectPath,
|
|
@@ -1695,93 +1901,19 @@ export class FingerprintEngine {
|
|
|
1695
1901
|
cookie: {
|
|
1696
1902
|
name: 'pow_clearance',
|
|
1697
1903
|
value: ticket,
|
|
1698
|
-
options: {
|
|
1699
|
-
httpOnly: true,
|
|
1700
|
-
secure: this.isProduction, // Le maxAge est déjà inclus dans le ticket, mais on le met aussi sur le cookie
|
|
1701
|
-
maxAge: optimalTtl,
|
|
1702
|
-
}
|
|
1904
|
+
options: { httpOnly: true, secure: this.isProduction, maxAge: finalTtl }
|
|
1703
1905
|
}
|
|
1704
1906
|
};
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
if (logger && challengeContext) {
|
|
1712
|
-
logger({ type: 'challenge_failed', deviceId: cookies?.device_id, reason: 'Invalid PoW solution', timestamp: Date.now() });
|
|
1713
|
-
} else if (logger && !challengeContext) {
|
|
1714
|
-
logger({ type: 'challenge_failed', deviceId: cookies?.device_id, reason: 'Nonce not found or expired', timestamp: Date.now() });
|
|
1907
|
+
} else {
|
|
1908
|
+
// If the solution is invalid, we should treat it as a high-suspicion event.
|
|
1909
|
+
// This prevents the request from proceeding and forces a new, likely harder, challenge.
|
|
1910
|
+
this._log('Challenge solution invalid', { pow_nonce });
|
|
1911
|
+
suspicionVector.honeypotScore = 100; // Invalid solution is a strong bot signal.
|
|
1912
|
+
finalScore = this.calculateFinalScore(suspicionVector);
|
|
1715
1913
|
}
|
|
1716
1914
|
}
|
|
1717
1915
|
// --- FIN DE LA LOGIQUE DE PRIORITÉ ---
|
|
1718
1916
|
|
|
1719
|
-
// Resolve identity and check for persisted "condemned" status early.
|
|
1720
|
-
const { deviceId, deviceData, newCookie } = await resolveRequestIdentity(requestContext, this.securityConfig);
|
|
1721
|
-
const isNewDevice = !!newCookie;
|
|
1722
|
-
|
|
1723
|
-
this._log('Identity resolved', { deviceId, isNewDevice, hasDeviceData: !!deviceData });
|
|
1724
|
-
|
|
1725
|
-
if (deviceData?.condemned) {
|
|
1726
|
-
this._log('Device condemned - blocking request', { deviceId });
|
|
1727
|
-
if (onDeviceCompromised) {
|
|
1728
|
-
onDeviceCompromised({ deviceId: cookies?.device_id, clientIp, reason: 'Previously condemned', score: 100, vector: { honeypotScore: 100 } });
|
|
1729
|
-
}
|
|
1730
|
-
return { action: 'block', status: 404, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
|
|
1731
|
-
}
|
|
1732
|
-
|
|
1733
|
-
// The engine now works with the context directly, no more rawReq dependency here.
|
|
1734
|
-
const suspicionVector = await __internal.getSuspicionVector(requestContext, this.securityConfig);
|
|
1735
|
-
// honeypotScore et behaviorScore sont maintenant inclus directement dans le vecteur de suspicion.
|
|
1736
|
-
|
|
1737
|
-
this._log('Suspicion vector calculated', {
|
|
1738
|
-
vector: suspicionVector,
|
|
1739
|
-
weights: this.securityConfig.weights
|
|
1740
|
-
});
|
|
1741
|
-
|
|
1742
|
-
let finalScore = this.calculateFinalScore(suspicionVector);
|
|
1743
|
-
|
|
1744
|
-
this._log('Final score calculated', { finalScore });
|
|
1745
|
-
|
|
1746
|
-
// Si c'est un nouvel appareil, on lui impose un challenge de base, même si son score est bas.
|
|
1747
|
-
// Cela augmente le coût pour les bots qui tentent de simplement supprimer leurs cookies.
|
|
1748
|
-
// NOUVEAU : Cette logique est maintenant configurable.
|
|
1749
|
-
const challengeNewDevices = this.securityConfig.challengeNewDevices === true;
|
|
1750
|
-
if (isNewDevice && finalScore < thresholds.low) {
|
|
1751
|
-
this._log('New device - enforcing minimum challenge score', {
|
|
1752
|
-
originalScore: finalScore,
|
|
1753
|
-
enforcedScore: thresholds.low
|
|
1754
|
-
});
|
|
1755
|
-
finalScore = thresholds.low;
|
|
1756
|
-
}
|
|
1757
|
-
|
|
1758
|
-
const blockThreshold = thresholds.block ?? 95;
|
|
1759
|
-
const isBlocked = finalScore >= blockThreshold;
|
|
1760
|
-
|
|
1761
|
-
const isSuspiciousHigh = finalScore >= thresholds.high && !isBlocked;
|
|
1762
|
-
const isSuspiciousMedium = finalScore >= thresholds.medium;
|
|
1763
|
-
const isSuspicious = finalScore >= thresholds.low;
|
|
1764
|
-
|
|
1765
|
-
// Calculate an analog "suspicion factor" (0 to 1+) for progressive difficulty
|
|
1766
|
-
const suspicionFactor = isSuspicious
|
|
1767
|
-
? Math.min(
|
|
1768
|
-
1.5, // On autorise un dépassement pour rendre les challenges très difficiles si le score est très élevé
|
|
1769
|
-
(finalScore - thresholds.low) / (thresholds.high - thresholds.low),
|
|
1770
|
-
)
|
|
1771
|
-
: 0;
|
|
1772
|
-
|
|
1773
|
-
this._log('Suspicion levels evaluated', {
|
|
1774
|
-
finalScore,
|
|
1775
|
-
isBlocked,
|
|
1776
|
-
isSuspiciousHigh,
|
|
1777
|
-
isSuspiciousMedium,
|
|
1778
|
-
isSuspicious,
|
|
1779
|
-
suspicionFactor,
|
|
1780
|
-
thresholds: { low: thresholds.low, medium: thresholds.medium, high: thresholds.high, block: blockThreshold }
|
|
1781
|
-
});
|
|
1782
|
-
|
|
1783
|
-
const powCookie = cookies?.pow_clearance;
|
|
1784
|
-
|
|
1785
1917
|
// If the action is to block, we should still include the score and vector for logging/testing.
|
|
1786
1918
|
if (isBlocked) {
|
|
1787
1919
|
this._log('Request blocked - score exceeded block threshold', { finalScore, blockThreshold });
|
|
@@ -1807,9 +1939,8 @@ export class FingerprintEngine {
|
|
|
1807
1939
|
return { action: 'block', status: 404, score: 100, vector: { honeypotScore: 100 } };
|
|
1808
1940
|
}
|
|
1809
1941
|
|
|
1810
|
-
if (isSuspicious && !isTicketValid(clientIp, powCookie)) {
|
|
1942
|
+
if (isSuspicious && !isTicketValid(clientIp, powCookie)) { // The powCookie check is now after the PoW solution check
|
|
1811
1943
|
this._log('Suspicious request without valid ticket - issuing challenge', { finalScore, hasPowCookie: !!powCookie });
|
|
1812
|
-
|
|
1813
1944
|
// Honeypot: Direct probing of challenge endpoints is highly suspicious.
|
|
1814
1945
|
// A legitimate user only hits these endpoints via the challenge page itself.
|
|
1815
1946
|
// If we see a pow_nonce on a request that IS suspicious but has no valid ticket,
|
|
@@ -1853,17 +1984,18 @@ export class FingerprintEngine {
|
|
|
1853
1984
|
const maxMemDifficulty = 48; // 48Mo pour les plus suspects
|
|
1854
1985
|
const memDifficulty = Math.round(minMemDifficulty + memActivationFactor * (maxMemDifficulty - minMemDifficulty));
|
|
1855
1986
|
|
|
1856
|
-
this._log('Challenge parameters calculated', {
|
|
1857
|
-
suspicionFactor,
|
|
1858
|
-
memActivationFactor,
|
|
1859
|
-
memDifficulty,
|
|
1860
|
-
cpuTarget: cpuChallengeDetails.target
|
|
1987
|
+
this._log('Challenge parameters calculated', {
|
|
1988
|
+
suspicionFactor,
|
|
1989
|
+
memActivationFactor,
|
|
1990
|
+
memDifficulty,
|
|
1991
|
+
cpuTarget: cpuChallengeDetails.target
|
|
1861
1992
|
});
|
|
1862
1993
|
|
|
1863
1994
|
// Store the entire challenge context with a short TTL (e.g., 5 minutes)
|
|
1864
1995
|
await store.set(`secret:${nonce}`, {
|
|
1865
1996
|
clientSecret,
|
|
1866
1997
|
cpuTarget: cpuChallengeDetails.target,
|
|
1998
|
+
suspicionScore: finalScore, // *** FIX: Store the score that triggered the challenge ***
|
|
1867
1999
|
memDifficulty: memDifficulty,
|
|
1868
2000
|
originalPath: path, // *** FIX: Store the original path ***
|
|
1869
2001
|
}, this.securityConfig.challengeTtl || 300); // NOUVEAU: TTL configurable (5min par défaut)
|
|
@@ -2162,7 +2294,9 @@ export const powMiddleware = (securityConfig) => {
|
|
|
2162
2294
|
* This is a common pattern to allow mocking of ES module functions.
|
|
2163
2295
|
*/
|
|
2164
2296
|
export const __internal = {
|
|
2297
|
+
store, // Export the store for testing
|
|
2165
2298
|
getDeviceHash,
|
|
2299
|
+
getCompositeDeviceHash,
|
|
2166
2300
|
getSuspicionVector,
|
|
2167
2301
|
cyrb53, // Export for testing
|
|
2168
2302
|
FingerprintBuilder, // Export for testing
|
|
@@ -2170,6 +2304,10 @@ export const __internal = {
|
|
|
2170
2304
|
determineOptimalTicketTtl,
|
|
2171
2305
|
getRequestPatternScore, // Expose for testing
|
|
2172
2306
|
getBehaviorScore, // Expose for testing
|
|
2307
|
+
getCrossLayerInconsistency, // Expose for testing
|
|
2308
|
+
// Expose page generators for security testing
|
|
2309
|
+
generateCpuTargetChallengePage,
|
|
2310
|
+
generateCombinedPoWChallengePage,
|
|
2173
2311
|
};
|
|
2174
2312
|
|
|
2175
2313
|
// --- THRESHOLD AUTO-TUNING SECTION ---
|