@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.
@@ -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
- // This is a placeholder that can be fleshed out, but currently the regex handles it.
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.70,
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.70,
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.70,
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('(?:{{|\\\\$\\\\{|<%=\\s*|#{\\s*)[^}]*?' + DANGEROUS_SINKS + '.*?(?:}}|}|%>|})', 'i')
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('(?:{{|\\\\$\\\\{|<%=\\s*|#{\\s*).*?' + ARITHMETIC + '.*?(?:}}|}|%>|})')
25
+ pattern: new RegExp('(?:{{|\\$\\{|<%=\\s*|#{\\s*).*?' + ARITHMETIC + '.*?(?:}}|}|%>|})')
26
26
  },
27
27
  {
28
28
  id: 'ssti-template-syntax-alone',
@@ -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*["']?[^"'>]*(?:alert|confirm|prompt|document\.|window\.|eval|fetch|Function\s*\()/i
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
- evaluatePayloads,
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);
@@ -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 = Object.fromEntries(
63
- req.headers.cookie.split('; ').map(c => c.split('='))
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