Initial commit: Telegram Management System
Some checks failed
Deploy / deploy (push) Has been cancelled
Some checks failed
Deploy / deploy (push) Has been cancelled
Full-stack web application for Telegram management - Frontend: Vue 3 + Vben Admin - Backend: NestJS - Features: User management, group broadcast, statistics 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
304
marketing-agent/services/api-gateway/src/middleware/security.js
Normal file
304
marketing-agent/services/api-gateway/src/middleware/security.js
Normal file
@@ -0,0 +1,304 @@
|
||||
import helmet from 'helmet';
|
||||
import cors from 'cors';
|
||||
import hpp from 'hpp';
|
||||
import mongoSanitize from 'express-mongo-sanitize';
|
||||
import { logger } from '../utils/logger.js';
|
||||
import { config } from '../config/index.js';
|
||||
import crypto from 'crypto';
|
||||
|
||||
/**
|
||||
* Configure CORS
|
||||
*/
|
||||
export const corsOptions = {
|
||||
origin: function (origin, callback) {
|
||||
const allowedOrigins = config.cors?.allowedOrigins || [
|
||||
'http://localhost:8080',
|
||||
'http://localhost:3000',
|
||||
'https://app.marketing-agent.com'
|
||||
];
|
||||
|
||||
// Allow requests with no origin (like mobile apps or Postman)
|
||||
if (!origin) return callback(null, true);
|
||||
|
||||
if (allowedOrigins.indexOf('*') !== -1 || allowedOrigins.indexOf(origin) !== -1) {
|
||||
callback(null, true);
|
||||
} else {
|
||||
logger.warn('CORS blocked request', { origin, ip: origin });
|
||||
callback(new Error('Not allowed by CORS'));
|
||||
}
|
||||
},
|
||||
credentials: true,
|
||||
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
|
||||
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key', 'X-Request-ID'],
|
||||
exposedHeaders: ['X-Request-ID', 'X-RateLimit-Limit', 'X-RateLimit-Remaining', 'X-RateLimit-Reset'],
|
||||
maxAge: 86400 // 24 hours
|
||||
};
|
||||
|
||||
/**
|
||||
* Configure Helmet security headers
|
||||
*/
|
||||
export const helmetConfig = helmet({
|
||||
contentSecurityPolicy: {
|
||||
directives: {
|
||||
defaultSrc: ["'self'"],
|
||||
scriptSrc: ["'self'", "'unsafe-inline'"],
|
||||
styleSrc: ["'self'", "'unsafe-inline'"],
|
||||
imgSrc: ["'self'", "data:", "https:"],
|
||||
connectSrc: ["'self'"],
|
||||
fontSrc: ["'self'"],
|
||||
objectSrc: ["'none'"],
|
||||
mediaSrc: ["'self'"],
|
||||
frameSrc: ["'none'"],
|
||||
upgradeInsecureRequests: config.environment === 'production' ? [] : null
|
||||
}
|
||||
},
|
||||
hsts: {
|
||||
maxAge: 31536000,
|
||||
includeSubDomains: true,
|
||||
preload: true
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Request ID middleware
|
||||
*/
|
||||
export function requestId(req, res, next) {
|
||||
const id = req.get('X-Request-ID') || crypto.randomUUID();
|
||||
req.id = id;
|
||||
res.setHeader('X-Request-ID', id);
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* IP whitelist/blacklist middleware
|
||||
*/
|
||||
export function ipFilter(options = {}) {
|
||||
const whitelist = options.whitelist || [];
|
||||
const blacklist = options.blacklist || [];
|
||||
|
||||
return (req, res, next) => {
|
||||
const clientIp = req.ip || req.connection.remoteAddress;
|
||||
|
||||
// Check blacklist first
|
||||
if (blacklist.length > 0 && blacklist.includes(clientIp)) {
|
||||
logger.warn('Blacklisted IP attempted access', { ip: clientIp });
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Access denied'
|
||||
});
|
||||
}
|
||||
|
||||
// Check whitelist if configured
|
||||
if (whitelist.length > 0 && !whitelist.includes(clientIp)) {
|
||||
logger.warn('Non-whitelisted IP attempted access', { ip: clientIp });
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Access denied'
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Security headers middleware
|
||||
*/
|
||||
export function securityHeaders(req, res, next) {
|
||||
// Additional security headers
|
||||
res.setHeader('X-Frame-Options', 'DENY');
|
||||
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||
res.setHeader('X-XSS-Protection', '1; mode=block');
|
||||
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
|
||||
|
||||
// Remove potentially sensitive headers
|
||||
res.removeHeader('X-Powered-By');
|
||||
res.removeHeader('Server');
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* API version check middleware
|
||||
*/
|
||||
export function apiVersionCheck(supportedVersions = ['v1']) {
|
||||
return (req, res, next) => {
|
||||
const version = req.headers['api-version'] || req.query.apiVersion;
|
||||
|
||||
if (version && !supportedVersions.includes(version)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
error: 'Unsupported API version',
|
||||
supportedVersions
|
||||
});
|
||||
}
|
||||
|
||||
req.apiVersion = version || supportedVersions[0];
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent parameter pollution
|
||||
*/
|
||||
export const preventParamPollution = hpp({
|
||||
whitelist: ['sort', 'fields', 'filter', 'page', 'limit']
|
||||
});
|
||||
|
||||
/**
|
||||
* MongoDB injection prevention
|
||||
*/
|
||||
export const preventMongoInjection = mongoSanitize({
|
||||
replaceWith: '_',
|
||||
onSanitize: ({ req, key }) => {
|
||||
logger.warn('MongoDB injection attempt prevented', {
|
||||
ip: req.ip,
|
||||
path: req.path,
|
||||
key
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Request logging middleware
|
||||
*/
|
||||
export function requestLogger(req, res, next) {
|
||||
const start = Date.now();
|
||||
|
||||
res.on('finish', () => {
|
||||
const duration = Date.now() - start;
|
||||
|
||||
logger.info('Request processed', {
|
||||
requestId: req.id,
|
||||
method: req.method,
|
||||
path: req.path,
|
||||
statusCode: res.statusCode,
|
||||
duration,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('user-agent'),
|
||||
userId: req.user?.id
|
||||
});
|
||||
});
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Error logging middleware
|
||||
*/
|
||||
export function errorLogger(err, req, res, next) {
|
||||
logger.error('Request error', {
|
||||
requestId: req.id,
|
||||
error: err.message,
|
||||
stack: err.stack,
|
||||
method: req.method,
|
||||
path: req.path,
|
||||
ip: req.ip,
|
||||
userId: req.user?.id
|
||||
});
|
||||
|
||||
next(err);
|
||||
}
|
||||
|
||||
/**
|
||||
* Security audit middleware
|
||||
*/
|
||||
export function securityAudit(eventType) {
|
||||
return (req, res, next) => {
|
||||
logger.info('Security event', {
|
||||
eventType,
|
||||
requestId: req.id,
|
||||
userId: req.user?.id,
|
||||
ip: req.ip,
|
||||
path: req.path,
|
||||
method: req.method,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Trusted proxy configuration
|
||||
*/
|
||||
export function configureTrustedProxies(app) {
|
||||
// Trust proxies for accurate IP detection
|
||||
app.set('trust proxy', config.trustProxy || ['loopback', 'linklocal', 'uniquelocal']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Session fixation prevention
|
||||
*/
|
||||
export function preventSessionFixation(req, res, next) {
|
||||
if (req.session && req.user) {
|
||||
// Regenerate session ID on login
|
||||
if (req.path.includes('/login') && res.statusCode === 200) {
|
||||
req.session.regenerate((err) => {
|
||||
if (err) {
|
||||
logger.error('Session regeneration failed', { error: err });
|
||||
}
|
||||
next();
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* CSRF protection for state-changing operations
|
||||
*/
|
||||
export function csrfProtection(options = {}) {
|
||||
const excludePaths = options.exclude || ['/api/v1/auth/login', '/api/v1/auth/register'];
|
||||
|
||||
return (req, res, next) => {
|
||||
// Skip for excluded paths
|
||||
if (excludePaths.some(path => req.path.includes(path))) {
|
||||
return next();
|
||||
}
|
||||
|
||||
// Skip for safe methods
|
||||
if (['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const token = req.headers['x-csrf-token'] || req.body._csrf;
|
||||
const sessionToken = req.session?.csrfToken;
|
||||
|
||||
if (!token || !sessionToken || token !== sessionToken) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
error: 'Invalid CSRF token'
|
||||
});
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply all security middleware
|
||||
*/
|
||||
export function applySecurityMiddleware(app) {
|
||||
// Basic security
|
||||
app.use(helmetConfig);
|
||||
app.use(cors(corsOptions));
|
||||
app.use(requestId);
|
||||
app.use(securityHeaders);
|
||||
|
||||
// Request processing
|
||||
app.use(express.json({ limit: '10mb' }));
|
||||
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
||||
|
||||
// Injection prevention
|
||||
app.use(preventMongoInjection);
|
||||
app.use(preventParamPollution);
|
||||
|
||||
// Logging
|
||||
app.use(requestLogger);
|
||||
|
||||
// Configure trusted proxies
|
||||
configureTrustedProxies(app);
|
||||
}
|
||||
Reference in New Issue
Block a user