@chiranthmoger/fortifyjs 1.1.0 → 1.1.1
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 +231 -100
- package/bin/banner.js +29 -0
- package/bin/fortifyjs.js +12 -12
- package/index.d.ts +103 -3
- package/package.json +3 -2
- package/src/adapters/express.js +36 -21
- package/src/core/engine.js +63 -1
- package/src/core/normalizer.js +2 -2
- package/src/core/sinks.js +214 -0
- package/src/detectors/cmdi.js +7 -2
- package/src/detectors/open-redirect.js +10 -0
- package/src/detectors/path-traversal.js +17 -7
- package/src/detectors/prompt-injection.js +110 -0
- package/src/detectors/prototype-pollution.js +12 -2
- package/src/detectors/sqli.js +10 -0
- package/src/detectors/ssrf.js +202 -13
- package/src/detectors/template-injection.js +2 -2
- package/src/detectors/xss.js +22 -17
- package/src/index.js +34 -2
- package/src/presets.js +19 -1
- package/src/shields/csrf.js +10 -4
- package/src/shields/file-upload.js +5 -0
- package/src/shields/llm-guard.js +235 -0
- package/src/shields/rate-limiter.js +53 -11
- package/src/shields/sanitizer.js +113 -0
- package/src/shields/store.js +97 -0
- package/src/detectors/sqli.js.bak +0 -446
package/src/detectors/ssrf.js
CHANGED
|
@@ -1,8 +1,189 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
function parseIpv4ToNumber(ipStr) {
|
|
4
|
+
if (!ipStr || typeof ipStr !== 'string') return null;
|
|
5
|
+
const trimmed = ipStr.trim();
|
|
6
|
+
|
|
7
|
+
// Single hex integer (e.g. 0x7f000001)
|
|
8
|
+
if (/^0x[0-9a-fA-F]+$/i.test(trimmed)) {
|
|
9
|
+
const num = parseInt(trimmed, 16);
|
|
10
|
+
return num >= 0 && num <= 0xFFFFFFFF ? num : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Single octal integer (e.g. 017700000001)
|
|
14
|
+
if (/^0[0-7]+$/.test(trimmed)) {
|
|
15
|
+
const num = parseInt(trimmed, 8);
|
|
16
|
+
return num >= 0 && num <= 0xFFFFFFFF ? num : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Single decimal integer (e.g. 2130706433)
|
|
20
|
+
if (/^\d+$/.test(trimmed)) {
|
|
21
|
+
const num = Number(trimmed);
|
|
22
|
+
return num >= 0 && num <= 0xFFFFFFFF ? num : null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Dotted parts (can be 1 to 4 parts, hex/octal/decimal)
|
|
26
|
+
const parts = trimmed.split('.');
|
|
27
|
+
if (parts.length < 1 || parts.length > 4) return null;
|
|
28
|
+
|
|
29
|
+
const parsedParts = [];
|
|
30
|
+
for (const part of parts) {
|
|
31
|
+
if (!part) return null;
|
|
32
|
+
let val;
|
|
33
|
+
if (/^0x[0-9a-fA-F]+$/i.test(part)) {
|
|
34
|
+
val = parseInt(part, 16);
|
|
35
|
+
} else if (/^0\d+$/.test(part)) {
|
|
36
|
+
val = parseInt(part, 8);
|
|
37
|
+
} else if (/^\d+$/.test(part)) {
|
|
38
|
+
val = parseInt(part, 10);
|
|
39
|
+
} else {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
if (isNaN(val) || val < 0) return null;
|
|
43
|
+
parsedParts.push(val);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (parsedParts.length === 4) {
|
|
47
|
+
if (parsedParts.some(p => p > 255)) return null;
|
|
48
|
+
return ((parsedParts[0] << 24) >>> 0) + ((parsedParts[1] << 16) >>> 0) + ((parsedParts[2] << 8) >>> 0) + parsedParts[3];
|
|
49
|
+
} else if (parsedParts.length === 3) {
|
|
50
|
+
if (parsedParts[0] > 255 || parsedParts[1] > 255 || parsedParts[2] > 0xFFFF) return null;
|
|
51
|
+
return ((parsedParts[0] << 24) >>> 0) + ((parsedParts[1] << 16) >>> 0) + parsedParts[2];
|
|
52
|
+
} else if (parsedParts.length === 2) {
|
|
53
|
+
if (parsedParts[0] > 255 || parsedParts[1] > 0xFFFFFF) return null;
|
|
54
|
+
return ((parsedParts[0] << 24) >>> 0) + parsedParts[1];
|
|
55
|
+
} else if (parsedParts.length === 1) {
|
|
56
|
+
if (parsedParts[0] > 0xFFFFFFFF) return null;
|
|
57
|
+
return parsedParts[0] >>> 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isPrivateIpv4Number(num) {
|
|
64
|
+
if (num === null || num === undefined) return false;
|
|
65
|
+
// 0.0.0.0/8
|
|
66
|
+
if (((num & 0xFF000000) >>> 0) === 0x00000000) return true;
|
|
67
|
+
// 10.0.0.0/8
|
|
68
|
+
if (((num & 0xFF000000) >>> 0) === 0x0A000000) return true;
|
|
69
|
+
// 127.0.0.0/8 Loopback
|
|
70
|
+
if (((num & 0xFF000000) >>> 0) === 0x7F000000) return true;
|
|
71
|
+
// 100.64.0.0/10 Carrier-grade NAT
|
|
72
|
+
if (((num & 0xFFC00000) >>> 0) === 0x64400000) return true;
|
|
73
|
+
// 169.254.0.0/16 Link-local / Cloud Metadata
|
|
74
|
+
if (((num & 0xFFFF0000) >>> 0) === 0xA9FE0000) return true;
|
|
75
|
+
// 172.16.0.0/12 Private
|
|
76
|
+
if (((num & 0xFFF00000) >>> 0) === 0xAC100000) return true;
|
|
77
|
+
// 192.0.0.0/24 IETF Protocol
|
|
78
|
+
if (((num & 0xFFFFFF00) >>> 0) === 0xC0000000) return true;
|
|
79
|
+
// 192.168.0.0/16 Private
|
|
80
|
+
if (((num & 0xFFFF0000) >>> 0) === 0xC0A80000) return true;
|
|
81
|
+
// 198.18.0.0/15 Benchmarking
|
|
82
|
+
if (((num & 0xFFFE0000) >>> 0) === 0xC6120000) return true;
|
|
83
|
+
// 224.0.0.0/4 Multicast
|
|
84
|
+
if (((num & 0xF0000000) >>> 0) === 0xE0000000) return true;
|
|
85
|
+
// 240.0.0.0/4 Reserved
|
|
86
|
+
if (((num & 0xF0000000) >>> 0) === 0xF0000000) return true;
|
|
87
|
+
// 255.255.255.255 Broadcast
|
|
88
|
+
if ((num >>> 0) === 0xFFFFFFFF) return true;
|
|
89
|
+
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isPrivateIPv6(ipStr) {
|
|
94
|
+
if (!ipStr || typeof ipStr !== 'string') return false;
|
|
95
|
+
let clean = ipStr.trim().toLowerCase();
|
|
96
|
+
if (clean.startsWith('[') && clean.endsWith(']')) {
|
|
97
|
+
clean = clean.slice(1, -1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Canonicalize hextet leading zeros
|
|
101
|
+
const parts = clean.split(':');
|
|
102
|
+
const normalizedHextets = parts.map(p => {
|
|
103
|
+
if (!p) return '';
|
|
104
|
+
if (p.includes('.')) return p; // IPv4 dotted part
|
|
105
|
+
return p.replace(/^0+/, '') || '0';
|
|
106
|
+
}).join(':');
|
|
107
|
+
|
|
108
|
+
// Loopback ::1 or 0:0:0:0:0:0:0:1
|
|
109
|
+
if (clean === '::1' || normalizedHextets === '0:0:0:0:0:0:0:1' || normalizedHextets === '::1' || /^0*(?::0*)*:1$/.test(clean)) return true;
|
|
110
|
+
// Unspecified :: or 0:0:0:0:0:0:0:0
|
|
111
|
+
if (clean === '::' || normalizedHextets === '0:0:0:0:0:0:0:0' || normalizedHextets === '::' || /^0*(?::0*)*:0*$/.test(clean)) return true;
|
|
112
|
+
|
|
113
|
+
// IPv4 mapped IPv6 (e.g. ::ffff:127.0.0.1, ::ffff:7f00:1, ::127.0.0.1, 0000:0000:0000:0000:0000:ffff:127.0.0.1)
|
|
114
|
+
if (
|
|
115
|
+
clean.startsWith('::ffff:') ||
|
|
116
|
+
clean.startsWith('0:0:0:0:0:ffff:') ||
|
|
117
|
+
normalizedHextets.startsWith('0:0:0:0:0:ffff:') ||
|
|
118
|
+
clean.startsWith('::')
|
|
119
|
+
) {
|
|
120
|
+
let v4Part = clean.replace(/^(?:::ffff:|0:0:0:0:0:ffff:|0000:0000:0000:0000:0000:ffff:|::)/, '');
|
|
121
|
+
if (v4Part.includes('.')) {
|
|
122
|
+
const v4Num = parseIpv4ToNumber(v4Part);
|
|
123
|
+
if (v4Num !== null && isPrivateIpv4Number(v4Num)) return true;
|
|
124
|
+
} else if (v4Part.includes(':')) {
|
|
125
|
+
const v4SubParts = v4Part.split(':');
|
|
126
|
+
if (v4SubParts.length === 2) {
|
|
127
|
+
const high = parseInt(v4SubParts[0], 16);
|
|
128
|
+
const low = parseInt(v4SubParts[1], 16);
|
|
129
|
+
if (!isNaN(high) && !isNaN(low)) {
|
|
130
|
+
const v4Num = ((high << 16) >>> 0) + low;
|
|
131
|
+
if (isPrivateIpv4Number(v4Num)) return true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Unique Local (fc00::/7 -> fc00 to fdff)
|
|
138
|
+
if (/^f[cd][0-9a-f]{0,2}:/i.test(normalizedHextets) || /^f[cd][0-9a-f]{2}:/i.test(clean)) return true;
|
|
139
|
+
// Link-Local (fe80::/10 -> fe80 to febf)
|
|
140
|
+
if (/^fe[89ab][0-9a-f]{0,2}:/i.test(normalizedHextets) || /^fe[89ab][0-9a-f]:/i.test(clean)) return true;
|
|
141
|
+
// AWS IMDS IPv6 (fd00:ec2::254)
|
|
142
|
+
if (clean.startsWith('fd00:ec2:') || normalizedHextets.startsWith('fd00:ec2:')) return true;
|
|
143
|
+
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
3
147
|
function isPrivateIP(ipString) {
|
|
4
|
-
if (!ipString) return false;
|
|
5
|
-
|
|
148
|
+
if (!ipString || typeof ipString !== 'string') return false;
|
|
149
|
+
let clean = ipString.trim();
|
|
150
|
+
if (clean.startsWith('[') && clean.endsWith(']')) {
|
|
151
|
+
clean = clean.slice(1, -1);
|
|
152
|
+
}
|
|
153
|
+
const lower = clean.toLowerCase();
|
|
154
|
+
if (lower === 'localhost' || lower.endsWith('.localhost')) return true;
|
|
155
|
+
|
|
156
|
+
// Try IPv4
|
|
157
|
+
const v4Num = parseIpv4ToNumber(clean);
|
|
158
|
+
if (v4Num !== null) {
|
|
159
|
+
return isPrivateIpv4Number(v4Num);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Try IPv6
|
|
163
|
+
return isPrivateIPv6(clean);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function extractHostFromUrlString(urlString) {
|
|
167
|
+
if (!urlString || typeof urlString !== 'string') return null;
|
|
168
|
+
try {
|
|
169
|
+
const match = urlString.match(/^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/(?:[^@\/\?#]+@)?(\[[^\]]+\]|[^/?#:]+)/);
|
|
170
|
+
if (match) {
|
|
171
|
+
let host = match[1];
|
|
172
|
+
if (host.startsWith('[') && host.endsWith(']')) {
|
|
173
|
+
host = host.slice(1, -1);
|
|
174
|
+
}
|
|
175
|
+
return host;
|
|
176
|
+
}
|
|
177
|
+
} catch (_) {}
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function hasPrivateHostInUrl(payload) {
|
|
182
|
+
if (!payload || typeof payload !== 'string') return false;
|
|
183
|
+
const host = extractHostFromUrlString(payload);
|
|
184
|
+
if (host) {
|
|
185
|
+
if (isPrivateIP(host)) return true;
|
|
186
|
+
}
|
|
6
187
|
return false;
|
|
7
188
|
}
|
|
8
189
|
|
|
@@ -11,48 +192,56 @@ module.exports = {
|
|
|
11
192
|
label: 'ssrf',
|
|
12
193
|
|
|
13
194
|
isPrivateIP,
|
|
195
|
+
parseIpv4ToNumber,
|
|
196
|
+
isPrivateIpv4Number,
|
|
197
|
+
isPrivateIPv6,
|
|
14
198
|
|
|
15
199
|
getSignals() {
|
|
16
200
|
return [
|
|
17
201
|
{
|
|
18
202
|
id: 'private-ip-access',
|
|
19
203
|
confidence: 0.85,
|
|
20
|
-
pattern: /(?:https?|ftp):\/\/(?:127\.\d+\.\d+\.\d+|10\.\d+\.\d+\.\d+|172\.(?:1[6-9]|2[0-9]|3[0-1])\.\d+\.\d+|192\.168\.\d+\.\d+|\[?::1\]?|\[?::ffff:127\.0\.0\.1\]?|0\.0\.0\.0)(?::\d+)?(?:[\/\?#]|$)/i
|
|
204
|
+
pattern: /(?:https?|ftp):\/\/(?:[^@\/\?#]+@)?(?:127\.\d+\.\d+\.\d+|10\.\d+\.\d+\.\d+|172\.(?:1[6-9]|2[0-9]|3[0-1])\.\d+\.\d+|192\.168\.\d+\.\d+|\[?::1\]?|\[?::ffff:127\.0\.0\.1\]?|0\.0\.0\.0)(?::\d+)?(?:[\/\?#]|$)/i
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: 'bitwise-private-ip',
|
|
208
|
+
confidence: 0.85,
|
|
209
|
+
test: hasPrivateHostInUrl
|
|
21
210
|
},
|
|
22
211
|
{
|
|
23
212
|
id: 'cloud-metadata-access',
|
|
24
213
|
confidence: 0.90,
|
|
25
|
-
pattern: /(?:https?|ftp):\/\/(?:169\.254\.169\.254|metadata\.google\.internal|100\.100\.100\.200|fd00:ec2::254|192\.0\.0\.192)/i
|
|
214
|
+
pattern: /(?:https?|ftp):\/\/(?:[^@\/\?#]+@)?(?:169\.254\.169\.254|metadata\.google\.internal|100\.100\.100\.200|fd00:ec2::254|192\.0\.0\.192|instance-data)(?::\d+)?(?:[\/\?#]|$)/i
|
|
26
215
|
},
|
|
27
216
|
{
|
|
28
217
|
id: 'dangerous-url-scheme',
|
|
29
218
|
confidence: 0.80,
|
|
30
|
-
pattern: /^(?:file|gopher|dict|ldap|tftp):\/\//i
|
|
219
|
+
pattern: /^(?:file|gopher|dict|ldap|tftp|php|data):\/\//i
|
|
31
220
|
},
|
|
32
221
|
{
|
|
33
222
|
id: 'decimal-ip',
|
|
34
|
-
confidence: 0.
|
|
35
|
-
pattern: /(?:https?|ftp):\/\/(?:2130706433|3232235520)(?::\d+)?(?:[\/\?#]|$)/i
|
|
223
|
+
confidence: 0.75,
|
|
224
|
+
pattern: /(?:https?|ftp):\/\/(?:[^@\/\?#]+@)?(?:2130706433|3232235520|167772160|2886729728|2851995648|0)(?::\d+)?(?:[\/\?#]|$)/i
|
|
36
225
|
},
|
|
37
226
|
{
|
|
38
227
|
id: 'hex-ip',
|
|
39
|
-
confidence: 0.
|
|
40
|
-
pattern: /(?:https?|ftp):\/\/(?:0x7f000001|0x7f\.0x00\.0x00\.0x01|0x7f\.0\.0\.1)(?::\d+)?(?:[\/\?#]|$)/i
|
|
228
|
+
confidence: 0.75,
|
|
229
|
+
pattern: /(?:https?|ftp):\/\/(?:[^@\/\?#]+@)?(?:0x7f000001|0x7f\.0x00\.0x00\.0x01|0x7f\.0\.0\.1|0x0a000001|0xc0a80001)(?::\d+)?(?:[\/\?#]|$)/i
|
|
41
230
|
},
|
|
42
231
|
{
|
|
43
232
|
id: 'octal-ip',
|
|
44
|
-
confidence: 0.
|
|
45
|
-
pattern: /(?:https?|ftp):\/\/(?:0177\.0000\.0000\.0001|0177\.0\.0\.1)(?::\d+)?(?:[\/\?#]|$)/i
|
|
233
|
+
confidence: 0.75,
|
|
234
|
+
pattern: /(?:https?|ftp):\/\/(?:[^@\/\?#]+@)?(?:017700000001|0[0-7]{9,11}|0177\.0000\.0000\.0001|0177\.0\.0\.1|0012\.0\.0\.1|0300\.0250\.0\.1)(?::\d+)?(?:[\/\?#]|$)/i
|
|
46
235
|
},
|
|
47
236
|
{
|
|
48
237
|
id: 'dns-rebinding-localhost',
|
|
49
238
|
confidence: 0.75,
|
|
50
|
-
pattern: /(?:https?|ftp):\/\/(?:localhost|0\.0\.0\.0|\[0:0:0:0:0:0:0:1\])(?::\d+)?(?:[\/\?#]|$)/i
|
|
239
|
+
pattern: /(?:https?|ftp):\/\/(?:[^@\/\?#]+@)?(?:localhost|[a-zA-Z0-9.-]+\.localhost|0\.0\.0\.0|\[0:0:0:0:0:0:0:1\]|\[0000:0000:0000:0000:0000:0000:0000:0001\]|\[::1\]|\[::\])(?::\d+)?(?:[\/\?#]|$)/i
|
|
51
240
|
},
|
|
52
241
|
{
|
|
53
242
|
id: 'dns-rebinding-service',
|
|
54
243
|
confidence: 0.80,
|
|
55
|
-
pattern: /(?:https?|ftp):\/\/(?:[a-zA-Z0-9.-]+\.(?:nip\.io|xip\.io|1u\.ms|sslip\.io)|spoofed\.com)/i
|
|
244
|
+
pattern: /(?:https?|ftp):\/\/(?:[^@\/\?#]+@)?(?:[a-zA-Z0-9.-]+\.(?:nip\.io|xip\.io|1u\.ms|sslip\.io)|spoofed\.com)/i
|
|
56
245
|
},
|
|
57
246
|
{
|
|
58
247
|
id: 'short-url-ssrf',
|
|
@@ -12,7 +12,7 @@ module.exports = {
|
|
|
12
12
|
{
|
|
13
13
|
id: 'ssti-dangerous-sink',
|
|
14
14
|
confidence: 0.90,
|
|
15
|
-
pattern: new RegExp('(?:{{
|
|
15
|
+
pattern: new RegExp('(?:{{|\\$\\{|<%=\\s*|#{\\s*)[^}]*?' + DANGEROUS_SINKS + '.*?(?:}}|}|%>|})', 'i')
|
|
16
16
|
},
|
|
17
17
|
{
|
|
18
18
|
id: 'ssti-jinja-dangerous',
|
|
@@ -22,7 +22,7 @@ module.exports = {
|
|
|
22
22
|
{
|
|
23
23
|
id: 'ssti-arithmetic-probe',
|
|
24
24
|
confidence: 0.70,
|
|
25
|
-
pattern: new RegExp('(?:{{
|
|
25
|
+
pattern: new RegExp('(?:{{|\\$\\{|<%=\\s*|#{\\s*).*?' + ARITHMETIC + '.*?(?:}}|}|%>|})')
|
|
26
26
|
},
|
|
27
27
|
{
|
|
28
28
|
id: 'ssti-template-syntax-alone',
|
package/src/detectors/xss.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const JS_EXECUTION_SINKS = new Set(['alert', 'confirm', 'prompt', 'eval', 'fetch', 'function', 'settimeout', 'setinterval']);
|
|
3
|
+
const JS_EXECUTION_SINKS = new Set(['alert', 'confirm', 'prompt', 'eval', 'fetch', 'function', 'settimeout', 'setinterval', 'location', 'navigate', 'assign', 'replace', 'write', 'writeln']);
|
|
4
4
|
const JS_GLOBAL_OBJECTS = new Set(['window', 'globalthis', 'self', 'top', 'parent']);
|
|
5
5
|
function isAsciiLetter(ch) {
|
|
6
6
|
return /[A-Za-z]/.test(ch);
|
|
@@ -105,22 +105,27 @@ module.exports = {
|
|
|
105
105
|
name: 'xss',
|
|
106
106
|
label: 'xss',
|
|
107
107
|
getSignals() {
|
|
108
|
-
return
|
|
109
|
-
{
|
|
110
|
-
id: 'script-tag',
|
|
111
|
-
confidence: 0.85,
|
|
112
|
-
pattern: /<\s*script\b/i
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
id: 'html-event-attribute',
|
|
116
|
-
confidence: 0.75,
|
|
117
|
-
pattern: /<[^>]+\bon\w+\s*=/i
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
id: 'event-handler-payload',
|
|
121
|
-
confidence: 0.75,
|
|
122
|
-
pattern: /\bon\w+\s*=\s*["']
|
|
123
|
-
},
|
|
108
|
+
return [
|
|
109
|
+
{
|
|
110
|
+
id: 'script-tag',
|
|
111
|
+
confidence: 0.85,
|
|
112
|
+
pattern: /<\s*script\b/i
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: 'html-event-attribute',
|
|
116
|
+
confidence: 0.75,
|
|
117
|
+
pattern: /<[^>]+\bon\w+\s*=/i
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: 'event-handler-payload',
|
|
121
|
+
confidence: 0.75,
|
|
122
|
+
pattern: /\bon\w+\s*=\s*(?:["'][^"'>]*|[^"'\s>]*)(?:(?:alert|confirm|prompt|eval|fetch|location|write)\s*\(|(?:window|globalThis|self|top|parent|document)(?:\.|\[)|Function\s*\()/i
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: 'js-bracket-execution-sink',
|
|
126
|
+
confidence: 0.75,
|
|
127
|
+
pattern: /(?:window|globalThis|self|top|parent|document)\s*\[\s*['"][^'"]+['"]\s*\]\s*(?:\(|=)/i
|
|
128
|
+
},
|
|
124
129
|
{
|
|
125
130
|
id: 'javascript-url-with-sink',
|
|
126
131
|
confidence: 0.75,
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
-
const { shield } = require('./presets');
|
|
2
|
+
const { shield, PRESETS } = require('./presets');
|
|
3
3
|
const { DetectionEngine } = require('./core/engine');
|
|
4
4
|
const { Normalizer } = require('./core/normalizer');
|
|
5
5
|
const { expressMiddleware, fortifyjs, secureRouter } = require('./adapters/express');
|
|
@@ -9,6 +9,22 @@ const { koaMiddleware } = require('./adapters/koa');
|
|
|
9
9
|
const { honoMiddleware } = require('./adapters/hono');
|
|
10
10
|
const { genericAdapter } = require('./adapters/generic');
|
|
11
11
|
const nextjsAdapter = require('./adapters/nextjs');
|
|
12
|
+
const {
|
|
13
|
+
FortifySinkError,
|
|
14
|
+
FortifyPromptError,
|
|
15
|
+
assertSafeCommand,
|
|
16
|
+
assertSafePath,
|
|
17
|
+
assertSafeUrl,
|
|
18
|
+
assertSafeNoSql,
|
|
19
|
+
assertSafeRedirect,
|
|
20
|
+
assertSafePrompt,
|
|
21
|
+
scanPrompt
|
|
22
|
+
} = require('./core/sinks');
|
|
23
|
+
const { llmGuard } = require('./shields/llm-guard');
|
|
24
|
+
const { sanitizerFactory, sanitizeObject } = require('./shields/sanitizer');
|
|
25
|
+
const { isPrivateIP } = require('./detectors/ssrf');
|
|
26
|
+
const { MemoryStore, BaseStore } = require('./shields/store');
|
|
27
|
+
|
|
12
28
|
const DEFAULT_THRESHOLD = 0.5;
|
|
13
29
|
|
|
14
30
|
function samplePayload(sample) {
|
|
@@ -57,6 +73,7 @@ function assertSafeSqlQuery(query, options = {}) {
|
|
|
57
73
|
}
|
|
58
74
|
return result;
|
|
59
75
|
}
|
|
76
|
+
|
|
60
77
|
function evaluatePayloads(samples, options = {}) {
|
|
61
78
|
if (!Array.isArray(samples)) {
|
|
62
79
|
throw new TypeError('samples must be an array');
|
|
@@ -112,10 +129,13 @@ function evaluatePayloads(samples, options = {}) {
|
|
|
112
129
|
|
|
113
130
|
module.exports = {
|
|
114
131
|
shield,
|
|
132
|
+
PRESETS,
|
|
115
133
|
DetectionEngine,
|
|
116
134
|
Normalizer,
|
|
117
135
|
evaluatePayloads,
|
|
118
136
|
fortifyjsQueryError,
|
|
137
|
+
FortifySinkError,
|
|
138
|
+
FortifyPromptError,
|
|
119
139
|
expressMiddleware,
|
|
120
140
|
createNestMiddleware,
|
|
121
141
|
fortifyjs,
|
|
@@ -123,7 +143,19 @@ module.exports = {
|
|
|
123
143
|
nestjsMiddleware,
|
|
124
144
|
scanSqlQuery,
|
|
125
145
|
assertSafeSqlQuery,
|
|
126
|
-
|
|
146
|
+
assertSafeCommand,
|
|
147
|
+
assertSafePath,
|
|
148
|
+
assertSafeUrl,
|
|
149
|
+
assertSafeNoSql,
|
|
150
|
+
assertSafeRedirect,
|
|
151
|
+
assertSafePrompt,
|
|
152
|
+
scanPrompt,
|
|
153
|
+
llmGuard,
|
|
154
|
+
sanitizerFactory,
|
|
155
|
+
sanitizeObject,
|
|
156
|
+
isPrivateIP,
|
|
157
|
+
MemoryStore,
|
|
158
|
+
BaseStore,
|
|
127
159
|
fastifyPlugin,
|
|
128
160
|
koaMiddleware,
|
|
129
161
|
honoMiddleware,
|
package/src/presets.js
CHANGED
|
@@ -144,10 +144,21 @@ function shield(tier = 'basic', overrides = {}) {
|
|
|
144
144
|
stack.push(dashboardFactory(config.dashboard, logger));
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
+
if (config.sanitize) {
|
|
148
|
+
const { sanitizerFactory } = require('./shields/sanitizer');
|
|
149
|
+
stack.push(sanitizerFactory(typeof config.sanitize === 'object' ? config.sanitize : {}));
|
|
150
|
+
}
|
|
151
|
+
|
|
147
152
|
// Build detector
|
|
148
153
|
const detectorOptions = {
|
|
149
154
|
level: config.level,
|
|
150
|
-
behavioral: config.behavioral
|
|
155
|
+
behavioral: config.behavioral,
|
|
156
|
+
onBlocked: config.onBlocked,
|
|
157
|
+
allowRoutes: config.allowRoutes,
|
|
158
|
+
allowParams: config.allowParams,
|
|
159
|
+
routeLevels: config.routeLevels,
|
|
160
|
+
whitelist: config.whitelist,
|
|
161
|
+
mode: config.mode
|
|
151
162
|
};
|
|
152
163
|
|
|
153
164
|
// Note: We use expressMiddleware factory here
|
|
@@ -162,6 +173,13 @@ function shield(tier = 'basic', overrides = {}) {
|
|
|
162
173
|
stack.push(detectorMiddleware);
|
|
163
174
|
|
|
164
175
|
return function fortifyjsStack(req, res, next) {
|
|
176
|
+
if (config.allowRoutes && Array.isArray(config.allowRoutes)) {
|
|
177
|
+
const pathname = req.path || (req.url ? req.url.split('?')[0] : '');
|
|
178
|
+
if (config.allowRoutes.includes(pathname)) {
|
|
179
|
+
return next();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
165
183
|
let index = 0;
|
|
166
184
|
function runNext(err) {
|
|
167
185
|
if (err) return next(err);
|
package/src/shields/csrf.js
CHANGED
|
@@ -9,7 +9,7 @@ const crypto = require('crypto');
|
|
|
9
9
|
*/
|
|
10
10
|
module.exports = function createCsrfShield(options = {}) {
|
|
11
11
|
const secret = options.secret || crypto.randomBytes(32).toString('hex');
|
|
12
|
-
if (!options.secret) {
|
|
12
|
+
if (!options.secret && process.env.NODE_ENV !== 'test' && !options.silent) {
|
|
13
13
|
console.warn('fortifyjs: CSRF secret not provided. A random secret was generated, but it will not persist across restarts.');
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -59,9 +59,15 @@ module.exports = function createCsrfShield(options = {}) {
|
|
|
59
59
|
// We assume cookie-parser might not be present, so we do basic parsing
|
|
60
60
|
let cookies = req.cookies || {};
|
|
61
61
|
if (!req.cookies && req.headers.cookie) {
|
|
62
|
-
cookies =
|
|
63
|
-
|
|
64
|
-
)
|
|
62
|
+
cookies = {};
|
|
63
|
+
const pairs = req.headers.cookie.split(';');
|
|
64
|
+
for (const pair of pairs) {
|
|
65
|
+
const trimmed = pair.trim();
|
|
66
|
+
const idx = trimmed.indexOf('=');
|
|
67
|
+
if (idx !== -1) {
|
|
68
|
+
cookies[trimmed.slice(0, idx).trim()] = trimmed.slice(idx + 1).trim();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
const currentSignedToken = cookies[cookieName];
|
|
@@ -80,6 +80,11 @@ function fileUploadShieldFactory(options = {}) {
|
|
|
80
80
|
return false;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
if (filename.includes(':') || filename.includes('::$DATA')) {
|
|
84
|
+
block(res, next, 'Alternate Data Stream syntax or forbidden character in filename');
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
83
88
|
const parts = filename.split('.');
|
|
84
89
|
const ext = parts.length > 1 ? '.' + parts[parts.length - 1].toLowerCase() : '';
|
|
85
90
|
|