@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.
- package/LICENSE +9 -0
- package/README.md +186 -0
- package/bin/fortifyjs.js +135 -0
- package/examples/fastify.js +18 -0
- package/examples/hono.js +13 -0
- package/examples/kitchen-sink.js +50 -0
- package/examples/koa.js +14 -0
- package/examples/minimal-express.js +13 -0
- package/examples/production-express.js +20 -0
- package/index.d.ts +101 -0
- package/package.json +70 -0
- package/src/adapters/express.js +1027 -0
- package/src/adapters/fastify.js +56 -0
- package/src/adapters/generic.js +15 -0
- package/src/adapters/hono.js +77 -0
- package/src/adapters/koa.js +58 -0
- package/src/adapters/nestjs.js +15 -0
- package/src/adapters/nextjs.js +84 -0
- package/src/analyzers/adaptive.js +92 -0
- package/src/analyzers/behavioral.js +264 -0
- package/src/core/confidence.js +23 -0
- package/src/core/engine.js +162 -0
- package/src/core/normalizer.js +149 -0
- package/src/core/whitelist.js +40 -0
- package/src/dashboard/handler.js +307 -0
- package/src/detectors/cmdi.js +82 -0
- package/src/detectors/crlf.js +33 -0
- package/src/detectors/graphql.js +56 -0
- package/src/detectors/hpp.js +38 -0
- package/src/detectors/ldap.js +50 -0
- package/src/detectors/nosqli.js +134 -0
- package/src/detectors/open-redirect.js +42 -0
- package/src/detectors/path-traversal.js +55 -0
- package/src/detectors/prototype-pollution.js +65 -0
- package/src/detectors/sqli.js +447 -0
- package/src/detectors/sqli.js.bak +446 -0
- package/src/detectors/ssrf.js +64 -0
- package/src/detectors/template-injection.js +34 -0
- package/src/detectors/xss.js +191 -0
- package/src/detectors/xxe.js +45 -0
- package/src/forensics/reporter.js +74 -0
- package/src/index.js +132 -0
- package/src/logger.js +110 -0
- package/src/presets.js +183 -0
- package/src/shields/bot-detector.js +88 -0
- package/src/shields/cors.js +95 -0
- package/src/shields/csrf.js +120 -0
- package/src/shields/file-upload.js +160 -0
- package/src/shields/headers.js +99 -0
- package/src/shields/rate-limiter.js +70 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEFAULT_OPTIONS = {
|
|
4
|
+
contentSecurityPolicy: {
|
|
5
|
+
directives: {
|
|
6
|
+
defaultSrc: ["'self'"]
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
xContentTypeOptions: 'nosniff',
|
|
10
|
+
xFrameOptions: 'DENY',
|
|
11
|
+
xXssProtection: '0',
|
|
12
|
+
hsts: { maxAge: 15552000, includeSubDomains: true },
|
|
13
|
+
referrerPolicy: 'no-referrer',
|
|
14
|
+
permissionsPolicy: 'camera=(), microphone=(), geolocation=()',
|
|
15
|
+
xDnsPrefetchControl: 'off',
|
|
16
|
+
xPermittedCrossDomainPolicies: 'none',
|
|
17
|
+
crossOriginOpenerPolicy: 'same-origin',
|
|
18
|
+
crossOriginResourcePolicy: 'same-origin',
|
|
19
|
+
crossOriginEmbedderPolicy: 'require-corp'
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function buildCspString(directives) {
|
|
23
|
+
if (!directives || typeof directives !== 'object') return '';
|
|
24
|
+
const parts = [];
|
|
25
|
+
for (const [key, value] of Object.entries(directives)) {
|
|
26
|
+
const directiveName = key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
|
|
27
|
+
const directiveValue = Array.isArray(value) ? value.join(' ') : value;
|
|
28
|
+
parts.push(`${directiveName} ${directiveValue}`);
|
|
29
|
+
}
|
|
30
|
+
return parts.join('; ');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildHstsString(options) {
|
|
34
|
+
if (typeof options === 'string') return options;
|
|
35
|
+
let str = `max-age=${options.maxAge || 15552000}`;
|
|
36
|
+
if (options.includeSubDomains) str += '; includeSubDomains';
|
|
37
|
+
if (options.preload) str += '; preload';
|
|
38
|
+
return str;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Creates the Security Headers shield middleware
|
|
43
|
+
* @param {Object} options - Shield configuration
|
|
44
|
+
* @returns {Function} middleware(req, res, next)
|
|
45
|
+
*/
|
|
46
|
+
module.exports = function createHeadersShield(options = {}) {
|
|
47
|
+
const config = { ...DEFAULT_OPTIONS, ...options };
|
|
48
|
+
|
|
49
|
+
let cspString = '';
|
|
50
|
+
if (config.contentSecurityPolicy !== false) {
|
|
51
|
+
cspString = buildCspString(config.contentSecurityPolicy?.directives || config.contentSecurityPolicy);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let hstsString = '';
|
|
55
|
+
if (config.hsts !== false) {
|
|
56
|
+
hstsString = buildHstsString(config.hsts);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return function headersShield(req, res, next) {
|
|
60
|
+
if (config.contentSecurityPolicy !== false && cspString) {
|
|
61
|
+
res.setHeader('Content-Security-Policy', cspString);
|
|
62
|
+
}
|
|
63
|
+
if (config.xContentTypeOptions !== false) {
|
|
64
|
+
res.setHeader('X-Content-Type-Options', config.xContentTypeOptions || DEFAULT_OPTIONS.xContentTypeOptions);
|
|
65
|
+
}
|
|
66
|
+
if (config.xFrameOptions !== false) {
|
|
67
|
+
res.setHeader('X-Frame-Options', config.frameguard || config.xFrameOptions || DEFAULT_OPTIONS.xFrameOptions);
|
|
68
|
+
}
|
|
69
|
+
if (config.xXssProtection !== false) {
|
|
70
|
+
res.setHeader('X-XSS-Protection', config.xXssProtection || DEFAULT_OPTIONS.xXssProtection);
|
|
71
|
+
}
|
|
72
|
+
if (config.hsts !== false && hstsString) {
|
|
73
|
+
res.setHeader('Strict-Transport-Security', hstsString);
|
|
74
|
+
}
|
|
75
|
+
if (config.referrerPolicy !== false) {
|
|
76
|
+
res.setHeader('Referrer-Policy', config.referrerPolicy || DEFAULT_OPTIONS.referrerPolicy);
|
|
77
|
+
}
|
|
78
|
+
if (config.permissionsPolicy !== false) {
|
|
79
|
+
res.setHeader('Permissions-Policy', config.permissionsPolicy || DEFAULT_OPTIONS.permissionsPolicy);
|
|
80
|
+
}
|
|
81
|
+
if (config.xDnsPrefetchControl !== false) {
|
|
82
|
+
res.setHeader('X-DNS-Prefetch-Control', config.xDnsPrefetchControl || DEFAULT_OPTIONS.xDnsPrefetchControl);
|
|
83
|
+
}
|
|
84
|
+
if (config.xPermittedCrossDomainPolicies !== false) {
|
|
85
|
+
res.setHeader('X-Permitted-Cross-Domain-Policies', config.xPermittedCrossDomainPolicies || DEFAULT_OPTIONS.xPermittedCrossDomainPolicies);
|
|
86
|
+
}
|
|
87
|
+
if (config.crossOriginOpenerPolicy !== false) {
|
|
88
|
+
res.setHeader('Cross-Origin-Opener-Policy', config.crossOriginOpenerPolicy || DEFAULT_OPTIONS.crossOriginOpenerPolicy);
|
|
89
|
+
}
|
|
90
|
+
if (config.crossOriginResourcePolicy !== false) {
|
|
91
|
+
res.setHeader('Cross-Origin-Resource-Policy', config.crossOriginResourcePolicy || DEFAULT_OPTIONS.crossOriginResourcePolicy);
|
|
92
|
+
}
|
|
93
|
+
if (config.crossOriginEmbedderPolicy !== false) {
|
|
94
|
+
res.setHeader('Cross-Origin-Embedder-Policy', config.crossOriginEmbedderPolicy || DEFAULT_OPTIONS.crossOriginEmbedderPolicy);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
next();
|
|
98
|
+
};
|
|
99
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
class IPRateLimiter {
|
|
3
|
+
constructor(windowMs = 300000, maxCapacity = 10000, maxEventsPerKey = 1000) {
|
|
4
|
+
this.windowMs = Number.isFinite(windowMs) && windowMs > 0 ? windowMs : 300000;
|
|
5
|
+
this.maxCapacity = Number.isFinite(maxCapacity) && maxCapacity > 0 ? maxCapacity : 10000;
|
|
6
|
+
this.maxEventsPerKey = Number.isFinite(maxEventsPerKey) && maxEventsPerKey > 0 ? maxEventsPerKey : 1000;
|
|
7
|
+
this.ips = new Map();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
pruneExpired(now) {
|
|
11
|
+
for (const [ip, timestamps] of this.ips) {
|
|
12
|
+
const validTimestamps = timestamps.filter(t => now - t < this.windowMs);
|
|
13
|
+
if (validTimestamps.length === 0) {
|
|
14
|
+
this.ips.delete(ip);
|
|
15
|
+
} else {
|
|
16
|
+
this.ips.set(ip, validTimestamps);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
recordSuspicious(ip) {
|
|
22
|
+
const now = Date.now();
|
|
23
|
+
if (!this.ips.has(ip)) {
|
|
24
|
+
if (this.ips.size >= this.maxCapacity) {
|
|
25
|
+
this.pruneExpired(now);
|
|
26
|
+
}
|
|
27
|
+
if (this.ips.size >= this.maxCapacity) {
|
|
28
|
+
this.ips.delete(this.ips.keys().next().value);
|
|
29
|
+
}
|
|
30
|
+
this.ips.set(ip, []);
|
|
31
|
+
}
|
|
32
|
+
const timestamps = this.ips.get(ip);
|
|
33
|
+
|
|
34
|
+
// Cleanup old timestamps for this IP
|
|
35
|
+
const validTimestamps = timestamps.filter(t => now - t < this.windowMs);
|
|
36
|
+
validTimestamps.push(now);
|
|
37
|
+
if (validTimestamps.length > this.maxEventsPerKey) {
|
|
38
|
+
validTimestamps.splice(0, validTimestamps.length - this.maxEventsPerKey);
|
|
39
|
+
}
|
|
40
|
+
this.ips.delete(ip);
|
|
41
|
+
this.ips.set(ip, validTimestamps);
|
|
42
|
+
return validTimestamps.length;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function rateLimiterFactory(options = {}) {
|
|
48
|
+
const windowMs = options.windowMs || 15 * 60 * 1000;
|
|
49
|
+
const max = options.max || 100;
|
|
50
|
+
const limiter = new IPRateLimiter(windowMs, 10000, max);
|
|
51
|
+
|
|
52
|
+
return function rateLimitMiddleware(req, res, next) {
|
|
53
|
+
const ip = req.ip || (req.connection && req.connection.remoteAddress) || 'unknown';
|
|
54
|
+
const currentHits = limiter.recordSuspicious(ip);
|
|
55
|
+
|
|
56
|
+
if (currentHits >= max) {
|
|
57
|
+
if (res.status && res.json) {
|
|
58
|
+
res.status(429).json({ error: 'Too many requests' });
|
|
59
|
+
} else {
|
|
60
|
+
// Fallback for non-Express adapters if needed
|
|
61
|
+
res.statusCode = 429;
|
|
62
|
+
res.end(JSON.stringify({ error: 'Too many requests' }));
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
next();
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { IPRateLimiter, rateLimiterFactory };
|