@chiranthmoger/fortifyjs 1.1.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.
Files changed (50) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +186 -0
  3. package/bin/fortifyjs.js +135 -0
  4. package/examples/fastify.js +18 -0
  5. package/examples/hono.js +13 -0
  6. package/examples/kitchen-sink.js +50 -0
  7. package/examples/koa.js +14 -0
  8. package/examples/minimal-express.js +13 -0
  9. package/examples/production-express.js +20 -0
  10. package/index.d.ts +101 -0
  11. package/package.json +70 -0
  12. package/src/adapters/express.js +1027 -0
  13. package/src/adapters/fastify.js +56 -0
  14. package/src/adapters/generic.js +15 -0
  15. package/src/adapters/hono.js +77 -0
  16. package/src/adapters/koa.js +58 -0
  17. package/src/adapters/nestjs.js +15 -0
  18. package/src/adapters/nextjs.js +84 -0
  19. package/src/analyzers/adaptive.js +92 -0
  20. package/src/analyzers/behavioral.js +264 -0
  21. package/src/core/confidence.js +23 -0
  22. package/src/core/engine.js +162 -0
  23. package/src/core/normalizer.js +149 -0
  24. package/src/core/whitelist.js +40 -0
  25. package/src/dashboard/handler.js +307 -0
  26. package/src/detectors/cmdi.js +82 -0
  27. package/src/detectors/crlf.js +33 -0
  28. package/src/detectors/graphql.js +56 -0
  29. package/src/detectors/hpp.js +38 -0
  30. package/src/detectors/ldap.js +50 -0
  31. package/src/detectors/nosqli.js +134 -0
  32. package/src/detectors/open-redirect.js +42 -0
  33. package/src/detectors/path-traversal.js +55 -0
  34. package/src/detectors/prototype-pollution.js +65 -0
  35. package/src/detectors/sqli.js +447 -0
  36. package/src/detectors/sqli.js.bak +446 -0
  37. package/src/detectors/ssrf.js +64 -0
  38. package/src/detectors/template-injection.js +34 -0
  39. package/src/detectors/xss.js +191 -0
  40. package/src/detectors/xxe.js +45 -0
  41. package/src/forensics/reporter.js +74 -0
  42. package/src/index.js +132 -0
  43. package/src/logger.js +110 -0
  44. package/src/presets.js +183 -0
  45. package/src/shields/bot-detector.js +88 -0
  46. package/src/shields/cors.js +95 -0
  47. package/src/shields/csrf.js +120 -0
  48. package/src/shields/file-upload.js +160 -0
  49. package/src/shields/headers.js +99 -0
  50. package/src/shields/rate-limiter.js +70 -0
