thrilled-be-core
Version:
Core Express backend package with middleware, logging, security, and base application setup
519 lines (518 loc) • 19.6 kB
JavaScript
"use strict";
/**
* Enhanced Security Plugin for Enterprise Applications
*
* This plugin provides comprehensive security hardening including:
* - Helmet.js integration with proper CSP
* - Dynamic CORS validation
* - Enhanced rate limiting per user
* - Advanced input sanitization
* - Security headers management
* - Suspicious request detection
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.EnhancedSecurityPlugin = void 0;
const tslib_1 = require("tslib");
const helmet_1 = tslib_1.__importDefault(require("helmet"));
const Plugin_1 = require("../plugins/Plugin");
// Function to import ES module from CommonJS context
const importESModule = async (specifier) => {
return new Function('specifier', 'return import(specifier)')(specifier);
};
/**
* Enhanced Security Plugin with comprehensive security hardening
*
* @example
* ```typescript
* // In your app setup
* import { EnhancedSecurityPlugin } from 'thrilled-be-core';
*
* const securityPlugin = new EnhancedSecurityPlugin({
* enableCSP: true,
* enableHSTS: process.env.NODE_ENV === 'production',
* allowedOrigins: ['https://myapp.com'],
* enableInputSanitization: true,
* });
*
* app.use(securityPlugin);
* ```
*/
class EnhancedSecurityPlugin extends Plugin_1.BasePlugin {
name = 'enhanced-security';
version = '1.0.0';
config;
allowedOrigins = new Set();
isProduction;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
validationModule;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
XSSProtection;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
SQLInjectionProtection;
constructor(config = {}) {
super(config.logger);
// Extract logger and create config without it
const { logger, ...securityConfig } = config;
this.config = {
enableCSP: true,
enableHSTS: true,
enableXSSProtection: true,
allowedOrigins: [],
enableDynamicCORS: true,
trustedProxies: [],
maxRequestSize: '10mb',
enableSecurityHeaders: true,
enableInputSanitization: true,
...securityConfig,
};
this.isProduction = process.env.NODE_ENV === 'production';
}
async setup() {
this.logger.info('Initializing enhanced security plugin...');
// Initialize allowed origins
this.initializeAllowedOrigins();
// Initialize input sanitization if enabled
if (this.config.enableInputSanitization) {
await this.initializeInputSanitization();
}
this.logger.info('Enhanced security plugin initialized', {
csp: this.config.enableCSP,
hsts: this.config.enableHSTS,
dynamicCORS: this.config.enableDynamicCORS,
securityHeaders: this.config.enableSecurityHeaders,
inputSanitization: this.config.enableInputSanitization,
environment: this.isProduction ? 'production' : 'development',
});
}
/**
* Initialize input sanitization modules
*/
async initializeInputSanitization() {
try {
// Try to dynamically import the validation module
this.validationModule = await importESModule('@thrilled/be-validation');
// Extract sanitization components
this.XSSProtection = this.validationModule.XSSProtection;
this.SQLInjectionProtection = this.validationModule.SQLInjectionProtection;
this.logger.info('Input sanitization modules initialized successfully');
}
catch (error) {
this.logger.warn('Failed to initialize input sanitization modules', { error });
// For testing purposes, create mock implementations
if (process.env.NODE_ENV === 'test') {
this.XSSProtection = {
middleware: (_options) => (_req, _res, next) => next()
};
this.SQLInjectionProtection = {
middleware: (_options) => (_req, _res, next) => next()
};
this.logger.info('Mock input sanitization modules initialized for testing');
return;
}
// Gracefully disable input sanitization if modules can't be loaded
this.config.enableInputSanitization = false;
}
}
registerMiddleware(app) {
// 1. Configure Helmet.js with comprehensive security headers
this.setupHelmetSecurity(app);
// 2. Setup input sanitization middleware
this.setupInputSanitization(app);
// 3. Setup dynamic CORS validation
this.setupDynamicCORS(app);
// 4. Setup enhanced request validation
this.setupRequestValidation(app);
this.logger.info('Enhanced security middleware registered successfully');
}
/**
* Register routes for this plugin
*/
registerRoutes(app) {
this.logger.debug(`Registering routes for plugin: ${this.name}`);
// Add security health endpoint
app.get('/health/security', (req, res) => {
res.json({
status: 'ok',
security: {
helmet: this.config.enableSecurityHeaders,
csp: this.config.enableCSP,
hsts: this.config.enableHSTS && this.isProduction,
dynamicCORS: this.config.enableDynamicCORS,
inputSanitization: this.config.enableInputSanitization && !!this.XSSProtection && !!this.SQLInjectionProtection,
allowedOrigins: this.allowedOrigins.size,
environment: this.isProduction ? 'production' : 'development',
},
timestamp: new Date().toISOString(),
});
});
this.logger.info('Security monitoring endpoints configured');
}
/**
* Setup input sanitization middleware
*/
setupInputSanitization(app) {
if (!this.config.enableInputSanitization || !this.XSSProtection || !this.SQLInjectionProtection) {
this.logger.info('Input sanitization disabled or modules not available');
return;
}
// XSS protection middleware
app.use(this.XSSProtection.middleware({
removeScriptTags: true,
encodeHtml: true,
allowSafeAttributes: [],
}));
// SQL injection protection middleware
app.use(this.SQLInjectionProtection.middleware({
escapeQuotes: true,
removeSqlKeywords: false, // Be conservative
}));
this.logger.info('Input sanitization middleware configured', {
xssProtection: true,
sqlInjectionProtection: true,
});
}
/**
* Setup Helmet.js with comprehensive security configuration
*/
setupHelmetSecurity(app) {
if (!this.config.enableSecurityHeaders) {
this.logger.info('Security headers disabled by configuration');
return;
}
// Content Security Policy configuration
const cspConfig = this.config.enableCSP ? {
directives: {
defaultSrc: ["'self'"],
styleSrc: [
"'self'",
"'unsafe-inline'", // Allow inline styles for development
'https://fonts.googleapis.com',
'https://cdnjs.cloudflare.com',
],
scriptSrc: [
"'self'",
...(this.isProduction ? [] : ["'unsafe-eval'"]), // Allow eval in development
],
fontSrc: [
"'self'",
'https://fonts.gstatic.com',
'data:',
],
imgSrc: [
"'self'",
'data:',
'https:',
],
connectSrc: [
"'self'",
...(this.isProduction ? [] : ['ws:', 'wss:']), // WebSocket for development
],
frameSrc: ["'none'"],
objectSrc: ["'none'"],
...(this.isProduction ? { upgradeInsecureRequests: [] } : {}),
},
reportOnly: !this.isProduction, // Report-only mode in development
} : false;
// HSTS configuration
const hstsConfig = this.config.enableHSTS && this.isProduction ? {
maxAge: 31536000, // 1 year
includeSubDomains: true,
preload: true,
} : false;
app.use((0, helmet_1.default)({
contentSecurityPolicy: cspConfig,
hsts: hstsConfig,
noSniff: true,
xssFilter: this.config.enableXSSProtection,
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
crossOriginEmbedderPolicy: false, // Disable for API compatibility
crossOriginOpenerPolicy: this.isProduction, // Only in production
crossOriginResourcePolicy: { policy: 'cross-origin' },
dnsPrefetchControl: true,
frameguard: { action: 'deny' },
hidePoweredBy: true,
ieNoOpen: true,
originAgentCluster: this.isProduction, // Only in production to avoid clustering warnings
permittedCrossDomainPolicies: false,
}));
// Add custom security headers
app.use((req, res, next) => {
// Security headers
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('X-Frame-Options', 'DENY');
res.setHeader('X-XSS-Protection', '1; mode=block');
res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
// API-specific headers
res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate');
res.setHeader('Pragma', 'no-cache');
res.setHeader('Expires', '0');
next();
});
this.logger.info('Helmet.js security headers configured', {
csp: !!cspConfig,
hsts: !!hstsConfig,
production: this.isProduction,
});
}
/**
* Setup dynamic CORS validation
*/
setupDynamicCORS(app) {
if (!this.config.enableDynamicCORS) {
this.logger.info('Dynamic CORS disabled by configuration');
return;
}
app.use((req, res, next) => {
const origin = req.headers.origin;
if (!origin) {
// Allow requests without origin (e.g., mobile apps, Postman)
this.addCORSHeaders(res, origin);
return next();
}
if (this.isOriginAllowed(origin)) {
this.addCORSHeaders(res, origin);
this.logger.debug('CORS allowed for origin', { origin });
}
else {
this.logger.warn('CORS blocked for unauthorized origin', { origin });
// Log security event
this.recordSecurityEvent('cors_blocked', {
origin,
userAgent: req.headers['user-agent'],
ip: req.ip,
path: req.path,
});
res.status(403).json({
success: false,
message: 'CORS policy violation',
});
return;
}
next();
});
this.logger.info('Dynamic CORS validation configured', {
allowedOrigins: this.config.allowedOrigins.length,
production: this.isProduction,
});
}
/**
* Setup enhanced request validation
*/
setupRequestValidation(app) {
// Request size limiting
app.use((req, res, next) => {
const contentLength = req.headers['content-length'];
const maxSize = this.parseSize(this.config.maxRequestSize);
if (contentLength && parseInt(contentLength) > maxSize) {
this.logger.warn('Request size exceeded limit', {
contentLength,
maxSize,
path: req.path,
ip: req.ip,
});
this.recordSecurityEvent('request_size_exceeded', {
contentLength,
maxSize,
path: req.path,
ip: req.ip,
});
res.status(413).json({
success: false,
message: 'Request entity too large',
statusCode: 413,
});
return;
}
next();
});
// Suspicious request detection
app.use((req, res, next) => {
this.detectSuspiciousRequests(req, res, next);
});
this.logger.info('Enhanced request validation configured');
}
/**
* Initialize allowed origins
*/
initializeAllowedOrigins() {
// Add configured origins
this.config.allowedOrigins.forEach(origin => {
this.allowedOrigins.add(origin);
});
// Add development origins if not in production
if (!this.isProduction) {
const devOrigins = [
'http://localhost:3000',
'http://localhost:3001',
'http://localhost:8080',
'http://localhost:8888',
'http://127.0.0.1:3000',
'http://127.0.0.1:8080',
'http://127.0.0.1:8888',
];
devOrigins.forEach(origin => {
this.allowedOrigins.add(origin);
});
}
this.logger.info('Allowed origins initialized', {
count: this.allowedOrigins.size,
origins: Array.from(this.allowedOrigins),
});
}
/**
* Check if origin is allowed
*/
isOriginAllowed(origin) {
// Always allow same origin
if (!origin)
return true;
// Check explicit allowed origins
if (this.allowedOrigins.has(origin))
return true;
// Check for wildcard subdomain matches (e.g., https://*.trusted.com)
for (const allowedOrigin of this.allowedOrigins) {
if (allowedOrigin.includes('*.')) {
// Convert wildcard pattern to regex: https://*.trusted.com -> https://[^.]+\.trusted\.com
const escaped = allowedOrigin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const pattern = escaped.replace('\\*', '[^.]+');
const regex = new RegExp(`^${pattern}$`);
if (regex.test(origin)) {
return true;
}
}
}
// In development, be more permissive with localhost
if (!this.isProduction && this.isDevelopmentOrigin(origin)) {
return true;
}
return false;
}
/**
* Check if origin is a development origin
*/
isDevelopmentOrigin(origin) {
const devPatterns = [
/^http:\/\/localhost:\d+$/,
/^http:\/\/127\.0\.0\.1:\d+$/,
/^http:\/\/0\.0\.0\.0:\d+$/,
];
return devPatterns.some(pattern => pattern.test(origin));
}
/**
* Add CORS headers to response
*/
addCORSHeaders(res, origin) {
const headers = {
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS, PATCH',
'Access-Control-Allow-Headers': 'Origin, X-Requested-With, Content-Type, Accept, Authorization, X-API-Key, X-Correlation-ID',
'Access-Control-Max-Age': '86400',
};
if (origin) {
headers['Access-Control-Allow-Origin'] = origin;
}
else {
headers['Access-Control-Allow-Origin'] = '*';
}
res.set(headers);
}
/**
* Detect suspicious requests
*/
detectSuspiciousRequests(req, res, next) {
const suspiciousPatterns = [
/(<script|javascript:|data:)/i, // XSS attempts
/(union\s+select|drop\s+table|insert\s+into)/i, // SQL injection
/(\.\.|\.\.\/)/g, // Path traversal
/(eval\s*\(|expression\s*\()/i, // Code injection
];
const checkData = JSON.stringify({
url: req.url,
body: req.body,
query: req.query,
headers: req.headers,
});
const suspicious = suspiciousPatterns.some(pattern => pattern.test(checkData));
if (suspicious) {
this.logger.warn('Suspicious request detected', {
ip: req.ip,
userAgent: req.headers['user-agent'],
path: req.path,
method: req.method,
});
this.recordSecurityEvent('suspicious_request', {
ip: req.ip,
userAgent: req.headers['user-agent'],
path: req.path,
method: req.method,
timestamp: new Date().toISOString(),
});
res.status(400).json({
success: false,
message: 'Invalid request detected',
statusCode: 400,
});
return;
}
next();
}
/**
* Record security event for monitoring
*/
recordSecurityEvent(event, data) {
this.logger.warn('Security event recorded', {
event,
...data,
timestamp: new Date().toISOString(),
});
// Here you would integrate with your monitoring/alerting system
// For example, send to monitoring service, write to security log, etc.
}
/**
* Parse size string to bytes
*/
parseSize(size) {
const units = {
b: 1,
kb: 1024,
mb: 1024 * 1024,
gb: 1024 * 1024 * 1024,
};
const match = size.toLowerCase().match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)?$/);
if (!match) {
this.logger.warn(`Invalid size format: ${size}, using default 1MB`);
return 1024 * 1024; // Default to 1MB
}
const value = parseFloat(match[1]);
const unit = match[2] || 'b';
return Math.floor(value * units[unit]);
}
/**
* Health check for security plugin
*/
async healthCheck() {
try {
return {
status: 'healthy',
details: {
helmet: this.config.enableSecurityHeaders,
csp: this.config.enableCSP,
hsts: this.config.enableHSTS && this.isProduction,
dynamicCORS: this.config.enableDynamicCORS,
inputSanitization: this.config.enableInputSanitization && !!this.XSSProtection && !!this.SQLInjectionProtection,
allowedOrigins: this.allowedOrigins.size,
environment: this.isProduction ? 'production' : 'development',
},
};
}
catch (error) {
return {
status: 'unhealthy',
details: {
error: error instanceof Error ? error.message : 'Unknown error',
},
};
}
}
}
exports.EnhancedSecurityPlugin = EnhancedSecurityPlugin;