@anonympins/fingerprint 0.7.4 → 0.7.5
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 +27 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/public/fingerprint-anti-bot.zip +0 -0
- package/src/js/dynamic-wasm.js +218 -218
- package/src/js/fingerprint.client.js +77 -43
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +291 -71
- package/src/js/tests/dynamic-wasm.test.js +77 -77
- package/src/js/tests/fingerprint.client.init.test.js +78 -0
- package/src/js/tests/fingerprint.test.js +18 -0
- package/src/js/tests/threatIntel.test.js +40 -1
- package/src/php/Challenge/ChallengeUtils.php +10 -0
- package/src/php/Config/SecurityProfiles.php +5 -0
- package/src/php/FingerprintEngine.php +186 -37
- package/src/php/Tests/ThreatIntelTest.php +34 -0
- package/src/php/Utils/RequestUtils.php +48 -0
- package/src/php/WordPress/fingerprint-anti-bot.php +1 -1
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-de_DE.po +1 -1
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.mo +0 -0
- package/src/php/WordPress/languages/fingerprint-wordpress-fr_FR.po +1 -1
- package/src/php/WordPress/readme.txt +1 -1
package/src/js/fingerprint.js
CHANGED
|
@@ -68,7 +68,8 @@ function canAttemptDns() {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
* Diffuse un ZKP banni aux pairs fédérés de manière asynchrone (non-bloquante)
|
|
71
|
+
* Diffuse un ZKP banni aux pairs fédérés de manière asynchrone (non-bloquante)
|
|
72
|
+
* avec protection par Anonymat Différentiel (Differential Privacy / Bruit de Laplace & Leurres).
|
|
72
73
|
* @private
|
|
73
74
|
* @param {string} zkpY - La clé publique ZKP du terminal banni.
|
|
74
75
|
* @param {object} config - La configuration de sécurité.
|
|
@@ -77,46 +78,87 @@ async function broadcastBannedZkp(zkpY, config) {
|
|
|
77
78
|
const peers = config.federatedPeers || [];
|
|
78
79
|
if (peers.length === 0) return;
|
|
79
80
|
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
let signature = '';
|
|
84
|
-
let isAsymmetric = false;
|
|
81
|
+
const dpConfig = config.differentialPrivacy || {};
|
|
82
|
+
const dpEnabled = dpConfig.enabled !== false;
|
|
83
|
+
const epsilon = typeof dpConfig.epsilon === 'number' ? dpConfig.epsilon : (config.dpEpsilon || 1.0);
|
|
85
84
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
85
|
+
const sendReport = (targetZkpY, ts) => {
|
|
86
|
+
const msg = `${ts}:${targetZkpY}`;
|
|
87
|
+
let signature = '';
|
|
88
|
+
let isAsymmetric = false;
|
|
89
|
+
|
|
90
|
+
if (process.env.ED25519_PRIVATE_KEY) {
|
|
91
|
+
try {
|
|
92
|
+
const cleanKey = process.env.ED25519_PRIVATE_KEY.replace(/\\n/g, '\n');
|
|
93
|
+
const signBuffer = crypto.sign(null, Buffer.from(msg), {
|
|
94
|
+
key: cleanKey,
|
|
95
|
+
format: 'pem',
|
|
96
|
+
type: 'pkcs8'
|
|
97
|
+
});
|
|
98
|
+
signature = signBuffer.toString('hex');
|
|
99
|
+
isAsymmetric = true;
|
|
100
|
+
} catch (e) {
|
|
101
|
+
console.error('[Fingerprint] Asymmetric broadcast signing failed, falling back to HMAC:', e.message);
|
|
102
|
+
}
|
|
98
103
|
}
|
|
99
|
-
}
|
|
100
104
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
if (!isAsymmetric) {
|
|
106
|
+
const secret = config.federationSecret || getPowSecret();
|
|
107
|
+
signature = crypto.createHmac('sha256', secret).update(msg).digest('hex');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
peers.forEach(peerUrl => {
|
|
111
|
+
fetch(peerUrl + '?coop_op=share_threat_intel', {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
headers: {
|
|
114
|
+
'Content-Type': 'application/json',
|
|
115
|
+
'X-Federation-Signature': isAsymmetric ? '' : signature,
|
|
116
|
+
'X-Federation-Signature-Ed25519': isAsymmetric ? signature : '',
|
|
117
|
+
'X-Federation-Timestamp': String(ts)
|
|
118
|
+
},
|
|
119
|
+
body: JSON.stringify({ zkpY: targetZkpY })
|
|
120
|
+
}).catch(() => {
|
|
121
|
+
// Échec de propagation silencieux pour ne pas perturber le thread principal
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
let reportTimestamp = Date.now();
|
|
127
|
+
|
|
128
|
+
// 1. Differential Privacy : Perturbation laplacienne de l'horodatage
|
|
129
|
+
if (dpEnabled) {
|
|
130
|
+
const deltaT = 5000; // Sensibilité de 5s
|
|
131
|
+
const b = deltaT / Math.max(0.1, epsilon);
|
|
132
|
+
const u = secureRandomFloat() - 0.5;
|
|
133
|
+
const safeU = Math.abs(u) < 1e-7 ? (u >= 0 ? 1e-7 : -1e-7) : u;
|
|
134
|
+
const laplaceNoise = -b * Math.sign(safeU) * Math.log(1 - 2 * Math.abs(safeU));
|
|
135
|
+
const clampedNoise = Math.max(-60000, Math.min(60000, Math.round(laplaceNoise)));
|
|
136
|
+
reportTimestamp = Date.now() + clampedNoise;
|
|
104
137
|
}
|
|
105
138
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
139
|
+
sendReport(zkpY, reportTimestamp);
|
|
140
|
+
|
|
141
|
+
// 2. Differential Privacy : Réponse randomisée par injection de leurres (Decoy ZKP)
|
|
142
|
+
if (dpEnabled) {
|
|
143
|
+
const decoyProbability = dpConfig.dummyRate !== undefined
|
|
144
|
+
? dpConfig.dummyRate
|
|
145
|
+
: (1 / (1 + Math.exp(epsilon)));
|
|
146
|
+
|
|
147
|
+
if (secureRandomFloat() < decoyProbability) {
|
|
148
|
+
const ZKP_P = 115792089237316195423570985008687907853269984665640564039457584007908834671663n;
|
|
149
|
+
const decoyBytes = crypto.randomBytes(32);
|
|
150
|
+
const decoyInt = (BigInt('0x' + decoyBytes.toString('hex')) % (ZKP_P - 2n)) + 1n;
|
|
151
|
+
const decoyZkpY = decoyInt.toString(16);
|
|
152
|
+
|
|
153
|
+
const uDecoy = secureRandomFloat() - 0.5;
|
|
154
|
+
const safeUDecoy = Math.abs(uDecoy) < 1e-7 ? (uDecoy >= 0 ? 1e-7 : -1e-7) : uDecoy;
|
|
155
|
+
const b = 5000 / Math.max(0.1, epsilon);
|
|
156
|
+
const decoyNoise = Math.max(-60000, Math.min(60000, Math.round(-b * Math.sign(safeUDecoy) * Math.log(1 - 2 * Math.abs(safeUDecoy)))));
|
|
157
|
+
const decoyTimestamp = Date.now() + decoyNoise;
|
|
158
|
+
|
|
159
|
+
sendReport(decoyZkpY, decoyTimestamp);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
120
162
|
}
|
|
121
163
|
|
|
122
164
|
const withTimeout = (promise, ms) => {
|
|
@@ -166,6 +208,9 @@ const bingbotEntries = loadBotWhitelist('bingbot.json', [
|
|
|
166
208
|
const yandexEntries = loadBotWhitelist('yandex.json', [
|
|
167
209
|
]);
|
|
168
210
|
|
|
211
|
+
const facebookEntries = loadBotWhitelist('facebook.json', [
|
|
212
|
+
]);
|
|
213
|
+
|
|
169
214
|
function generateSessionMapping() {
|
|
170
215
|
const randomStr = (len = 6) => Array.from({ length: len }, () => String.fromCharCode(crypto.randomInt(97, 123))).join('');
|
|
171
216
|
const randomHeader = () => `X-Sess-${crypto.randomBytes(4).toString('hex')}`;
|
|
@@ -706,6 +751,7 @@ const securityProfiles = {
|
|
|
706
751
|
http2AnomalyScore: 0.8, // NOUVEAU: Poids pour l'anomalie HTTP/2
|
|
707
752
|
threatIntelScore: 1.0, // NOUVEAU: Poids pour le réseau de Threat Intelligence Fédéré
|
|
708
753
|
virtualizationScore: 0.8,
|
|
754
|
+
mtuAnomalyScore: 0.9, // NOUVEAU: Poids pour l'anomalie MTU/fragmentation
|
|
709
755
|
},
|
|
710
756
|
thresholds: { low: 20, medium: 45, high: 75, block: 95 },
|
|
711
757
|
patterns: {
|
|
@@ -754,6 +800,7 @@ const securityProfiles = {
|
|
|
754
800
|
renderingAnomalyScore: 1.0,
|
|
755
801
|
http2AnomalyScore: 1.0,
|
|
756
802
|
threatIntelScore: 1.0,
|
|
803
|
+
mtuAnomalyScore: 0.9,
|
|
757
804
|
virtualizationScore: 1.0,
|
|
758
805
|
},
|
|
759
806
|
thresholds: { low: 10, medium: 35, high: 65, block: 90 },
|
|
@@ -805,6 +852,7 @@ const securityProfiles = {
|
|
|
805
852
|
renderingAnomalyScore: 0.2,
|
|
806
853
|
threatIntelScore: 0.6,
|
|
807
854
|
virtualizationScore: 0.8,
|
|
855
|
+
mtuAnomalyScore: 0.9,
|
|
808
856
|
},
|
|
809
857
|
thresholds: { low: 25, medium: 50, high: 80, block: 95 },
|
|
810
858
|
patterns: {
|
|
@@ -854,6 +902,7 @@ const securityProfiles = {
|
|
|
854
902
|
quicAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie QUIC
|
|
855
903
|
http2AnomalyScore: 0.5,
|
|
856
904
|
renderingAnomalyScore: 0.5, // NOUVEAU: Poids pour l'anomalie de rendu
|
|
905
|
+
mtuAnomalyScore: 0.9,
|
|
857
906
|
virtualizationScore: 0.8,
|
|
858
907
|
},
|
|
859
908
|
thresholds: { low: 25, medium: 55, high: 80, block: 95 },
|
|
@@ -902,6 +951,7 @@ const securityProfiles = {
|
|
|
902
951
|
quicAnomalyScore: 0.9, // NOUVEAU: Poids pour l'anomalie QUIC
|
|
903
952
|
http2AnomalyScore: 0.9,
|
|
904
953
|
renderingAnomalyScore: 0.9,
|
|
954
|
+
mtuAnomalyScore: 0.9,
|
|
905
955
|
virtualizationScore: 0.8,
|
|
906
956
|
},
|
|
907
957
|
thresholds: { low: 15, medium: 40, high: 70, block: 90 },
|
|
@@ -1694,6 +1744,11 @@ export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '',
|
|
|
1694
1744
|
if (!expiry || Date.now() > expiry) {
|
|
1695
1745
|
return false;
|
|
1696
1746
|
}
|
|
1747
|
+
if (statelessData.greenlist || (storedDeviceHash && storedDeviceHash.startsWith('webauthn:greenlist:'))) {
|
|
1748
|
+
if (!deviceId || deviceId === storedDeviceId) {
|
|
1749
|
+
return true;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1697
1752
|
if (storedDeviceHash && storedDeviceHash.startsWith('zkp:')) {
|
|
1698
1753
|
const expectedY = storedDeviceHash.split(':')[1];
|
|
1699
1754
|
if (zkpProof) {
|
|
@@ -1721,6 +1776,11 @@ export const isTicketValid = async (ip, ticket, deviceId = '', deviceHash = '',
|
|
|
1721
1776
|
await store.delete(`ticket:${ticket}`);
|
|
1722
1777
|
return false;
|
|
1723
1778
|
}
|
|
1779
|
+
if (ticketData.greenlist || (storedDeviceHash && storedDeviceHash.startsWith('webauthn:greenlist:'))) {
|
|
1780
|
+
if (!deviceId || deviceId === storedDeviceId) {
|
|
1781
|
+
return true;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1724
1784
|
if (storedDeviceHash && storedDeviceHash.startsWith('zkp:')) {
|
|
1725
1785
|
const expectedY = storedDeviceHash.split(':')[1];
|
|
1726
1786
|
if (zkpProof) {
|
|
@@ -3912,6 +3972,52 @@ function getVirtualizationAnomalyScore(context) {
|
|
|
3912
3972
|
return Math.min(100.0, score);
|
|
3913
3973
|
}
|
|
3914
3974
|
|
|
3975
|
+
/**
|
|
3976
|
+
* @private
|
|
3977
|
+
* Analyzes TCP MTU and fragmentation flags to detect network tunnels (VPN/Proxy).
|
|
3978
|
+
* @param {object} context - The request context.
|
|
3979
|
+
* @returns {{mtuAnomalyScore: number}}
|
|
3980
|
+
*/
|
|
3981
|
+
function getMtuAnomalyScore(context) {
|
|
3982
|
+
const mtuHeader = context.headers?.['x-tcp-mtu-info'];
|
|
3983
|
+
if (!mtuHeader || typeof mtuHeader !== 'string') {
|
|
3984
|
+
return { mtuAnomalyScore: 0.0 };
|
|
3985
|
+
}
|
|
3986
|
+
|
|
3987
|
+
const parts = mtuHeader.split(':');
|
|
3988
|
+
if (parts.length < 2) {
|
|
3989
|
+
return { mtuAnomalyScore: 0.0 };
|
|
3990
|
+
}
|
|
3991
|
+
|
|
3992
|
+
const mtu = parseInt(parts[0], 10);
|
|
3993
|
+
const df = parseInt(parts[1], 10); // Don't Fragment bit (1 or 0)
|
|
3994
|
+
|
|
3995
|
+
if (isNaN(mtu) || isNaN(df)) {
|
|
3996
|
+
return { mtuAnomalyScore: 0.0 };
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3999
|
+
let score = 0.0;
|
|
4000
|
+
|
|
4001
|
+
// Pénalité modérée pour les MTU typiques des VPNs/tunnels.
|
|
4002
|
+
if (mtu > 1200 && mtu <= 1420) {
|
|
4003
|
+
score += 35.0;
|
|
4004
|
+
} else if (mtu > 1420 && mtu < 1492) {
|
|
4005
|
+
score += 20.0;
|
|
4006
|
+
}
|
|
4007
|
+
|
|
4008
|
+
const ua = context.headers?.['user-agent'] || '';
|
|
4009
|
+
const uaParts = parseUserAgent(ua);
|
|
4010
|
+
const os = uaParts.os;
|
|
4011
|
+
|
|
4012
|
+
// Pénalités additionnelles en cas d'incohérence OS vs signature réseau.
|
|
4013
|
+
if (os) {
|
|
4014
|
+
if (os.startsWith('Windows') && mtu < 1492) score += 20.0;
|
|
4015
|
+
if ((os.startsWith('Android') || os.startsWith('iOS')) && mtu < 1480) score += 15.0;
|
|
4016
|
+
if (df === 0 && (os.startsWith('Windows') || os.startsWith('Mac') || os.startsWith('Linux'))) score += 40.0;
|
|
4017
|
+
}
|
|
4018
|
+
|
|
4019
|
+
return { mtuAnomalyScore: Math.min(100.0, score) };
|
|
4020
|
+
}
|
|
3915
4021
|
|
|
3916
4022
|
/**
|
|
3917
4023
|
* Returns a vector of raw (unweighted) suspicion scores.
|
|
@@ -4002,6 +4108,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
4002
4108
|
const { protocolAnomalyScore, http2AnomalyScore, quicAnomalyScore } = getProtocolAnomalyScore(context);
|
|
4003
4109
|
const { renderingAnomalyScore } = getRenderingAnomalyScore(context);
|
|
4004
4110
|
const virtualizationScore = getVirtualizationAnomalyScore(context);
|
|
4111
|
+
const { mtuAnomalyScore } = getMtuAnomalyScore(context);
|
|
4005
4112
|
|
|
4006
4113
|
// Save the updated device state to the store
|
|
4007
4114
|
// Note: deviceData.ips is a Set, which may not serialize correctly in all stores (e.g., JSON). A Redis store should handle this via custom serialization or by converting to an array.
|
|
@@ -4013,7 +4120,7 @@ export const getSuspicionVector = async (context, securityConfig) => {
|
|
|
4013
4120
|
deviceData.ips = new Set(deviceData.ips);
|
|
4014
4121
|
}
|
|
4015
4122
|
// Le vecteur de suspicion est maintenant complet.
|
|
4016
|
-
return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, protocolAnomalyScore, http2AnomalyScore, quicAnomalyScore, renderingAnomalyScore, threatIntelScore, virtualizationScore };
|
|
4123
|
+
return { ...behavioral, headerAnomalyScore, inconsistencyScore, behaviorScore, honeypotScore, botScore, requestPatternScore, crossLayerInconsistencyScore, timeInconsistencyScore, tlsSpoofingScore, clickVarianceScore, clientHintsInconsistencyScore, subnetScore, ipReputationScore, botnetClusterScore, tcpAnomalyScore, protocolAnomalyScore, http2AnomalyScore, quicAnomalyScore, renderingAnomalyScore, threatIntelScore, virtualizationScore, mtuAnomalyScore };
|
|
4017
4124
|
};
|
|
4018
4125
|
|
|
4019
4126
|
// A residential user can change networks (home, 4G, public wifi).
|
|
@@ -4232,6 +4339,24 @@ function generateCpuTargetChallengePage(challengeDetails, clientIp) {
|
|
|
4232
4339
|
</body></html>`;
|
|
4233
4340
|
}
|
|
4234
4341
|
|
|
4342
|
+
/**
|
|
4343
|
+
* Generates closed-shadow DOM honeypot markup for server-rendered challenge pages.
|
|
4344
|
+
* Utilizes Declarative Shadow DOM with mode="closed" to prevent screen reader false positives
|
|
4345
|
+
* while capturing bot web scrapers and crawlers.
|
|
4346
|
+
* @param {string[]} trapUrls
|
|
4347
|
+
* @returns {string}
|
|
4348
|
+
*/
|
|
4349
|
+
function generateClosedShadowTraps(trapUrls = []) {
|
|
4350
|
+
const list = Array.isArray(trapUrls) ? trapUrls : [];
|
|
4351
|
+
if (list.length === 0) return '';
|
|
4352
|
+
|
|
4353
|
+
const trapLinksHtml = list.map((url, idx) => {
|
|
4354
|
+
return `<a href="${url}" rel="nofollow" tabindex="-1"><span>> ${idx + 1}</span></a>`;
|
|
4355
|
+
}).join(' ');
|
|
4356
|
+
|
|
4357
|
+
return `<div style="position:absolute;left:-9999px;top:-9999px;width:0;height:0;overflow:hidden;visibility:hidden;pointer-events:none;" aria-hidden="true"><template shadowrootmode="closed" shadowroot="closed"><style>:host { position: absolute; left: -9999px; top: -9999px; width: 0; height: 0; overflow: hidden; visibility: hidden; pointer-events: none; } a { color: transparent; text-decoration: none; }</style>${trapLinksHtml}</template></div>`;
|
|
4358
|
+
}
|
|
4359
|
+
|
|
4235
4360
|
/**
|
|
4236
4361
|
* Generates the HTML content for a combined CPU + Memory PoW challenge.
|
|
4237
4362
|
* @param {object} cpuChallengeDetails - Details from generateCpuTargetChallenge.
|
|
@@ -4248,6 +4373,7 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
4248
4373
|
const fingerprint = originalFingerprint;
|
|
4249
4374
|
const baseBlock = createCpuChallengeBaseBlock(nonce, clientSecret, fingerprint, clientIp, tlsSessionId);
|
|
4250
4375
|
const baseBlockBytes = `[${baseBlock.toString('utf8').split('').map(c => c.charCodeAt(0)).join(',')}]`;
|
|
4376
|
+
const trapContainerHtml = generateClosedShadowTraps(trapUrls);
|
|
4251
4377
|
|
|
4252
4378
|
// Prépare la configuration pour l'initialisation du client, y compris les URL pièges.
|
|
4253
4379
|
const clientInitConfig = {
|
|
@@ -4315,12 +4441,24 @@ function generateCombinedPoWChallengePage(cpuChallengeDetails, memoryDifficulty,
|
|
|
4315
4441
|
}
|
|
4316
4442
|
|
|
4317
4443
|
if (!htmlTemplate) {
|
|
4318
|
-
htmlTemplate = `<html><head><title>Advanced Security Check</title></head><body style="font-family:sans-serif; text-align:center; padding-top:50px;"><h1>Enhanced Verification... (Level 2)</h1><p>Your activity requires an additional security check. This may take a few moments.</p><div id="loader" style="margin:20px;">⚙️ Initializing combined verification...</div><script><!-- FINGERPRINT_SOLVER_SCRIPT --></script><script><!-- FINGERPRINT_CHALLENGE_SCRIPT --></script
|
|
4444
|
+
htmlTemplate = `<html><head><title>Advanced Security Check</title></head><body style="font-family:sans-serif; text-align:center; padding-top:50px;"><h1>Enhanced Verification... (Level 2)</h1><p>Your activity requires an additional security check. This may take a few moments.</p><div id="loader" style="margin:20px;">⚙️ Initializing combined verification...</div><script><!-- FINGERPRINT_SOLVER_SCRIPT --></script><script><!-- FINGERPRINT_CHALLENGE_SCRIPT --></script><!-- FINGERPRINT_TRAPS --></body></html>`; // eslint-disable-line max-len
|
|
4319
4445
|
}
|
|
4320
4446
|
|
|
4321
|
-
|
|
4447
|
+
let renderedHtml = htmlTemplate
|
|
4322
4448
|
.replace('<!-- FINGERPRINT_SOLVER_SCRIPT -->', solverCode)
|
|
4323
4449
|
.replace('<!-- FINGERPRINT_CHALLENGE_SCRIPT -->', challengeScript);
|
|
4450
|
+
|
|
4451
|
+
if (renderedHtml.includes('<!-- FINGERPRINT_TRAPS -->')) {
|
|
4452
|
+
renderedHtml = renderedHtml.replace('<!-- FINGERPRINT_TRAPS -->', trapContainerHtml);
|
|
4453
|
+
} else if (trapContainerHtml) {
|
|
4454
|
+
if (renderedHtml.includes('</body>')) {
|
|
4455
|
+
renderedHtml = renderedHtml.replace('</body>', `${trapContainerHtml}</body>`);
|
|
4456
|
+
} else {
|
|
4457
|
+
renderedHtml += trapContainerHtml;
|
|
4458
|
+
}
|
|
4459
|
+
}
|
|
4460
|
+
|
|
4461
|
+
return renderedHtml;
|
|
4324
4462
|
}
|
|
4325
4463
|
|
|
4326
4464
|
/**
|
|
@@ -4611,7 +4749,8 @@ export class FingerprintEngine {
|
|
|
4611
4749
|
'similarityThreshold', 'reset',
|
|
4612
4750
|
'ed25519_private_key', 'ed25519_public_key', 'upowModel',
|
|
4613
4751
|
'federatedPeers', 'federationSecret', 'filterWhitelist',
|
|
4614
|
-
'challengeRateLimit'
|
|
4752
|
+
'challengeRateLimit',
|
|
4753
|
+
'differentialPrivacy', 'dpEpsilon'
|
|
4615
4754
|
]);
|
|
4616
4755
|
|
|
4617
4756
|
// 1. Check for essential keys
|
|
@@ -4649,35 +4788,31 @@ export class FingerprintEngine {
|
|
|
4649
4788
|
}
|
|
4650
4789
|
}
|
|
4651
4790
|
calculateFinalScore(suspicionVector) {
|
|
4652
|
-
const
|
|
4653
|
-
if (!
|
|
4654
|
-
|
|
4655
|
-
const
|
|
4656
|
-
|
|
4657
|
-
(
|
|
4658
|
-
|
|
4659
|
-
|
|
4660
|
-
(
|
|
4661
|
-
(
|
|
4662
|
-
(
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
(
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
(suspicionVector.threatIntelScore || 0) * (weights.threatIntelScore || 0) + // NOUVEAU: QUIC Anomaly
|
|
4678
|
-
(suspicionVector.renderingAnomalyScore || 0) * (weights.renderingAnomalyScore || 0); // NOUVEAU: Rendering Anomaly
|
|
4679
|
-
|
|
4680
|
-
return Math.min(100, score);
|
|
4791
|
+
const baseWeights = this.securityConfig.weights;
|
|
4792
|
+
if (!baseWeights) return 0;
|
|
4793
|
+
|
|
4794
|
+
const dynamicWeights = { ...baseWeights };
|
|
4795
|
+
if ((suspicionVector.mtuAnomalyScore || 0) > 50.0) {
|
|
4796
|
+
this._log('Tunnel detected, amplifying suspicion weights.', { mtuScore: suspicionVector.mtuAnomalyScore });
|
|
4797
|
+
|
|
4798
|
+
// Augmente le poids des incohérences de bas niveau (difficiles à falsifier)
|
|
4799
|
+
dynamicWeights.tlsSpoofingScore = (baseWeights.tlsSpoofingScore || 0.8) * 1.25;
|
|
4800
|
+
dynamicWeights.crossLayerInconsistencyScore = (baseWeights.crossLayerInconsistencyScore || 0.4) * 1.4;
|
|
4801
|
+
dynamicWeights.clientHintsInconsistencyScore = (baseWeights.clientHintsInconsistencyScore || 0.7) * 1.2;
|
|
4802
|
+
|
|
4803
|
+
// Augmente le poids des anomalies comportementales (un bot derrière un VPN est plus suspect)
|
|
4804
|
+
dynamicWeights.behaviorScore = (baseWeights.behaviorScore || 0.7) * 1.15;
|
|
4805
|
+
dynamicWeights.requestPatternScore = (baseWeights.requestPatternScore || 0.6) * 1.2;
|
|
4806
|
+
}
|
|
4807
|
+
|
|
4808
|
+
let score = 0;
|
|
4809
|
+
for (const key in dynamicWeights) {
|
|
4810
|
+
const metricScore = suspicionVector[key] || 0.0;
|
|
4811
|
+
if (metricScore > 0) {
|
|
4812
|
+
score += metricScore * dynamicWeights[key];
|
|
4813
|
+
}
|
|
4814
|
+
}
|
|
4815
|
+
return Math.min(100.0, score);
|
|
4681
4816
|
}
|
|
4682
4817
|
|
|
4683
4818
|
|
|
@@ -4998,9 +5133,84 @@ export class FingerprintEngine {
|
|
|
4998
5133
|
|
|
4999
5134
|
// Bypass instantané si l'appareil a prouvé cryptographiquement son identité matérielle (Secure Enclave / TPM)
|
|
5000
5135
|
const { deviceId, deviceData, newCookie } = await resolveRequestIdentity(requestContext, this.securityConfig);
|
|
5136
|
+
|
|
5137
|
+
// Règle Anti-Ferme : Même avec du matériel sécurisé, un appareil condamné ou menant une attaque certaine est révoqué et bloqué
|
|
5138
|
+
if (deviceData?.condemned || this._hasCertainAttack(requestContext)) {
|
|
5139
|
+
if (deviceData) {
|
|
5140
|
+
deviceData.condemned = true;
|
|
5141
|
+
deviceData.webauthnVerified = false;
|
|
5142
|
+
await store.set(`device:${deviceId}`, deviceData);
|
|
5143
|
+
}
|
|
5144
|
+
this._log('Condemned device or attack detected - revoking hardware trust and blocking', { deviceId });
|
|
5145
|
+
const decision = { action: 'block', status: 404, body: 'Forbidden', score: 100, vector: { honeypotScore: 100 } };
|
|
5146
|
+
if (this.dryRun) {
|
|
5147
|
+
this._log(`[Dry Run] Intended action: ${decision.action}`, { score: decision.score });
|
|
5148
|
+
decision.intendedAction = decision.action;
|
|
5149
|
+
decision.action = 'next';
|
|
5150
|
+
delete decision.status;
|
|
5151
|
+
delete decision.body;
|
|
5152
|
+
if (requestContext._newCookies) {
|
|
5153
|
+
const deviceCookie = requestContext._newCookies.find(c => c.name === 'device_id');
|
|
5154
|
+
if (deviceCookie) decision.newCookieForResponse = deviceCookie;
|
|
5155
|
+
}
|
|
5156
|
+
}
|
|
5157
|
+
return decision;
|
|
5158
|
+
}
|
|
5159
|
+
|
|
5160
|
+
// Validation immédiate de l'attestation matérielle WebAuthn si présente dans la requête
|
|
5161
|
+
const behaviorHeader = requestContext.headers?.['x-behavior-metrics'];
|
|
5162
|
+
if (behaviorHeader && typeof behaviorHeader === 'string' && behaviorHeader.startsWith('{')) {
|
|
5163
|
+
try {
|
|
5164
|
+
const parsedMetrics = JSON.parse(behaviorHeader);
|
|
5165
|
+
if (parsedMetrics && parsedMetrics.webauthnAnchor && deviceData) {
|
|
5166
|
+
if (verifyWebAuthnHardwareAnchor(parsedMetrics.webauthnAnchor, deviceData)) {
|
|
5167
|
+
deviceData.webauthnVerified = true;
|
|
5168
|
+
await store.set(`device:${deviceId}`, deviceData);
|
|
5169
|
+
}
|
|
5170
|
+
}
|
|
5171
|
+
} catch (e) {}
|
|
5172
|
+
}
|
|
5173
|
+
|
|
5001
5174
|
if (deviceData && deviceData.webauthnVerified) {
|
|
5002
5175
|
this._log('Hardware-anchored device verified (WebAuthn) - full bypass granted', { deviceId });
|
|
5003
|
-
|
|
5176
|
+
const greenlistTtl = 365 * 86400 * 1000; // 1 an (permanent)
|
|
5177
|
+
const greenlistTicket = generateStatelessTicket({
|
|
5178
|
+
expiry: Date.now() + greenlistTtl,
|
|
5179
|
+
originalIp: clientIp,
|
|
5180
|
+
deviceId,
|
|
5181
|
+
deviceHash: 'webauthn:greenlist:' + (deviceData.webauthnCredentialId || deviceId),
|
|
5182
|
+
greenlist: true
|
|
5183
|
+
});
|
|
5184
|
+
const isHttps = requestContext.headers?.['x-forwarded-proto'] === 'https' ||
|
|
5185
|
+
requestContext.rawReq?.secure ||
|
|
5186
|
+
requestContext.rawReq?.protocol === 'https' ||
|
|
5187
|
+
requestContext.rawReq?.connection?.encrypted;
|
|
5188
|
+
const secureOption = isHttps || this.isProduction;
|
|
5189
|
+
|
|
5190
|
+
const decision = {
|
|
5191
|
+
action: 'next',
|
|
5192
|
+
score: 0,
|
|
5193
|
+
vector: { webauthn_verified: 100 },
|
|
5194
|
+
cookie: {
|
|
5195
|
+
name: 'pow_clearance',
|
|
5196
|
+
value: greenlistTicket,
|
|
5197
|
+
options: {
|
|
5198
|
+
httpOnly: true,
|
|
5199
|
+
secure: secureOption,
|
|
5200
|
+
sameSite: 'strict',
|
|
5201
|
+
...(secureOption && { partitioned: true }),
|
|
5202
|
+
maxAge: greenlistTtl,
|
|
5203
|
+
path: '/'
|
|
5204
|
+
}
|
|
5205
|
+
}
|
|
5206
|
+
};
|
|
5207
|
+
if (requestContext._newCookies) {
|
|
5208
|
+
const deviceCookie = requestContext._newCookies.find(c => c.name === 'device_id');
|
|
5209
|
+
if (deviceCookie) {
|
|
5210
|
+
decision.newCookieForResponse = deviceCookie;
|
|
5211
|
+
}
|
|
5212
|
+
}
|
|
5213
|
+
return decision;
|
|
5004
5214
|
}
|
|
5005
5215
|
|
|
5006
5216
|
if (isStatic) {
|
|
@@ -5043,7 +5253,7 @@ export class FingerprintEngine {
|
|
|
5043
5253
|
if (filterWhitelist === true) {
|
|
5044
5254
|
bypassWhitelist = this._hasCertainAttack(requestContext);
|
|
5045
5255
|
} else if (typeof filterWhitelist === 'number') {
|
|
5046
|
-
|
|
5256
|
+
const res = await getScoreAndVector();
|
|
5047
5257
|
if (res.score > filterWhitelist) {
|
|
5048
5258
|
bypassWhitelist = true;
|
|
5049
5259
|
}
|
|
@@ -6633,6 +6843,7 @@ export const default_whitelist = () => [
|
|
|
6633
6843
|
googlebot_whitelist(),
|
|
6634
6844
|
bingbot_whitelist(),
|
|
6635
6845
|
yandex_whitelist(),
|
|
6846
|
+
facebook_whitelist(),
|
|
6636
6847
|
// === Moteurs de recherche majeurs ===
|
|
6637
6848
|
{ userAgent: 'Googlebot', hostnameSuffix: '.googlebot.com' },
|
|
6638
6849
|
{ userAgent: 'Google-Extended', hostnameSuffix: '.google.com' },
|
|
@@ -6750,6 +6961,11 @@ export const bingbot_whitelist = () => ({
|
|
|
6750
6961
|
entries: bingbotEntries
|
|
6751
6962
|
});
|
|
6752
6963
|
|
|
6964
|
+
export const facebook_whitelist = () => ({
|
|
6965
|
+
type: 'allowlist',
|
|
6966
|
+
entries: facebookEntries
|
|
6967
|
+
});
|
|
6968
|
+
|
|
6753
6969
|
|
|
6754
6970
|
/**
|
|
6755
6971
|
* Extracts the TLS Session ID or ticket hash from the request context.
|
|
@@ -6983,6 +7199,9 @@ export const powMiddleware = (securityConfig) => {
|
|
|
6983
7199
|
|
|
6984
7200
|
case 'next':
|
|
6985
7201
|
default:
|
|
7202
|
+
if (decision.cookie && res && typeof res.cookie === 'function') {
|
|
7203
|
+
res.cookie(decision.cookie.name, decision.cookie.value, decision.cookie.options);
|
|
7204
|
+
}
|
|
6986
7205
|
return next();
|
|
6987
7206
|
}
|
|
6988
7207
|
};
|
|
@@ -6999,6 +7218,7 @@ export const __internal = {
|
|
|
6999
7218
|
checkChallengeRateLimit,
|
|
7000
7219
|
getDeviceHash,
|
|
7001
7220
|
getCompositeDeviceHash,
|
|
7221
|
+
generateClosedShadowTraps,
|
|
7002
7222
|
getSuspicionVector,
|
|
7003
7223
|
getTlsSessionId,
|
|
7004
7224
|
pruneTrafficData,
|