package/src/presets.js ADDED
@@ -0,0 +1,183 @@
1
+ 'use strict';
2
+
3
+ const { DetectionEngine } = require('./core/engine');
4
+ const { expressMiddleware } = require('./adapters/express');
5
+ const { Logger } = require('./logger');
6
+
7
+ const PRESETS = {
8
+ basic: {
9
+ level: 'balanced',
10
+ headers: true,
11
+ rateLimit: { max: 100, windowMs: 15 * 60 * 1000 },
12
+ cors: { origin: 'same-origin' },
13
+ csrf: false,
14
+ botDetection: { enabled: true, action: 'flag' },
15
+ behavioral: { enabled: true, entropyOnly: true },
16
+ dashboard: false,
17
+ logging: { level: 'info', format: 'text' }
18
+ },
19
+ medium: {
20
+ level: 'balanced',
21
+ headers: true,
22
+ rateLimit: { max: 200, windowMs: 15 * 60 * 1000 },
23
+ cors: { origin: 'same-origin' }, // user overrides this typically
24
+ csrf: false,
25
+ botDetection: { enabled: true, action: 'block', blockList: ['scrapy', 'python-requests'] },
26
+ behavioral: { enabled: true },
27
+ fileUpload: { enabled: true },
28
+ dashboard: false,
29
+ logging: { level: 'warn', format: 'json' }
30
+ },
31
+ hard: {
32
+ level: 'strict',
33
+ headers: true,
34
+ rateLimit: { max: 100, windowMs: 15 * 60 * 1000 },
35
+ cors: { origin: 'same-origin' },
36
+ csrf: { cookieName: '_fortify_csrf' },
37
+ botDetection: { enabled: true, action: 'block' },
38
+ behavioral: { enabled: true, learningRequests: 5000 },
39
+ adaptive: { enabled: true },
40
+ fileUpload: { enabled: true },
41
+ dashboard: { enabled: false }, // available but off by default
42
+ logging: { level: 'warn', format: 'json' }
43
+ },
44
+ advanced: {
45
+ level: 'strict',
46
+ headers: true,
47
+ rateLimit: { max: 100, windowMs: 15 * 60 * 1000 },
48
+ cors: { origin: 'same-origin' },
49
+ csrf: { cookieName: '_fortify_csrf' },
50
+ botDetection: { enabled: true, action: 'block' },
51
+ behavioral: { enabled: true, learningRequests: 5000 },
52
+ adaptive: { enabled: true },
53
+ fileUpload: { enabled: true, scanFilenameForInjection: true },
54
+ dashboard: { enabled: true, path: '/admin/security' },
55
+ logging: { level: 'debug', format: 'json' }
56
+ }
57
+ };
58
+
59
+ function isObject(item) {
60
+ return (item && typeof item === 'object' && !Array.isArray(item));
61
+ }
62
+
63
+ function deepMerge(target, source) {
64
+ let output = Object.assign({}, target);
65
+ if (isObject(target) && isObject(source)) {
66
+ Object.keys(source).forEach(key => {
67
+ if (isObject(source[key])) {
68
+ if (!(key in target)) Object.assign(output, { [key]: source[key] });
69
+ else output[key] = deepMerge(target[key], source[key]);
70
+ } else {
71
+ Object.assign(output, { [key]: source[key] });
72
+ }
73
+ });
74
+ }
75
+ return output;
76
+ }
77
+
78
+ /**
79
+ * Main factory for fortifyjs middleware
80
+ * @param {string|Object} tier
81
+ * @param {Object} overrides
82
+ * @returns {Function} Express middleware stack
83
+ */
84
+ function shield(tier = 'basic', overrides = {}) {
85
+ if (typeof tier === 'object') {
86
+ overrides = tier;
87
+ tier = 'basic';
88
+ }
89
+
90
+ const preset = PRESETS[tier];
91
+ if (!preset) {
92
+ throw new Error(`Unknown tier: "${tier}". Use: basic, medium, hard, advanced`);
93
+ }
94
+
95
+ const config = deepMerge(preset, overrides);
96
+
97
+ // Create shared logger
98
+ const loggerOptions = typeof config.logging === 'object' ? config.logging : {};
99
+ const logger = new Logger(loggerOptions);
100
+
101
+ // Load shields (lazy load to avoid circular deps if needed)
102
+ const { rateLimiterFactory } = require('./shields/rate-limiter');
103
+ const headersFactory = require('./shields/headers');
104
+ const corsFactory = require('./shields/cors');
105
+ const csrfFactory = require('./shields/csrf');
106
+ const botFactory = require('./shields/bot-detector');
107
+ const { fileUploadShieldFactory } = require('./shields/file-upload');
108
+ const dashboardFactory = require('./dashboard/handler').createDashboardHandler;
109
+ const { AdaptiveBlocker } = require('./analyzers/adaptive');
110
+
111
+ // Build the stack
112
+ const stack = [];
113
+
114
+ if (config.rateLimit) {
115
+ stack.push(rateLimiterFactory(typeof config.rateLimit === 'object' ? config.rateLimit : {}));
116
+ }
117
+
118
+ if (config.headers) {
119
+ stack.push(headersFactory(typeof config.headers === 'object' ? config.headers : {}));
120
+ }
121
+
122
+ if (config.cors) {
123
+ stack.push(corsFactory(typeof config.cors === 'object' ? config.cors : {}));
124
+ }
125
+
126
+ if (config.csrf) {
127
+ stack.push(csrfFactory(typeof config.csrf === 'object' ? config.csrf : {}));
128
+ }
129
+
130
+ if (config.botDetection && config.botDetection.enabled) {
131
+ stack.push(botFactory(config.botDetection));
132
+ }
133
+
134
+ if (config.adaptive && config.adaptive.enabled) {
135
+ const adaptiveBlocker = new AdaptiveBlocker(typeof config.adaptive === 'object' ? config.adaptive : {});
136
+ stack.push(adaptiveBlocker.middleware());
137
+ }
138
+
139
+ if (config.fileUpload && config.fileUpload.enabled) {
140
+ stack.push(fileUploadShieldFactory(config.fileUpload));
141
+ }
142
+
143
+ if (config.dashboard && config.dashboard.enabled) {
144
+ stack.push(dashboardFactory(config.dashboard, logger));
145
+ }
146
+
147
+ // Build detector
148
+ const detectorOptions = {
149
+ level: config.level,
150
+ behavioral: config.behavioral
151
+ };
152
+
153
+ // Note: We use expressMiddleware factory here
154
+ // For other frameworks, this `shield()` factory would need to return the respective adapter's composition
155
+ // To keep it simple for v1, `shield()` returns Express middleware, and Fastify users use `fastifyPlugin` directly
156
+
157
+ const detectorMiddleware = expressMiddleware({
158
+ ...detectorOptions,
159
+ logAttacks: (msg, evt) => logger.warn(msg, { meta: evt })
160
+ });
161
+
162
+ stack.push(detectorMiddleware);
163
+
164
+ return function fortifyjsStack(req, res, next) {
165
+ let index = 0;
166
+ function runNext(err) {
167
+ if (err) return next(err);
168
+ if (index >= stack.length) return next();
169
+ const middleware = stack[index++];
170
+ try {
171
+ const result = middleware(req, res, runNext);
172
+ if (result && typeof result.catch === 'function') {
173
+ result.catch(next);
174
+ }
175
+ } catch (e) {
176
+ next(e);
177
+ }
178
+ }
179
+ runNext();
180
+ };
181
+ }
182
+
183
+ module.exports = { shield, PRESETS };
@@ -0,0 +1,88 @@
1
+ 'use strict';
2
+
3
+ const KNOWN_GOOD_BOTS = [
4
+ 'googlebot',
5
+ 'bingbot',
6
+ 'slurp',
7
+ 'duckduckbot',
8
+ 'baiduspider'
9
+ ];
10
+
11
+ const KNOWN_BAD_BOTS = [
12
+ 'scrapy',
13
+ 'python-requests',
14
+ 'go-http-client',
15
+ 'java/',
16
+ 'libwww-perl',
17
+ 'wget',
18
+ 'curl'
19
+ ];
20
+
21
+ /**
22
+ * Creates the bot detection shield middleware
23
+ * @param {Object} options - Shield configuration
24
+ * @returns {Function} middleware(req, res, next)
25
+ */
26
+ module.exports = function createBotDetectorShield(options = {}) {
27
+ const enabled = options.enabled !== false;
28
+ const allowSearchEngines = options.allowSearchEngines !== false;
29
+ const blockList = options.blockList || KNOWN_BAD_BOTS;
30
+ const allowList = options.allowList || [];
31
+ const action = options.action || 'block'; // 'block' or 'flag'
32
+
33
+ return function botDetectorShield(req, res, next) {
34
+ if (!enabled) return next();
35
+
36
+ const ua = req.headers['user-agent'] || '';
37
+ const uaLower = ua.toLowerCase();
38
+
39
+ let isBot = false;
40
+ let botReason = '';
41
+
42
+ // 1. Check allow list
43
+ if (ua && allowList.some(b => uaLower.includes(b.toLowerCase()))) {
44
+ return next();
45
+ }
46
+
47
+ // 2. Check search engines
48
+ if (ua && allowSearchEngines && KNOWN_GOOD_BOTS.some(b => uaLower.includes(b))) {
49
+ return next();
50
+ }
51
+
52
+ // 3. Empty User-Agent
53
+ if (!ua || ua.trim() === '') {
54
+ isBot = true;
55
+ botReason = 'empty-user-agent';
56
+ }
57
+ // 4. Check blocklist
58
+ else if (blockList.some(b => uaLower.includes(b.toLowerCase()))) {
59
+ isBot = true;
60
+ botReason = 'known-bad-bot';
61
+ }
62
+ // 5. Headless browsers
63
+ else if (uaLower.includes('headless') || uaLower.includes('phantomjs') || uaLower.includes('puppeteer')) {
64
+ isBot = true;
65
+ botReason = 'headless-browser';
66
+ }
67
+ // 6. Missing typical browser headers combined with suspicious UA
68
+ else if (!req.headers['accept'] && !req.headers['accept-language'] && !req.headers['accept-encoding']) {
69
+ isBot = true;
70
+ botReason = 'missing-browser-headers';
71
+ }
72
+
73
+ if (isBot) {
74
+ req.isBot = true;
75
+ req.botReason = botReason;
76
+
77
+ if (action === 'block') {
78
+ const err = new Error('Bot traffic detected');
79
+ err.status = 403;
80
+ err.code = 'EBOTBLOCKED';
81
+ err.reason = botReason;
82
+ return next(err);
83
+ }
84
+ }
85
+
86
+ next();
87
+ };
88
+ };
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Creates the CORS engine shield middleware
5
+ * @param {Object} options - Shield configuration
6
+ * @returns {Function} middleware(req, res, next)
7
+ */
8
+ module.exports = function createCorsShield(options = {}) {
9
+ const allowOrigin = options.origin || '*';
10
+ const allowMethods = options.methods || 'GET,HEAD,PUT,PATCH,POST,DELETE';
11
+ const allowedHeaders = options.allowedHeaders;
12
+ const exposedHeaders = options.exposedHeaders;
13
+ const credentials = options.credentials || false;
14
+ const maxAge = options.maxAge;
15
+
16
+ if (credentials === true && allowOrigin === '*') {
17
+ throw new Error('CORS: Cannot use credentials=true with origin="*"');
18
+ }
19
+
20
+ function handleOrigin(reqOrigin, originConfig, cb) {
21
+ if (typeof originConfig === 'function') {
22
+ originConfig(reqOrigin, cb);
23
+ } else if (Array.isArray(originConfig)) {
24
+ cb(null, originConfig.includes(reqOrigin) ? reqOrigin : false);
25
+ } else if (originConfig === '*') {
26
+ cb(null, '*');
27
+ } else if (typeof originConfig === 'string') {
28
+ cb(null, originConfig === reqOrigin ? reqOrigin : false);
29
+ } else if (originConfig instanceof RegExp) {
30
+ cb(null, originConfig.test(reqOrigin) ? reqOrigin : false);
31
+ } else {
32
+ cb(null, false);
33
+ }
34
+ }
35
+
36
+ return function corsShield(req, res, next) {
37
+ const origin = req.headers.origin;
38
+
39
+ // Set Vary header for dynamic origin
40
+ if (allowOrigin !== '*') {
41
+ res.setHeader('Vary', 'Origin');
42
+ }
43
+
44
+ if (!origin) {
45
+ return next(); // Not a CORS request
46
+ }
47
+
48
+ handleOrigin(origin, allowOrigin, (err, matchedOrigin) => {
49
+ if (err) return next(err);
50
+
51
+ if (!matchedOrigin) {
52
+ if (req.method === 'OPTIONS') {
53
+ res.statusCode = 204;
54
+ return res.end();
55
+ }
56
+ return next();
57
+ }
58
+
59
+ res.setHeader('Access-Control-Allow-Origin', matchedOrigin);
60
+
61
+ if (credentials) {
62
+ res.setHeader('Access-Control-Allow-Credentials', 'true');
63
+ }
64
+
65
+ if (exposedHeaders) {
66
+ const exposed = Array.isArray(exposedHeaders) ? exposedHeaders.join(',') : exposedHeaders;
67
+ res.setHeader('Access-Control-Expose-Headers', exposed);
68
+ }
69
+
70
+ if (req.method === 'OPTIONS') {
71
+ res.setHeader('Access-Control-Allow-Methods', Array.isArray(allowMethods) ? allowMethods.join(',') : allowMethods);
72
+
73
+ let requestHeaders = allowedHeaders;
74
+ if (!requestHeaders && req.headers['access-control-request-headers']) {
75
+ requestHeaders = req.headers['access-control-request-headers'];
76
+ }
77
+
78
+ if (requestHeaders) {
79
+ const headersStr = Array.isArray(requestHeaders) ? requestHeaders.join(',') : requestHeaders;
80
+ res.setHeader('Access-Control-Allow-Headers', headersStr);
81
+ }
82
+
83
+ if (maxAge !== undefined) {
84
+ res.setHeader('Access-Control-Max-Age', maxAge.toString());
85
+ }
86
+
87
+ res.statusCode = 204;
88
+ res.setHeader('Content-Length', '0');
89
+ return res.end();
90
+ }
91
+
92
+ next();
93
+ });
94
+ };
95
+ };
@@ -0,0 +1,120 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ /**
6
+ * Creates the CSRF protection shield middleware
7
+ * @param {Object} options - Shield configuration
8
+ * @returns {Function} middleware(req, res, next)
9
+ */
10
+ module.exports = function createCsrfShield(options = {}) {
11
+ const secret = options.secret || crypto.randomBytes(32).toString('hex');
12
+ if (!options.secret) {
13
+ console.warn('fortifyjs: CSRF secret not provided. A random secret was generated, but it will not persist across restarts.');
14
+ }
15
+
16
+ const cookieName = options.cookieName || '_fortify_csrf';
17
+ const headerName = options.headerName || 'x-csrf-token';
18
+ const bodyField = options.bodyField || '_csrf';
19
+ const sameSite = options.sameSite || 'Lax';
20
+ const secure = options.secure !== undefined ? options.secure : process.env.NODE_ENV === 'production';
21
+ const ignoreMethods = options.ignoreMethods || ['GET', 'HEAD', 'OPTIONS'];
22
+ const ignoreRoutes = options.ignoreRoutes || [];
23
+
24
+ function signToken(token) {
25
+ const hmac = crypto.createHmac('sha256', secret);
26
+ hmac.update(token);
27
+ return `${token}.${hmac.digest('hex')}`;
28
+ }
29
+
30
+ function verifyToken(signedToken) {
31
+ if (!signedToken || typeof signedToken !== 'string') return false;
32
+ const parts = signedToken.split('.');
33
+ if (parts.length !== 2) return false;
34
+ const [token, signature] = parts;
35
+ const expectedSigned = signToken(token);
36
+ if (expectedSigned.length !== signedToken.length) return false;
37
+ return crypto.timingSafeEqual(Buffer.from(expectedSigned), Buffer.from(signedToken));
38
+ }
39
+
40
+ function matchRoute(path, patterns) {
41
+ for (const pattern of patterns) {
42
+ if (pattern.endsWith('/*')) {
43
+ const prefix = pattern.slice(0, -2);
44
+ if (path.startsWith(prefix)) return true;
45
+ } else if (path === pattern) {
46
+ return true;
47
+ }
48
+ }
49
+ return false;
50
+ }
51
+
52
+ return function csrfShield(req, res, next) {
53
+ // Basic route ignore (e.g. webhooks)
54
+ if (ignoreRoutes.length > 0 && req.path && matchRoute(req.path, ignoreRoutes)) {
55
+ return next();
56
+ }
57
+
58
+ // Parse cookies if not already parsed
59
+ // We assume cookie-parser might not be present, so we do basic parsing
60
+ let cookies = req.cookies || {};
61
+ if (!req.cookies && req.headers.cookie) {
62
+ cookies = Object.fromEntries(
63
+ req.headers.cookie.split('; ').map(c => c.split('='))
64
+ );
65
+ }
66
+
67
+ const currentSignedToken = cookies[cookieName];
68
+
69
+ // Expose csrfToken generator for views
70
+ req.csrfToken = function() {
71
+ const newToken = crypto.randomBytes(16).toString('hex');
72
+ const newSignedToken = signToken(newToken);
73
+
74
+ let cookieHeader = `${cookieName}=${newSignedToken}; Path=/; HttpOnly; SameSite=${sameSite}`;
75
+ if (secure) cookieHeader += '; Secure';
76
+
77
+ res.setHeader('Set-Cookie', cookieHeader);
78
+ return newSignedToken;
79
+ };
80
+
81
+ // If method is safe, we don't enforce, just optionally generate/refresh token if missing
82
+ if (ignoreMethods.includes(req.method)) {
83
+ if (!currentSignedToken) {
84
+ req.csrfToken(); // trigger generation
85
+ }
86
+ return next();
87
+ }
88
+
89
+ // Mutation method: Enforce token
90
+ let incomingToken = null;
91
+
92
+ // Check header
93
+ if (req.headers[headerName.toLowerCase()]) {
94
+ incomingToken = req.headers[headerName.toLowerCase()];
95
+ } else if (req.headers['x-xsrf-token']) {
96
+ incomingToken = req.headers['x-xsrf-token'];
97
+ }
98
+
99
+ // Check body
100
+ if (!incomingToken && req.body && typeof req.body === 'object') {
101
+ incomingToken = req.body[bodyField];
102
+ }
103
+
104
+ if (!incomingToken) {
105
+ const err = new Error('CSRF token missing');
106
+ err.status = 403;
107
+ err.code = 'EBADCSRFTOKEN';
108
+ return next(err);
109
+ }
110
+
111
+ if (!currentSignedToken || currentSignedToken !== incomingToken || !verifyToken(incomingToken)) {
112
+ const err = new Error('CSRF token invalid');
113
+ err.status = 403;
114
+ err.code = 'EBADCSRFTOKEN';
115
+ return next(err);
116
+ }
117
+
118
+ next();
119
+ };
120
+ };
@@ -0,0 +1,160 @@
1
+ 'use strict';
2
+
3
+ const { DetectionEngine } = require('../core/engine');
4
+
5
+ const defaultOptions = {
6
+ enabled: true,
7
+ allowedExtensions: ['.jpg', '.jpeg', '.png', '.gif', '.pdf', '.docx'],
8
+ blockedExtensions: ['.php', '.asp', '.aspx', '.jsp', '.exe', '.sh', '.bat', '.cmd'],
9
+ maxFilenameLength: 255,
10
+ blockDoubleExtensions: true,
11
+ blockNullBytes: true,
12
+ blockPathTraversal: true,
13
+ blockDotFiles: true,
14
+ validateMimeType: true,
15
+ scanFilenameForInjection: true,
16
+ };
17
+
18
+ const mimeMap = {
19
+ '.jpg': 'image/jpeg',
20
+ '.jpeg': 'image/jpeg',
21
+ '.png': 'image/png',
22
+ '.gif': 'image/gif',
23
+ '.pdf': 'application/pdf',
24
+ '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
25
+ '.doc': 'application/msword',
26
+ '.xls': 'application/vnd.ms-excel',
27
+ '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
28
+ '.txt': 'text/plain',
29
+ '.csv': 'text/csv',
30
+ '.json': 'application/json',
31
+ '.xml': 'application/xml',
32
+ '.zip': 'application/zip',
33
+ '.tar': 'application/x-tar',
34
+ '.gz': 'application/gzip',
35
+ '.mp4': 'video/mp4',
36
+ '.mp3': 'audio/mpeg'
37
+ };
38
+
39
+ function fileUploadShieldFactory(options = {}) {
40
+ const config = Object.assign({}, defaultOptions, options);
41
+
42
+ if (!config.enabled) {
43
+ return (req, res, next) => next();
44
+ }
45
+
46
+ let engine = null;
47
+ if (config.scanFilenameForInjection) {
48
+ engine = new DetectionEngine();
49
+ }
50
+
51
+ function block(res, next, reason) {
52
+ res.status(403).json({ error: 'File upload blocked', reason });
53
+ }
54
+
55
+ function validateFile(file, res, next) {
56
+ if (!file) return true;
57
+
58
+ // multer-style file has originalname, some others might have name or filename
59
+ const filename = file.originalname || file.name || file.filename || (typeof file === 'string' ? file : '');
60
+
61
+ if (!filename) return true; // Could not determine filename, skip or block? We skip if we can't tell.
62
+
63
+ if (config.maxFilenameLength && filename.length > config.maxFilenameLength) {
64
+ block(res, next, 'Filename too long');
65
+ return false;
66
+ }
67
+
68
+ if (config.blockNullBytes && filename.indexOf('\0') !== -1) {
69
+ block(res, next, 'Null byte detected in filename');
70
+ return false;
71
+ }
72
+
73
+ if (config.blockPathTraversal && (filename.includes('../') || filename.includes('..\\') || filename.includes('/') || filename.includes('\\'))) {
74
+ block(res, next, 'Path traversal detected in filename');
75
+ return false;
76
+ }
77
+
78
+ if (config.blockDotFiles && filename.startsWith('.')) {
79
+ block(res, next, 'Dot files are not allowed');
80
+ return false;
81
+ }
82
+
83
+ const parts = filename.split('.');
84
+ const ext = parts.length > 1 ? '.' + parts[parts.length - 1].toLowerCase() : '';
85
+
86
+ if (config.blockDoubleExtensions && parts.length > 2) {
87
+ block(res, next, 'Double extensions are not allowed');
88
+ return false;
89
+ }
90
+
91
+ if (config.blockedExtensions && config.blockedExtensions.length > 0) {
92
+ if (config.blockedExtensions.includes(ext)) {
93
+ block(res, next, 'File extension is blocked');
94
+ return false;
95
+ }
96
+ }
97
+
98
+ if (config.allowedExtensions && config.allowedExtensions.length > 0) {
99
+ if (!config.allowedExtensions.includes(ext)) {
100
+ block(res, next, 'File extension is not allowed');
101
+ return false;
102
+ }
103
+ }
104
+
105
+ if (config.validateMimeType && file.mimetype && ext) {
106
+ const expectedMime = mimeMap[ext];
107
+ if (expectedMime && !file.mimetype.toLowerCase().startsWith(expectedMime.split('/')[0])) {
108
+ // Relaxed checking, e.g. application/pdf could be exact, but just compare what we have roughly
109
+ // Or strict comparison:
110
+ if (file.mimetype.toLowerCase() !== expectedMime) {
111
+ block(res, next, 'MIME type does not match file extension');
112
+ return false;
113
+ }
114
+ }
115
+ }
116
+
117
+ if (config.scanFilenameForInjection && engine) {
118
+ const result = engine.detect(filename, { source: 'filename' });
119
+ if (result.label && result.label !== 'benign') {
120
+ block(res, next, 'Filename contains malicious payload');
121
+ return false;
122
+ }
123
+ }
124
+
125
+ return true;
126
+ }
127
+
128
+ return function fileUploadShield(req, res, next) {
129
+ let filesToScan = [];
130
+
131
+ if (req.file) {
132
+ filesToScan.push(req.file);
133
+ }
134
+
135
+ if (req.files) {
136
+ if (Array.isArray(req.files)) {
137
+ filesToScan = filesToScan.concat(req.files);
138
+ } else if (typeof req.files === 'object') {
139
+ Object.keys(req.files).forEach(key => {
140
+ const field = req.files[key];
141
+ if (Array.isArray(field)) {
142
+ filesToScan = filesToScan.concat(field);
143
+ } else {
144
+ filesToScan.push(field);
145
+ }
146
+ });
147
+ }
148
+ }
149
+
150
+ for (let i = 0; i < filesToScan.length; i++) {
151
+ if (!validateFile(filesToScan[i], res, next)) {
152
+ return; // Response already sent
153
+ }
154
+ }
155
+
156
+ next();
157
+ };
158
+ }
159
+
160
+ module.exports = { fileUploadShieldFactory };