Skip to main content
Glama
security.ts9.05 kB
import { Request, Response, NextFunction } from 'express'; import cors from 'cors'; import helmet from 'helmet'; import { z } from 'zod'; // Environment configuration schema const securityConfigSchema = z.object({ allowedOrigins: z.string().default('*'), corsMaxAge: z.number().default(86400), // 24 hours enableHSTS: z.boolean().default(true), hstsMaxAge: z.number().default(31536000), // 1 year enableCSP: z.boolean().default(true), logSecurityEvents: z.boolean().default(true), }); type SecurityConfig = z.infer<typeof securityConfigSchema>; /** * Parse allowed origins from environment variable */ function parseAllowedOrigins(origins: string): string[] | boolean { if (origins === '*') { return true; // Allow all origins } if (origins === 'false' || origins === 'none') { return false; // Allow no origins } // Parse comma-separated list of origins return origins.split(',').map(origin => origin.trim()).filter(Boolean); } /** * Get security configuration from environment variables */ function getSecurityConfig(): SecurityConfig { const config = { allowedOrigins: process.env.CORS_ALLOWED_ORIGINS || '*', corsMaxAge: parseInt(process.env.CORS_MAX_AGE || '86400', 10), enableHSTS: process.env.ENABLE_HSTS !== 'false', hstsMaxAge: parseInt(process.env.HSTS_MAX_AGE || '31536000', 10), enableCSP: process.env.ENABLE_CSP !== 'false', logSecurityEvents: process.env.LOG_SECURITY_EVENTS !== 'false', }; return securityConfigSchema.parse(config); } /** * Security event logger middleware */ export function securityLogger(req: Request, _res: Response, next: NextFunction): void { const config = getSecurityConfig(); if (!config.logSecurityEvents) { return next(); } const securityInfo = { timestamp: new Date().toISOString(), method: req.method, url: req.url, ip: req.ip || req.connection.remoteAddress, userAgent: req.get('User-Agent'), origin: req.get('Origin'), referer: req.get('Referer'), contentType: req.get('Content-Type'), acceptEncoding: req.get('Accept-Encoding'), authorization: req.get('Authorization') ? '[REDACTED]' : undefined, }; // Log security-relevant information (without sensitive data) console.log('Security Log:', JSON.stringify(securityInfo, null, 2)); next(); } /** * Configure CORS with security best practices */ export function configureCORS() { const config = getSecurityConfig(); const allowedOrigins = parseAllowedOrigins(config.allowedOrigins); return cors({ origin: allowedOrigins, methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], allowedHeaders: [ 'Content-Type', 'Authorization', 'X-Requested-With', 'X-API-Key', 'Accept', 'Origin', ], exposedHeaders: [ 'X-RateLimit-Limit', 'X-RateLimit-Remaining', 'X-RateLimit-Reset', ], credentials: true, maxAge: config.corsMaxAge, preflightContinue: false, optionsSuccessStatus: 204, }); } /** * Configure security headers using helmet */ export function configureSecurityHeaders() { const config = getSecurityConfig(); return helmet({ // Content Security Policy contentSecurityPolicy: config.enableCSP ? { directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:", "https:"], connectSrc: ["'self'", "https://api.tally.so"], fontSrc: ["'self'", "https:", "data:"], objectSrc: ["'none'"], mediaSrc: ["'self'"], frameSrc: ["'none'"], upgradeInsecureRequests: [], }, reportOnly: false, } : false, // Strict Transport Security (HSTS) hsts: config.enableHSTS ? { maxAge: config.hstsMaxAge, includeSubDomains: true, preload: true, } : false, // X-Content-Type-Options noSniff: true, // X-Frame-Options frameguard: { action: 'deny', }, // X-XSS-Protection (legacy, but still useful for older browsers) xssFilter: true, // Referrer Policy referrerPolicy: { policy: ['no-referrer-when-downgrade'], }, // X-Permitted-Cross-Domain-Policies permittedCrossDomainPolicies: false, // X-DNS-Prefetch-Control dnsPrefetchControl: { allow: false, }, // Expect-CT is deprecated and removed from helmet v7+ // Remove X-Powered-By header hidePoweredBy: true, // Origin Agent Cluster originAgentCluster: true, // Cross-Origin-Embedder-Policy crossOriginEmbedderPolicy: false, // May interfere with API responses // Cross-Origin-Opener-Policy crossOriginOpenerPolicy: { policy: 'same-origin-allow-popups', }, // Cross-Origin-Resource-Policy crossOriginResourcePolicy: { policy: 'cross-origin', }, }); } /** * Custom security middleware for additional protections */ export function customSecurityMiddleware(req: Request, res: Response, next: NextFunction): void { // Set custom security headers res.setHeader('X-API-Version', '1.0.0'); res.setHeader('X-Request-ID', req.headers['x-request-id'] || generateRequestId()); // Prevent caching of sensitive endpoints if (req.path.includes('/api/') && req.method !== 'GET') { res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, private'); res.setHeader('Pragma', 'no-cache'); res.setHeader('Expires', '0'); } // Add timing attack protection for authentication endpoints if (req.path.includes('/auth') || req.path.includes('/login')) { // Add small random delay to prevent timing attacks const delay = Math.random() * 100; // 0-100ms setTimeout(() => next(), delay); return; } next(); } /** * Generate a unique request ID */ function generateRequestId(): string { return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } /** * Security validation middleware for request integrity */ export function securityValidation(req: Request, res: Response, next: NextFunction): void { const decodedUrl = decodeURIComponent(req.url); const originalUrl = req.originalUrl; // 1. Directory Traversal if (decodedUrl.includes('../') || decodedUrl.includes('..\\\\') || originalUrl.includes('../') || originalUrl.includes('..\\\\')) { console.warn(`Security Warning: Directory traversal attempt detected: ${req.url}`); res.status(400).json({ error: 'Invalid request path', code: 'SECURITY_VIOLATION', message: 'Directory traversal is not permitted.', }); return; } // 2. Null Byte Injection if (decodedUrl.includes('\\0') || originalUrl.includes('\\0') || decodedUrl.includes('%00') || originalUrl.includes('%00')) { console.warn(`Security Warning: Null byte detected in URL: ${req.url}`); res.status(400).json({ error: 'Invalid request path', code: 'SECURITY_VIOLATION', message: 'Null bytes are not permitted in the URL.', }); return; } // 3. XSS and JavaScript Protocol const xssPatterns = /<script|javascript:|onerror|onload|onmouseover/i; if (xssPatterns.test(decodedUrl)) { console.warn(`Security Warning: XSS or JavaScript protocol attempt detected: ${req.url}`); res.status(400).json({ error: 'Invalid request path', code: 'SECURITY_VIOLATION', message: 'XSS and JavaScript protocol are not permitted.', }); return; } // 4. User-Agent Validation const userAgent = req.headers['user-agent']; if (!userAgent) { console.warn('Security Warning: Missing or empty User-Agent header.'); } else if (userAgent.length > 256) { console.warn(`Security Warning: Oversized User-Agent header: ${userAgent.length} chars`); } // 5. Content-Length Validation const contentLength = req.headers['content-length']; if (contentLength) { const length = parseInt(contentLength, 10); if (isNaN(length) || length < 0) { console.warn(`Security Warning: Invalid Content-Length header: ${contentLength}`); res.status(400).json({ error: 'Invalid Content-Length', code: 'INVALID_CONTENT_LENGTH', message: 'Content-Length header is not a valid non-negative integer.', }); return; } if (length > 1024 * 1024) { // 1MB limit console.warn(`Security Warning: Oversized payload attempt: ${length} bytes`); res.status(413).json({ error: 'Payload Too Large', code: 'PAYLOAD_TOO_LARGE', message: `Payload size exceeds the limit of 1MB.`, }); return; } } next(); } /** * Apply all security middleware in the correct order */ export function applySecurityMiddleware() { return [ securityLogger, configureCORS(), configureSecurityHeaders(), customSecurityMiddleware, securityValidation, ]; } export default { configureCORS, configureSecurityHeaders, customSecurityMiddleware, securityLogger, securityValidation, applySecurityMiddleware, };

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/learnwithcc/tally-mcp'

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