@noony-serverless/core
Version:
A Middy base framework compatible with Firebase and GCP Cloud Functions with TypeScript
186 lines • 7.96 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SecurityPresets = exports.securityHeaders = exports.SecurityHeadersMiddleware = void 0;
const DEFAULT_OPTIONS = {
contentSecurityPolicy: "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';",
hstsMaxAge: 31536000, // 1 year
hstsIncludeSubDomains: true,
frameOptions: 'DENY',
contentTypeOptions: 'nosniff',
referrerPolicy: 'strict-origin-when-cross-origin',
permissionsPolicy: 'geolocation=(), microphone=(), camera=(), payment=(), usb=(), magnetometer=(), gyroscope=(), speaker=()',
crossOriginEmbedderPolicy: 'require-corp',
crossOriginOpenerPolicy: 'same-origin',
crossOriginResourcePolicy: 'same-origin',
removeServerHeader: true,
removePoweredBy: true,
};
/**
* Validates CORS origin against allowed patterns
*/
const isOriginAllowed = (origin, allowedOrigins) => {
if (allowedOrigins === true)
return true;
if (allowedOrigins === false)
return false;
if (typeof allowedOrigins === 'string')
return origin === allowedOrigins;
if (Array.isArray(allowedOrigins)) {
return allowedOrigins.some((allowed) => {
// Support wildcard patterns like *.example.com
if (allowed.includes('*')) {
const regex = new RegExp('^' + allowed.replace(/\*/g, '.*') + '$');
return regex.test(origin);
}
return origin === allowed;
});
}
return false;
};
/**
* Security Headers Middleware
* Implements comprehensive security headers following OWASP recommendations
*
* @template TBody - The type of the request body payload (preserves type chain)
* @template TUser - The type of the authenticated user (preserves type chain)
*/
class SecurityHeadersMiddleware {
options;
constructor(options = {}) {
this.options = { ...DEFAULT_OPTIONS, ...options };
}
async before(context) {
const headers = {};
// Content Security Policy
headers['Content-Security-Policy'] = this.options.contentSecurityPolicy;
// Strict Transport Security (HTTPS only)
const hstsValue = `max-age=${this.options.hstsMaxAge}${this.options.hstsIncludeSubDomains ? '; includeSubDomains' : ''}; preload`;
headers['Strict-Transport-Security'] = hstsValue;
// Frame Options
headers['X-Frame-Options'] = this.options.frameOptions;
// Content Type Options
headers['X-Content-Type-Options'] = this.options.contentTypeOptions;
// Referrer Policy
headers['Referrer-Policy'] = this.options.referrerPolicy;
// Permissions Policy
headers['Permissions-Policy'] = this.options.permissionsPolicy;
// Cross-Origin Policies
headers['Cross-Origin-Embedder-Policy'] =
this.options.crossOriginEmbedderPolicy;
headers['Cross-Origin-Opener-Policy'] =
this.options.crossOriginOpenerPolicy;
headers['Cross-Origin-Resource-Policy'] =
this.options.crossOriginResourcePolicy;
// Remove identifying headers
if (this.options.removeServerHeader) {
delete headers['Server'];
}
if (this.options.removePoweredBy) {
delete headers['X-Powered-By'];
}
// CORS headers
if (this.options.cors) {
const originHeader = context.req.headers?.['origin'];
const origin = Array.isArray(originHeader)
? originHeader[0]
: originHeader || '';
const requestMethod = context.req.headers?.['access-control-request-method'];
const requestHeaders = context.req.headers?.['access-control-request-headers'];
// Handle preflight requests
if (context.req.method === 'OPTIONS' &&
(requestMethod || requestHeaders)) {
if (this.options.cors.origin &&
isOriginAllowed(origin, this.options.cors.origin)) {
headers['Access-Control-Allow-Origin'] = origin;
}
if (this.options.cors.methods) {
headers['Access-Control-Allow-Methods'] =
this.options.cors.methods.join(', ');
}
if (this.options.cors.allowedHeaders) {
headers['Access-Control-Allow-Headers'] =
this.options.cors.allowedHeaders.join(', ');
}
if (this.options.cors.maxAge !== undefined) {
headers['Access-Control-Max-Age'] = String(this.options.cors.maxAge);
}
if (this.options.cors.credentials) {
headers['Access-Control-Allow-Credentials'] = 'true';
}
// Apply headers and return early for preflight
Object.entries(headers).forEach(([key, value]) => {
if (value !== undefined) {
context.res.header(key, value);
}
});
context.res.status(204).json({});
return;
}
// Handle actual requests
if (this.options.cors.origin &&
isOriginAllowed(origin, this.options.cors.origin)) {
headers['Access-Control-Allow-Origin'] = origin;
}
if (this.options.cors.exposedHeaders) {
headers['Access-Control-Expose-Headers'] =
this.options.cors.exposedHeaders.join(', ');
}
if (this.options.cors.credentials) {
headers['Access-Control-Allow-Credentials'] = 'true';
}
}
// Apply headers to response
Object.entries(headers).forEach(([key, value]) => {
if (value !== undefined) {
context.res.header(key, value);
}
});
}
}
exports.SecurityHeadersMiddleware = SecurityHeadersMiddleware;
/**
* Security Headers Middleware Factory
* @param options Security headers configuration
* @returns BaseMiddleware
*/
const securityHeaders = (options = {}) => new SecurityHeadersMiddleware(options);
exports.securityHeaders = securityHeaders;
/**
* Predefined security configurations
*/
exports.SecurityPresets = {
/**
* Strict security configuration for high-security applications
*/
STRICT: {
contentSecurityPolicy: "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self';",
hstsMaxAge: 63072000, // 2 years
frameOptions: 'DENY',
crossOriginEmbedderPolicy: 'require-corp',
crossOriginOpenerPolicy: 'same-origin',
crossOriginResourcePolicy: 'same-origin',
},
/**
* Balanced security configuration for most applications
*/
BALANCED: {
contentSecurityPolicy: "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-ancestors 'none';",
hstsMaxAge: 31536000, // 1 year
frameOptions: 'SAMEORIGIN',
},
/**
* Permissive security configuration for development
*/
DEVELOPMENT: {
contentSecurityPolicy: "default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data: https:; font-src 'self'; connect-src 'self' ws: wss:;",
hstsMaxAge: 0,
frameOptions: 'SAMEORIGIN',
cors: {
origin: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
},
},
};
//# sourceMappingURL=securityHeadersMiddleware.js.map