Skip to main content
Glama
security.js•8.81 kB
/** * Security Middleware * Comprehensive security setup with rate limiting, validation, and monitoring */ const rateLimit = require('express-rate-limit'); const helmet = require('helmet'); const cors = require('cors'); const { body, validationResult } = require('express-validator'); const config = require('../config/config'); const logger = require('../utils/logger'); /** * Rate limiting middleware - DISABLED (delegated to API Gateway) */ // const rateLimitMiddleware = rateLimit({ // windowMs: config.security.rateLimit.windowMs, // max: config.security.rateLimit.max, // message: config.security.rateLimit.message, // standardHeaders: config.security.rateLimit.standardHeaders, // legacyHeaders: config.security.rateLimit.legacyHeaders, // handler: (req, res) => { // logger.logSecurity('rate_limit_exceeded', { // ip: req.ip, // userAgent: req.get('User-Agent'), // endpoint: req.path // }); // // res.status(429).json({ // error: 'Too Many Requests', // message: config.security.rateLimit.message.error, // retryAfter: config.security.rateLimit.message.retryAfter // }); // }, // skip: (req) => { // // Skip rate limiting for health checks // return req.path === '/api/v1/health'; // } // }); /** * CORS middleware */ const corsMiddleware = cors({ origin: config.security.cors.origin, credentials: config.security.cors.credentials, methods: config.security.cors.methods, allowedHeaders: config.security.cors.allowedHeaders, optionsSuccessStatus: 200 }); /** * Helmet security middleware */ const helmetMiddleware = helmet({ contentSecurityPolicy: config.security.helmet.contentSecurityPolicy, hsts: config.security.helmet.hsts, crossOriginEmbedderPolicy: false // Allow embedding for API responses }); /** * Request validation middleware */ const validateRequest = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { logger.logSecurity('validation_failed', { ip: req.ip, errors: errors.array(), body: req.body }); return res.status(400).json({ error: 'Validation Error', details: errors.array() }); } next(); }; /** * Validation rules for /validate endpoint */ const validateDiagramsInput = [ body('diagrams') .isArray({ min: 1, max: config.validation.maxTotalDiagrams }) .withMessage(`Must be an array with 1-${config.validation.maxTotalDiagrams} diagrams`), body('diagrams.*.content') .isString() .isLength({ min: 1, max: 50000 }) .withMessage('Each diagram content must be a non-empty string (max 50,000 characters)'), body('diagrams.*.type') .isString() .isIn([ // Basic diagrams 'flowchart', 'graph', 'sequenceDiagram', 'classDiagram', 'stateDiagram', 'stateDiagram-v2', 'erDiagram', 'gantt', 'journey', 'pie', 'mindmap', 'timeline', 'kanban', // Advanced diagrams 'requirement', 'requirementDiagram', 'sankey-beta', 'xychart-beta', 'block', 'block-beta', 'c4', 'C4Context', 'quadrant', 'quadrantChart', // Langium-based diagrams 'gitGraph', 'info', 'architecture', 'architecture-beta', 'radar', 'packet', 'packet-beta', 'treemap', 'treemap-beta', // Other 'zenuml', 'exampleDiagram' ]) .withMessage('Each diagram type must be a valid Mermaid diagram type'), body('options.timeout') .optional() .isInt({ min: 1000, max: 60000 }) .withMessage('timeout must be between 1000-60000ms') ]; /** * File upload security middleware */ const fileUploadSecurity = (req, res, next) => { // Debug logging console.log('Files debug:', { files: req.files, filesLength: req.files?.length, filesType: typeof req.files, body: req.body }); // Check if files were uploaded if (!req.files || req.files.length === 0) { return res.status(400).json({ error: 'No files uploaded', message: 'Please upload at least one file' }); } // Validate file count if (req.files.length > config.upload.maxFiles) { logger.logSecurity('too_many_files', { ip: req.ip, fileCount: req.files.length, maxAllowed: config.upload.maxFiles }); return res.status(400).json({ error: 'Too many files', message: `Maximum ${config.upload.maxFiles} files allowed` }); } // Validate each file for (const file of req.files) { // Check file size if (file.size > config.upload.maxFileSize) { logger.logSecurity('file_too_large', { ip: req.ip, fileName: file.originalname, size: file.size, maxSize: config.upload.maxFileSize }); return res.status(400).json({ error: 'File too large', message: `File "${file.originalname}" exceeds maximum size of ${config.upload.maxFileSize / (1024 * 1024)}MB` }); } // Check mime type and file extension const fileExtension = require('path').extname(file.originalname).toLowerCase(); const mimeTypeValid = config.upload.allowedMimeTypes.includes(file.mimetype); const extensionValid = config.upload.allowedExtensions.includes(fileExtension); // Allow file if either MIME type is valid OR extension is valid // This handles cases where MIME type detection fails if (!mimeTypeValid && !extensionValid) { logger.logSecurity('invalid_file_type', { ip: req.ip, fileName: file.originalname, mimeType: file.mimetype, extension: fileExtension }); return res.status(400).json({ error: 'Invalid file type', message: `File type not allowed. Expected extensions: ${config.upload.allowedExtensions.join(', ')}` }); } // Check for suspicious file names if (isSuspiciousFileName(file.originalname)) { logger.logSecurity('suspicious_filename', { ip: req.ip, fileName: file.originalname }); return res.status(400).json({ error: 'Invalid file name', message: 'File name contains invalid characters' }); } } next(); }; /** * Check for suspicious file names * @param {string} fileName - File name to check * @returns {boolean} True if suspicious */ function isSuspiciousFileName(fileName) { const suspiciousPatterns = [ /\.\./, // Directory traversal /[<>:"|?*]/, // Invalid file name characters /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i, // Windows reserved names /^\./, // Hidden files /.{255,}/ // Extremely long names ]; return suspiciousPatterns.some(pattern => pattern.test(fileName)); } /** * Error handling middleware */ const errorHandler = (error, req, res, _next) => { logger.logError(error, { ip: req.ip, method: req.method, url: req.url, userAgent: req.get('User-Agent') }); // Handle specific error types if (error.type === 'entity.too.large') { return res.status(413).json({ error: 'Payload Too Large', message: 'Request body exceeds maximum allowed size' }); } if (error.code === 'LIMIT_FILE_SIZE') { return res.status(413).json({ error: 'File Too Large', message: 'One or more files exceed the maximum allowed size' }); } if (error.code === 'LIMIT_FILE_COUNT') { return res.status(400).json({ error: 'Too Many Files', message: 'Number of files exceeds the maximum allowed' }); } if (error.code === 'LIMIT_UNEXPECTED_FILE') { return res.status(400).json({ error: 'Unexpected File Field', message: 'Unexpected file field in upload' }); } // Default error response res.status(500).json({ error: 'Internal Server Error', message: config.server.env === 'development' ? error.message : 'Something went wrong' }); }; /** * Request logging middleware */ const requestLogger = (req, res, next) => { const startTime = Date.now(); res.on('finish', () => { const responseTime = Date.now() - startTime; logger.logRequest(req, res, responseTime); }); next(); }; /** * Content type validation middleware */ const validateContentType = (expectedTypes) => { return (req, res, next) => { const contentType = req.get('Content-Type'); if (!contentType || !expectedTypes.some(type => contentType.includes(type))) { return res.status(415).json({ error: 'Unsupported Media Type', message: `Expected one of: ${expectedTypes.join(', ')}` }); } next(); }; }; module.exports = { // rateLimitMiddleware, // DISABLED - delegated to API Gateway corsMiddleware, helmetMiddleware, validateRequest, validateDiagramsInput, fileUploadSecurity, errorHandler, requestLogger, validateContentType };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ai-of-mine/fast-mermaid-validator-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server