@anonympins/fingerprint 0.6.3 → 0.6.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/CHANGELOG.md +11 -0
- package/package.json +1 -1
- package/src/js/fingerprint.client.obfuscated.js +1 -1
- package/src/js/fingerprint.js +6944 -6802
- package/src/js/library.js +113 -43
- package/src/js/mongodb-store.js +13 -0
- package/src/js/redis-store.js +13 -0
- package/src/js/sql-store.js +80 -77
- package/src/js/tests/fingerprint.engine.test.js +2 -3
- package/src/js/tests/fingerprint.test.js +2 -2
- package/src/php/Challenge/ChallengeUtils.php +2 -2
- package/src/php/Config/SecurityProfiles.php +5 -0
- package/src/php/FingerprintEngine.php +100 -23
- package/src/php/Optimization/OptimizationOperators.php +39 -15
- package/src/php/RequestContext.php +4 -0
- package/src/php/Store/IStore.php +6 -0
- package/src/php/Store/MongoDbStore.php +12 -0
- package/src/php/Store/RedisStore.php +12 -0
- package/src/php/Tests/FingerprintEngineTest.php +46 -0
- package/src/php/Tests/IpReputationTest.php +1 -0
- package/src/php/Utils/RequestUtils.php +42 -9
package/src/js/library.js
CHANGED
|
@@ -1551,23 +1551,72 @@ Optimization.Operators.solveFraudDetection = (context, options = {}) => {
|
|
|
1551
1551
|
* @param {Array<object>} config.trafficData - Données de trafic collectées.
|
|
1552
1552
|
* @returns {function(object): number[]} Une fonction de fitness qui prend une configuration complète et retourne les scores [taux de faux positifs, taux de faux négatifs].
|
|
1553
1553
|
*/
|
|
1554
|
-
Optimization.Operators.createFullSecurityConfigEvaluator = (
|
|
1555
|
-
|
|
1556
|
-
|
|
1554
|
+
Optimization.Operators.createFullSecurityConfigEvaluator = (context) => {
|
|
1555
|
+
const trafficData = context.trafficData || [];
|
|
1556
|
+
const currentConfig = context.currentConfig || null;
|
|
1557
|
+
|
|
1558
|
+
// Ancres immuables (Baseline Anchors) pour forcer le calibrage d'échelle de suspicion
|
|
1559
|
+
const STATIC_ANCHORS = [
|
|
1560
|
+
{
|
|
1561
|
+
type: 'request_passed',
|
|
1562
|
+
weight: 15.0,
|
|
1563
|
+
vector: {
|
|
1564
|
+
historyScore: 0, rotationScore: 0, headerAnomalyScore: 0, requestPatternScore: 0,
|
|
1565
|
+
inconsistencyScore: 0, behaviorScore: 0, honeypotScore: 0, botScore: 0,
|
|
1566
|
+
crossLayerInconsistencyScore: 0, timeInconsistencyScore: 0, tlsSpoofingScore: 0,
|
|
1567
|
+
clickVarianceScore: 0, clientHintsInconsistencyScore: 0, subnetScore: 0,
|
|
1568
|
+
ipReputationScore: 0, botnetClusterScore: 0, tcpAnomalyScore: 0,
|
|
1569
|
+
quicAnomalyScore: 0, renderingAnomalyScore: 0, threatIntelScore: 0
|
|
1570
|
+
}
|
|
1571
|
+
},
|
|
1572
|
+
{
|
|
1573
|
+
type: 'request_blocked',
|
|
1574
|
+
weight: 15.0,
|
|
1575
|
+
vector: {
|
|
1576
|
+
historyScore: 100, rotationScore: 100, headerAnomalyScore: 80, requestPatternScore: 100,
|
|
1577
|
+
inconsistencyScore: 100, behaviorScore: 0, honeypotScore: 100, botScore: 100,
|
|
1578
|
+
crossLayerInconsistencyScore: 80, timeInconsistencyScore: 100, tlsSpoofingScore: 100,
|
|
1579
|
+
clickVarianceScore: 100, clientHintsInconsistencyScore: 90, subnetScore: 100,
|
|
1580
|
+
ipReputationScore: 100, botnetClusterScore: 100, tcpAnomalyScore: 100,
|
|
1581
|
+
quicAnomalyScore: 100, renderingAnomalyScore: 100, threatIntelScore: 100
|
|
1582
|
+
}
|
|
1583
|
+
},
|
|
1584
|
+
{
|
|
1585
|
+
type: 'request_passed', // Profil humain sain avec du bruit (doit rester sous le seuil d'alerte, ex: < 20)
|
|
1586
|
+
weight: 10.0,
|
|
1587
|
+
vector: {
|
|
1588
|
+
historyScore: 15, rotationScore: 10, headerAnomalyScore: 20, requestPatternScore: 15,
|
|
1589
|
+
inconsistencyScore: 10, behaviorScore: 15, honeypotScore: 0, botScore: 0,
|
|
1590
|
+
crossLayerInconsistencyScore: 10, timeInconsistencyScore: 10, tlsSpoofingScore: 10,
|
|
1591
|
+
clickVarianceScore: 0, clientHintsInconsistencyScore: 10, subnetScore: 5,
|
|
1592
|
+
ipReputationScore: 5, botnetClusterScore: 5, tcpAnomalyScore: 10,
|
|
1593
|
+
quicAnomalyScore: 10, renderingAnomalyScore: 5, threatIntelScore: 0
|
|
1594
|
+
}
|
|
1595
|
+
},
|
|
1596
|
+
{
|
|
1597
|
+
type: 'challenge_issued', // Profil robot furtif moyen (doit être challengé, ex: > 35)
|
|
1598
|
+
weight: 10.0,
|
|
1599
|
+
vector: {
|
|
1600
|
+
historyScore: 20, rotationScore: 20, headerAnomalyScore: 20, requestPatternScore: 40,
|
|
1601
|
+
inconsistencyScore: 30, behaviorScore: 30, honeypotScore: 0, botScore: 0,
|
|
1602
|
+
crossLayerInconsistencyScore: 20, timeInconsistencyScore: 20, tlsSpoofingScore: 30,
|
|
1603
|
+
clickVarianceScore: 20, clientHintsInconsistencyScore: 20, subnetScore: 15,
|
|
1604
|
+
ipReputationScore: 15, botnetClusterScore: 20, tcpAnomalyScore: 30,
|
|
1605
|
+
quicAnomalyScore: 20, renderingAnomalyScore: 20, threatIntelScore: 0
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
];
|
|
1557
1609
|
|
|
1558
1610
|
return function fullConfigFitness(config) {
|
|
1559
|
-
let falsePositives = 0;
|
|
1560
|
-
let falseNegatives = 0;
|
|
1611
|
+
let falsePositives = 0;
|
|
1612
|
+
let falseNegatives = 0;
|
|
1561
1613
|
let totalHumans = 0;
|
|
1562
1614
|
let totalBots = 0;
|
|
1563
|
-
let totalChallenges = 0;
|
|
1564
|
-
let totalChallengeCost = 0; // NOUVEAU: Coût cumulé des challenges
|
|
1565
1615
|
|
|
1566
|
-
|
|
1616
|
+
let maxHumanScore = 0;
|
|
1617
|
+
let minBotScore = 100;
|
|
1618
|
+
|
|
1567
1619
|
const calculateScore = (log) => {
|
|
1568
|
-
// Cette fonction interne devrait répliquer la logique de `getSuspicionVector`
|
|
1569
|
-
// en utilisant les poids et les paramètres de `config`.
|
|
1570
|
-
// Pour cet exemple, nous utilisons une version simplifiée.
|
|
1571
1620
|
let score = 0;
|
|
1572
1621
|
for (const key in config.weights) {
|
|
1573
1622
|
score += (log.vector?.[key] || 0) * config.weights[key];
|
|
@@ -1575,57 +1624,78 @@ Optimization.Operators.createFullSecurityConfigEvaluator = ({ trafficData }) =>
|
|
|
1575
1624
|
return score;
|
|
1576
1625
|
};
|
|
1577
1626
|
|
|
1578
|
-
// Pondération de la confiance : un challenge résolu est un signal humain plus fort.
|
|
1579
1627
|
const confidenceWeights = {
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1628
|
+
request_passed: 0.7,
|
|
1629
|
+
challenge_issued: 1.0,
|
|
1630
|
+
request_blocked: 1.0,
|
|
1631
|
+
challenge_solved: 1.5,
|
|
1632
|
+
trap_triggered: 2.0,
|
|
1585
1633
|
};
|
|
1586
1634
|
|
|
1635
|
+
// 1. Évaluation sur les données de trafic réelles
|
|
1587
1636
|
for (const log of trafficData) {
|
|
1588
|
-
const
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
const isLikelyBot = log.type === 'challenge_issued' || log.type === 'request_blocked';
|
|
1637
|
+
const weight = log.weight || 1.0;
|
|
1638
|
+
const confidence = (confidenceWeights[log.type] || 1.0) * weight;
|
|
1639
|
+
const isLikelyBot = log.type === 'challenge_issued' || log.type === 'request_blocked' || log.type === 'trap_triggered';
|
|
1592
1640
|
const isLikelyHuman = log.type === 'request_passed' || log.type === 'challenge_solved';
|
|
1593
1641
|
|
|
1642
|
+
const score = calculateScore(log);
|
|
1643
|
+
|
|
1594
1644
|
if (isLikelyBot) {
|
|
1595
|
-
totalBots
|
|
1596
|
-
|
|
1597
|
-
// Faux négatif : un bot qui aurait dû être challengé mais ne l'a pas été
|
|
1645
|
+
totalBots += confidence;
|
|
1646
|
+
minBotScore = Math.min(minBotScore, score);
|
|
1598
1647
|
if (score < config.thresholds.low) {
|
|
1599
1648
|
falseNegatives += confidence;
|
|
1600
|
-
} else {
|
|
1601
|
-
totalChallenges += confidence; // Un bot correctement challengé
|
|
1602
|
-
// Calculer le coût de ce challenge (proportionnel à la difficulté)
|
|
1603
|
-
const suspicionFactor = (score - config.thresholds.low) / (config.thresholds.high - config.thresholds.low);
|
|
1604
|
-
totalChallengeCost += Math.min(1, Math.max(0, suspicionFactor)) * confidence;
|
|
1605
1649
|
}
|
|
1606
1650
|
} else if (isLikelyHuman) {
|
|
1607
|
-
totalHumans
|
|
1608
|
-
|
|
1609
|
-
// Faux positif : un humain qui a été challengé inutilement
|
|
1651
|
+
totalHumans += confidence;
|
|
1652
|
+
maxHumanScore = Math.max(maxHumanScore, score);
|
|
1610
1653
|
if (score >= config.thresholds.low) {
|
|
1611
1654
|
falsePositives += confidence;
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
// 2. Évaluation sur les ancres immuables pour fixer l'échelle
|
|
1660
|
+
for (const anchor of STATIC_ANCHORS) {
|
|
1661
|
+
const score = calculateScore(anchor);
|
|
1662
|
+
const weight = anchor.weight;
|
|
1663
|
+
|
|
1664
|
+
if (anchor.type === 'request_blocked' || anchor.type === 'challenge_issued') {
|
|
1665
|
+
totalBots += weight;
|
|
1666
|
+
minBotScore = Math.min(minBotScore, score);
|
|
1667
|
+
if (score < config.thresholds.low) {
|
|
1668
|
+
falseNegatives += weight * 10; // Pénalité punitive forte
|
|
1669
|
+
}
|
|
1670
|
+
} else {
|
|
1671
|
+
totalHumans += weight;
|
|
1672
|
+
maxHumanScore = Math.max(maxHumanScore, score);
|
|
1673
|
+
if (score >= config.thresholds.low) {
|
|
1674
|
+
falsePositives += weight * 10; // Pénalité punitive forte
|
|
1615
1675
|
}
|
|
1616
1676
|
}
|
|
1617
1677
|
}
|
|
1618
1678
|
|
|
1619
1679
|
const falsePositiveRate = totalHumans > 0 ? falsePositives / totalHumans : 0;
|
|
1620
1680
|
const falseNegativeRate = totalBots > 0 ? falseNegatives / totalBots : 0;
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1681
|
+
|
|
1682
|
+
// 3. Pénalité de dérive d'échelle L2 (Régularisation par rapport au point d'origine)
|
|
1683
|
+
let regularizationPenalty = 0;
|
|
1684
|
+
if (currentConfig && currentConfig.weights) {
|
|
1685
|
+
for (const key in config.weights) {
|
|
1686
|
+
const originalVal = currentConfig.weights[key] || 0;
|
|
1687
|
+
regularizationPenalty += Math.pow(config.weights[key] - originalVal, 2);
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
// 4. Maximisation de la marge de séparation (SVM-like Margin Loss)
|
|
1692
|
+
const marginOverlap = Math.max(0, maxHumanScore - minBotScore);
|
|
1693
|
+
const marginPenalty = marginOverlap / 100;
|
|
1694
|
+
|
|
1695
|
+
const obj1 = falsePositiveRate + (regularizationPenalty * 0.05);
|
|
1696
|
+
const obj2 = falseNegativeRate + marginPenalty;
|
|
1697
|
+
|
|
1698
|
+
return [obj1, obj2];
|
|
1629
1699
|
};
|
|
1630
1700
|
};
|
|
1631
1701
|
|
package/src/js/mongodb-store.js
CHANGED
|
@@ -154,6 +154,19 @@ export function createMongoDbStore(db, collectionName = 'fingerprint_store') {
|
|
|
154
154
|
triggerFallback();
|
|
155
155
|
}
|
|
156
156
|
},
|
|
157
|
+
async clear() {
|
|
158
|
+
localStore.clear();
|
|
159
|
+
for (const timeout of localTimeouts.values()) {
|
|
160
|
+
clearTimeout(timeout);
|
|
161
|
+
}
|
|
162
|
+
localTimeouts.clear();
|
|
163
|
+
if (isDown) return;
|
|
164
|
+
try {
|
|
165
|
+
await collection.deleteMany({});
|
|
166
|
+
} catch (e) {
|
|
167
|
+
triggerFallback();
|
|
168
|
+
}
|
|
169
|
+
},
|
|
157
170
|
async init() {
|
|
158
171
|
try {
|
|
159
172
|
// Automates index configuration
|
package/src/js/redis-store.js
CHANGED
|
@@ -111,5 +111,18 @@ export function createRedisStore(redisClient) {
|
|
|
111
111
|
triggerFallback();
|
|
112
112
|
}
|
|
113
113
|
},
|
|
114
|
+
async clear() {
|
|
115
|
+
localStore.clear();
|
|
116
|
+
for (const timeout of localTimeouts.values()) {
|
|
117
|
+
clearTimeout(timeout);
|
|
118
|
+
}
|
|
119
|
+
localTimeouts.clear();
|
|
120
|
+
if (isDown) return;
|
|
121
|
+
try {
|
|
122
|
+
await redisClient.flushdb();
|
|
123
|
+
} catch (e) {
|
|
124
|
+
triggerFallback();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
114
127
|
};
|
|
115
128
|
}
|
package/src/js/sql-store.js
CHANGED
|
@@ -1,78 +1,81 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Creates a store adapter for Knex.js.
|
|
3
|
-
* This adapter is compatible with various SQL databases like PostgreSQL, MySQL, and SQLite.
|
|
4
|
-
* It handles serialization of complex objects and TTL for automatic data expiration.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Creates a store adapter for a Knex.js client.
|
|
9
|
-
*
|
|
10
|
-
* **Note:** You must create the table yourself before using the store.
|
|
11
|
-
* The table should have at least the following columns:
|
|
12
|
-
* - `key` (string, primary key)
|
|
13
|
-
* - `value` (text or json/jsonb)
|
|
14
|
-
* - `expiresAt` (datetime or timestamp with time zone)
|
|
15
|
-
*
|
|
16
|
-
* Example schema for PostgreSQL:
|
|
17
|
-
* ```sql
|
|
18
|
-
* CREATE TABLE your_table_name (
|
|
19
|
-
* "key" VARCHAR(255) PRIMARY KEY,
|
|
20
|
-
* "value" TEXT NOT NULL,
|
|
21
|
-
* "expiresAt" TIMESTAMPTZ
|
|
22
|
-
* );
|
|
23
|
-
* ```
|
|
24
|
-
*
|
|
25
|
-
* @param {import('knex').Knex} knex - An instance of the Knex client.
|
|
26
|
-
* @param {string} [tableName='fingerprint_store'] - The name of the table to use.
|
|
27
|
-
* @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
|
|
28
|
-
*/
|
|
29
|
-
export function createSqlStore(knex, tableName = 'fingerprint_store') {
|
|
30
|
-
// Custom replacer/reviver to handle Set serialization, similar to the Redis store.
|
|
31
|
-
const replacer = (k, v) => (v instanceof Set ? Array.from(v) : v);
|
|
32
|
-
const reviver = (k, v) => (k === 'ips' && Array.isArray(v) ? new Set(v) : v);
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
async get(key) {
|
|
36
|
-
const row = await knex(tableName).where('key', key).first();
|
|
37
|
-
if (!row) return null;
|
|
38
|
-
|
|
39
|
-
// Manually check for expiration, as not all SQL databases have automatic TTL cleanup.
|
|
40
|
-
if (row.expiresAt && new Date(row.expiresAt) < new Date()) {
|
|
41
|
-
await this.delete(key); // Clean up expired key.
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
return JSON.parse(row.value, reviver);
|
|
47
|
-
} catch (e) {
|
|
48
|
-
// In case of malformed JSON, treat it as a miss.
|
|
49
|
-
return null;
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
|
-
|
|
53
|
-
async set(key, value, ttl) {
|
|
54
|
-
const stringValue = JSON.stringify(value, replacer);
|
|
55
|
-
const expiresAt = ttl ? new Date(Date.now() + ttl * 1000) : null;
|
|
56
|
-
|
|
57
|
-
// Use native upsert capabilities of Knex for different SQL dialects.
|
|
58
|
-
await knex(tableName)
|
|
59
|
-
.insert({ key, value: stringValue, expiresAt })
|
|
60
|
-
.onConflict('key')
|
|
61
|
-
.merge();
|
|
62
|
-
},
|
|
63
|
-
|
|
64
|
-
async has(key) {
|
|
65
|
-
const row = await knex(tableName).where('key', key).first('key');
|
|
66
|
-
if (!row) return false;
|
|
67
|
-
// Also check for expiration here.
|
|
68
|
-
if (row.expiresAt && new Date(row.expiresAt) < new Date()) {
|
|
69
|
-
return false;
|
|
70
|
-
}
|
|
71
|
-
return true;
|
|
72
|
-
},
|
|
73
|
-
|
|
74
|
-
async delete(key) {
|
|
75
|
-
await knex(tableName).where('key', key).del();
|
|
76
|
-
},
|
|
77
|
-
|
|
1
|
+
/**
|
|
2
|
+
* @file Creates a store adapter for Knex.js.
|
|
3
|
+
* This adapter is compatible with various SQL databases like PostgreSQL, MySQL, and SQLite.
|
|
4
|
+
* It handles serialization of complex objects and TTL for automatic data expiration.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a store adapter for a Knex.js client.
|
|
9
|
+
*
|
|
10
|
+
* **Note:** You must create the table yourself before using the store.
|
|
11
|
+
* The table should have at least the following columns:
|
|
12
|
+
* - `key` (string, primary key)
|
|
13
|
+
* - `value` (text or json/jsonb)
|
|
14
|
+
* - `expiresAt` (datetime or timestamp with time zone)
|
|
15
|
+
*
|
|
16
|
+
* Example schema for PostgreSQL:
|
|
17
|
+
* ```sql
|
|
18
|
+
* CREATE TABLE your_table_name (
|
|
19
|
+
* "key" VARCHAR(255) PRIMARY KEY,
|
|
20
|
+
* "value" TEXT NOT NULL,
|
|
21
|
+
* "expiresAt" TIMESTAMPTZ
|
|
22
|
+
* );
|
|
23
|
+
* ```
|
|
24
|
+
*
|
|
25
|
+
* @param {import('knex').Knex} knex - An instance of the Knex client.
|
|
26
|
+
* @param {string} [tableName='fingerprint_store'] - The name of the table to use.
|
|
27
|
+
* @returns {import('./fingerprint.js').IStore} An object that complies with the IStore interface.
|
|
28
|
+
*/
|
|
29
|
+
export function createSqlStore(knex, tableName = 'fingerprint_store') {
|
|
30
|
+
// Custom replacer/reviver to handle Set serialization, similar to the Redis store.
|
|
31
|
+
const replacer = (k, v) => (v instanceof Set ? Array.from(v) : v);
|
|
32
|
+
const reviver = (k, v) => (k === 'ips' && Array.isArray(v) ? new Set(v) : v);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
async get(key) {
|
|
36
|
+
const row = await knex(tableName).where('key', key).first();
|
|
37
|
+
if (!row) return null;
|
|
38
|
+
|
|
39
|
+
// Manually check for expiration, as not all SQL databases have automatic TTL cleanup.
|
|
40
|
+
if (row.expiresAt && new Date(row.expiresAt) < new Date()) {
|
|
41
|
+
await this.delete(key); // Clean up expired key.
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(row.value, reviver);
|
|
47
|
+
} catch (e) {
|
|
48
|
+
// In case of malformed JSON, treat it as a miss.
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
async set(key, value, ttl) {
|
|
54
|
+
const stringValue = JSON.stringify(value, replacer);
|
|
55
|
+
const expiresAt = ttl ? new Date(Date.now() + ttl * 1000) : null;
|
|
56
|
+
|
|
57
|
+
// Use native upsert capabilities of Knex for different SQL dialects.
|
|
58
|
+
await knex(tableName)
|
|
59
|
+
.insert({ key, value: stringValue, expiresAt })
|
|
60
|
+
.onConflict('key')
|
|
61
|
+
.merge();
|
|
62
|
+
},
|
|
63
|
+
|
|
64
|
+
async has(key) {
|
|
65
|
+
const row = await knex(tableName).where('key', key).first('key');
|
|
66
|
+
if (!row) return false;
|
|
67
|
+
// Also check for expiration here.
|
|
68
|
+
if (row.expiresAt && new Date(row.expiresAt) < new Date()) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return true;
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async delete(key) {
|
|
75
|
+
await knex(tableName).where('key', key).del();
|
|
76
|
+
},
|
|
77
|
+
async clear() {
|
|
78
|
+
await knex(tableName).del();
|
|
79
|
+
}
|
|
80
|
+
};
|
|
78
81
|
}
|
|
@@ -92,8 +92,7 @@ describe('FingerprintEngine Challenge Validation', () => {
|
|
|
92
92
|
|
|
93
93
|
// The client solves the challenge
|
|
94
94
|
// --- FIX: Simulate the client creating the baseBlock for the solver ---
|
|
95
|
-
const
|
|
96
|
-
const baseBlock = new TextEncoder().encode(messageBase);
|
|
95
|
+
const baseBlock = new Uint8Array(challengeContext.baseBlock);
|
|
97
96
|
|
|
98
97
|
const cpuSolution = await solveCpuTargetInline(baseBlock, cpuTarget, null);
|
|
99
98
|
// The memory challenge seed does not include the fingerprint.
|
|
@@ -153,7 +152,7 @@ describe('FingerprintEngine Challenge Validation', () => {
|
|
|
153
152
|
// --- 2. Second request: Solution is submitted from "Machine B" ---
|
|
154
153
|
// --- FIX: Simulate the client creating the baseBlock for the solver ---
|
|
155
154
|
// The solver uses the fingerprint of the machine it's running on.
|
|
156
|
-
const messageBase = `${nonce}:${challengeContext.clientSecret}:fingerprint-B
|
|
155
|
+
const messageBase = `${nonce}:${challengeContext.clientSecret}:fingerprint-B:127.0.0.1::`;
|
|
157
156
|
const baseBlock = new TextEncoder().encode(messageBase);
|
|
158
157
|
|
|
159
158
|
const cpuSolution = await solveCpuTargetInline(baseBlock, cpuTarget, null);
|
|
@@ -739,7 +739,7 @@ describe('Fingerprint & PoW Security Suite', () => {
|
|
|
739
739
|
const memDifficulty = parseInt(challengeBody.match(/const memDifficulty = (\d+)/)[1], 10);
|
|
740
740
|
|
|
741
741
|
// The client constructs the base block and solves the challenge
|
|
742
|
-
const baseBlock = new TextEncoder().encode(`${nonce}:${clientSecret}:${solverFingerprint}
|
|
742
|
+
const baseBlock = new TextEncoder().encode(`${nonce}:${clientSecret}:${solverFingerprint}:${ip}::`);
|
|
743
743
|
const cpuSolution = await solveCpuTargetInline(baseBlock, cpuTargetHex, () => {});
|
|
744
744
|
const memSolution = await solveMemory(`:${nonce}:${clientSecret}`, memDifficulty);
|
|
745
745
|
|
|
@@ -821,7 +821,7 @@ describe('Fingerprint & PoW Security Suite', () => {
|
|
|
821
821
|
const memDifficulty = parseInt(challengeBody.match(/const memDifficulty = (\d+)/)[1], 10);
|
|
822
822
|
|
|
823
823
|
// The server stores the challenge context. We need to ensure the `baseBlock` is present for verification.
|
|
824
|
-
const baseBlock = new TextEncoder().encode(`${nonce}:${clientSecret}:${solverFingerprint}
|
|
824
|
+
const baseBlock = new TextEncoder().encode(`${nonce}:${clientSecret}:${solverFingerprint}:${ip}::`);
|
|
825
825
|
await inMemoryStore.set(`secret:${nonce}`, {
|
|
826
826
|
clientSecret: clientSecret,
|
|
827
827
|
cpuTarget: cpuTargetHex,
|
|
@@ -633,14 +633,14 @@ class ChallengeUtils
|
|
|
633
633
|
/**
|
|
634
634
|
* Crée le bloc de données de base pour le challenge CPU.
|
|
635
635
|
*/
|
|
636
|
-
public static function createCpuChallengeBaseBlock(string $nonce, string $clientSecret, string $fingerprint): string
|
|
636
|
+
public static function createCpuChallengeBaseBlock(string $nonce, string $clientSecret, string $fingerprint, string $clientIp = '', string $tlsSessionId = ''): string
|
|
637
637
|
{
|
|
638
638
|
$parts = explode('|', $fingerprint);
|
|
639
639
|
$filteredParts = array_filter($parts);
|
|
640
640
|
sort($filteredParts);
|
|
641
641
|
$sortedFingerprint = implode('|', $filteredParts);
|
|
642
642
|
|
|
643
|
-
return "{$nonce}:{$clientSecret}:{$sortedFingerprint}:";
|
|
643
|
+
return "{$nonce}:{$clientSecret}:{$sortedFingerprint}:{$clientIp}:{$tlsSessionId}:";
|
|
644
644
|
}
|
|
645
645
|
|
|
646
646
|
/**
|
|
@@ -58,6 +58,7 @@ class SecurityProfiles
|
|
|
58
58
|
'decayFactor' => 0.9,
|
|
59
59
|
'inactivityReset' => 5000,
|
|
60
60
|
],
|
|
61
|
+
'filterWhitelist' => 85.0, // Stratégie d'inspection modérée pour les IP/chemins en liste blanche
|
|
61
62
|
'wasm' => true,
|
|
62
63
|
],
|
|
63
64
|
|
|
@@ -108,6 +109,7 @@ class SecurityProfiles
|
|
|
108
109
|
],
|
|
109
110
|
'challengeNewDevices' => true, // Challenge all new devices
|
|
110
111
|
'wasm' => true,
|
|
112
|
+
'filterWhitelist' => true, // Tout comportement d'attaque certain bypass immédiatement la liste blanche
|
|
111
113
|
],
|
|
112
114
|
|
|
113
115
|
/**
|
|
@@ -155,6 +157,7 @@ class SecurityProfiles
|
|
|
155
157
|
],
|
|
156
158
|
// This would be a callable in PHP, but for now, we represent its intent.
|
|
157
159
|
'isApiRequest' => 'req.path.startsWith("/api/") || req.headers.accept?.includes("application/json")',
|
|
160
|
+
'filterWhitelist' => 75.0, // Seuil bas pour parer au vol de clés/tokens API légitimes
|
|
158
161
|
'wasm' => true,
|
|
159
162
|
],
|
|
160
163
|
|
|
@@ -204,6 +207,7 @@ class SecurityProfiles
|
|
|
204
207
|
'decayFactor' => 0.92,
|
|
205
208
|
'inactivityReset' => 10000,
|
|
206
209
|
],
|
|
210
|
+
'filterWhitelist' => 90.0, // Très tolérant, n'inspecte que si le score est presque au blocage
|
|
207
211
|
'wasm' => true,
|
|
208
212
|
],
|
|
209
213
|
|
|
@@ -257,6 +261,7 @@ class SecurityProfiles
|
|
|
257
261
|
// This would be a callable in PHP, but for now, we represent its intent.
|
|
258
262
|
'isApiRequest' => 'req.path.startsWith("/api/cart") || req.path.startsWith("/api/stock") || req.path.startsWith("/api/checkout")',
|
|
259
263
|
'wasm' => true,
|
|
264
|
+
'filterWhitelist' => true, // Tolérance zéro pour le scraping / scalping distribué
|
|
260
265
|
],
|
|
261
266
|
];
|
|
262
267
|
|