From ccf4c4372cf1ae2fd33fc37d3ad72b69c4635b94 Mon Sep 17 00:00:00 2001 From: Emanuel Jesus Leiva Navarro Date: Sat, 6 Sep 2025 16:46:50 -0400 Subject: [PATCH 1/9] add: .env.example --- .env.example | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..bdcba67 --- /dev/null +++ b/.env.example @@ -0,0 +1,29 @@ +# GitHub MCP SSE Server Configuration + +# GitHub API Token (required for API access) +# Generate a token at https://github.com/settings/tokens +GITHUB_TOKEN=your_github_token_here + +# Server Port Configuration +MCP_SSE_PORT=3200 + +# Timeout Configuration (in milliseconds) +MCP_TIMEOUT=180000 + +# Log Level (debug, info, warn, error) +LOG_LEVEL=info + +# CORS Configuration +CORS_ALLOW_ORIGIN=* + +# Multiplexing SSE Transport Configuration +# Set to 'true' to enable multiplexing SSE transport (handles multiple clients with a single transport) +# Set to 'false' to use individual SSE transport for each client (legacy behavior) +USE_MULTIPLEXING_SSE=false + +# Rate Limiting Configuration +RATE_LIMIT_WINDOW_MS=900000 # Time window for rate limiting in milliseconds (e.g., 900000 for 15 minutes) +RATE_LIMIT_MAX_REQUESTS=100 # Maximum number of requests allowed per window per IP +RATE_LIMIT_SSE_MAX=5 # Maximum number of SSE connections allowed per minute per IP +RATE_LIMIT_MESSAGES_MAX=30 # Maximum number of messages allowed per minute per IP +DEFAULT_USER_RATE_LIMIT=1000 # Default number of requests allowed per hour for a user From 123e3bf483636edd1506513853182dd05faaa896 Mon Sep 17 00:00:00 2001 From: Emanuel Jesus Leiva Navarro Date: Sat, 6 Sep 2025 17:18:51 -0400 Subject: [PATCH 2/9] fix issues 54 --- .env.example | 10 ++ README.md | 43 ++++---- package-lock.json | 21 ++++ package.json | 2 + src/config/index.ts | 12 +++ src/server.ts | 258 ++++++++++++++++++++++++++++++++++++++++---- 6 files changed, 299 insertions(+), 47 deletions(-) diff --git a/.env.example b/.env.example index bdcba67..e5d8684 100644 --- a/.env.example +++ b/.env.example @@ -27,3 +27,13 @@ RATE_LIMIT_MAX_REQUESTS=100 # Maximum number of requests allowed per window per RATE_LIMIT_SSE_MAX=5 # Maximum number of SSE connections allowed per minute per IP RATE_LIMIT_MESSAGES_MAX=30 # Maximum number of messages allowed per minute per IP DEFAULT_USER_RATE_LIMIT=1000 # Default number of requests allowed per hour for a user + +# Security Headers Configuration +HSTS_MAX_AGE=31536000 +CSP_REPORT_ONLY=false +CSP_REPORT_URI=https://your-domain.com/csp-report + +# For development +NODE_ENV=development +DISABLE_HSTS=true +CSP_REPORT_ONLY=true diff --git a/README.md b/README.md index 128bab4..a9239e5 100644 --- a/README.md +++ b/README.md @@ -59,30 +59,11 @@ The project follows a modular, feature-based architecture. All source code is lo pnpm install ``` -3. Create a `.env` file in the root directory with the following content: - ``` - # GitHub MCP SSE Server Configuration - # GitHub API Token (required for API access) - # Generate a token at https://github.com/settings/tokens - GITHUB_TOKEN=your_github_token_here - - # Server Port Configuration - MCP_SSE_PORT=3200 - - # Timeout Configuration (in milliseconds) - MCP_TIMEOUT=180000 - - # Log Level (debug, info, warn, error) - LOG_LEVEL=info - - # CORS Configuration - CORS_ALLOW_ORIGIN=* - - # Multiplexing SSE Transport Configuration - # Set to 'true' to enable multiplexing SSE transport (handles multiple clients with a single transport) - # Set to 'false' to use individual SSE transport for each client (legacy behavior) - USE_MULTIPLEXING_SSE=false +3. Create a `.env` file by copying the example file: + ```bash + cp .env.example .env ``` + Then, edit the `.env` file to add your GitHub token and customize the other settings as needed. 4. Build the project: ```bash @@ -251,6 +232,20 @@ The server provides the following GitHub API tools: - `get_me` - Get details of the authenticated user +## Security + +This server implements several security best practices to protect against common web vulnerabilities. The security measures are configured in `src/server.ts` and include: + +- **Helmet**: A collection of 12 middleware functions that set various HTTP headers to secure the application. This includes protection against XSS, clickjacking, and other common attacks. +- **Content Security Policy (CSP)**: A policy that helps prevent XSS attacks by specifying which sources of content are allowed to be loaded on a page. +- **HTTP Strict Transport Security (HSTS)**: A policy that forces browsers to use HTTPS for all connections to the server. +- **X-Frame-Options**: A header that prevents the application from being embedded in iframes, which helps prevent clickjacking attacks. +- **X-Content-Type-Options**: A header that prevents browsers from MIME-sniffing a response away from the declared content-type. +- **Referrer-Policy**: A header that controls how much referrer information (sent via the Referer header) should be included with requests. +- **Permissions-Policy**: A header that allows you to control which features and APIs can be used in the browser. + +The security headers can be configured using environment variables. For more information, see the `.env.example` file. + ## Troubleshooting ### Connection Issues @@ -300,4 +295,4 @@ If you're experiencing issues with the multiplexing SSE transport: ## License -MIT \ No newline at end of file +MIT diff --git a/package-lock.json b/package-lock.json index 1f086ab..2687c33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "dompurify": "^3.2.6", "dotenv": "^16.5.0", "express": "^5.1.0", + "helmet": "^8.1.0", "http-terminator": "^3.2.0", "jsdom": "^26.1.0", "raw-body": "^3.0.0", @@ -24,6 +25,7 @@ "@types/cors": "^2.8.19", "@types/dompurify": "^3.0.5", "@types/express": "^5.0.1", + "@types/helmet": "^0.0.48", "@types/jest": "^30.0.0", "@types/jsdom": "^21.1.7", "@types/node": "^22.14.1", @@ -1354,6 +1356,16 @@ "@types/send": "*" } }, + "node_modules/@types/helmet": { + "version": "0.0.48", + "resolved": "https://registry.npmjs.org/@types/helmet/-/helmet-0.0.48.tgz", + "integrity": "sha512-C7MpnvSDrunS1q2Oy1VWCY7CDWHozqSnM8P4tFeRTuzwqni+PYOjEredwcqWG+kLpYcgLsgcY3orHB54gbx2Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", @@ -3339,6 +3351,15 @@ "node": ">= 0.4" } }, + "node_modules/helmet": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", + "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", diff --git a/package.json b/package.json index f91713b..fc0a987 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "dompurify": "^3.2.6", "dotenv": "^16.5.0", "express": "^5.1.0", + "helmet": "^8.1.0", "http-terminator": "^3.2.0", "jsdom": "^26.1.0", "raw-body": "^3.0.0", @@ -53,6 +54,7 @@ "@types/cors": "^2.8.19", "@types/dompurify": "^3.0.5", "@types/express": "^5.0.1", + "@types/helmet": "^0.0.48", "@types/jest": "^30.0.0", "@types/jsdom": "^21.1.7", "@types/node": "^22.14.1", diff --git a/src/config/index.ts b/src/config/index.ts index cb30210..4e505e6 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -23,4 +23,16 @@ export const config = { sseTimeout: process.env.SSE_TIMEOUT ? parseInt(process.env.SSE_TIMEOUT, 10) : 1800000, corsAllowOrigin: process.env.CORS_ALLOW_ORIGIN ?? '', useMultiplexing: process.env.USE_MULTIPLEXING_SSE === 'true', + // Rate Limiting Configuration + rateLimitWindowMs: process.env.RATE_LIMIT_WINDOW_MS ? parseInt(process.env.RATE_LIMIT_WINDOW_MS, 10) : 15 * 60 * 1000, // 15 minutes + rateLimitMaxRequests: process.env.RATE_LIMIT_MAX_REQUESTS ? parseInt(process.env.RATE_LIMIT_MAX_REQUESTS, 10) : 100, // 100 requests + rateLimitSseMax: process.env.RATE_LIMIT_SSE_MAX ? parseInt(process.env.RATE_LIMIT_SSE_MAX, 10) : 5, // 5 SSE connections per minute + rateLimitMessagesMax: process.env.RATE_LIMIT_MESSAGES_MAX ? parseInt(process.env.RATE_LIMIT_MESSAGES_MAX, 10) : 30, // 30 messages per minute + defaultUserRateLimit: process.env.DEFAULT_USER_RATE_LIMIT ? parseInt(process.env.DEFAULT_USER_RATE_LIMIT, 10) : 1000, // 1000 requests per hour per user + // Security Headers Configuration + hstsMaxAge: process.env.HSTS_MAX_AGE ? parseInt(process.env.HSTS_MAX_AGE, 10) : 31536000, + cspReportOnly: process.env.CSP_REPORT_ONLY === 'true', + cspReportUri: process.env.CSP_REPORT_URI, + nodeEnv: process.env.NODE_ENV ?? 'development', + disableHsts: process.env.DISABLE_HSTS === 'true', }; diff --git a/src/server.ts b/src/server.ts index 428e04d..853a8a1 100644 --- a/src/server.ts +++ b/src/server.ts @@ -14,6 +14,107 @@ import { z } from 'zod'; // Importar configuración y logger centralizados import { config } from '#config/index'; import { logger } from '#core/logger'; +import helmet from 'helmet'; +import rateLimit from 'express-rate-limit'; +import { Request, Response, NextFunction } from 'express'; + +declare global { + namespace Express { + interface Request { + rateLimit?: { + limit: number; + current: number; + remaining: number; + resetTime?: Date; + }; + } + } +} + +const generalLimiter = rateLimit({ + windowMs: config.rateLimitWindowMs, + max: config.rateLimitMaxRequests, + message: { + error: 'Too many requests from this IP', + retryAfter: `${config.rateLimitWindowMs / 60000} minutes` + }, + standardHeaders: true, + legacyHeaders: false, + handler: (req, res) => { + logger.warn(`Rate limit exceeded for IP: ${req.ip} on ${req.method} ${req.url}`); + res.status(429).json({ + error: 'Rate limit exceeded', + retryAfter: Math.ceil((req.rateLimit?.resetTime?.getTime() || Date.now()) / 1000) + }); + } +}); + +const sseLimiter = rateLimit({ + windowMs: 60 * 1000, // 1 minuto + max: config.rateLimitSseMax, + message: 'Too many SSE connections from this IP' +}); + +const messageLimiter = rateLimit({ + windowMs: 60 * 1000, // 1 minuto + max: config.rateLimitMessagesMax, + message: 'Too many messages from this IP', // This will be overridden by handler + handler: (req, res) => { + logger.warn(`Message Rate limit exceeded for IP: ${req.ip}`); + res.status(429).json({ + error: 'Rate limit exceeded', + message: 'Too many messages from this IP' + }); + } +}); + +const createUserLimiter = () => rateLimit({ + windowMs: 60 * 60 * 1000, // 1 hora + max: (req: Request) => { + // @ts-ignore + return req.user?.rateLimits?.requestsPerHour || config.defaultUserRateLimit; + }, + message: 'User rate limit exceeded' +}); + +const CRITICAL_TOOLS = [ + 'create_repository', + 'merge_pull_request', + 'push_files', + 'create_fork' +]; + +const isCriticalOperation = (toolName: string): boolean => { + return CRITICAL_TOOLS.includes(toolName); +}; + +const criticalOperationsLimiter = rateLimit({ + windowMs: 60 * 60 * 1000, // 1 hora + max: 10, // Solo 10 operaciones críticas por hora + message: 'Critical operation rate limit exceeded', // This will be overridden by handler + handler: (req, res) => { + logger.warn(`Critical operation rate limit exceeded for IP: ${req.ip}`); + res.status(429).json({ + error: 'Rate limit exceeded', + message: 'Critical operation rate limit exceeded' + }); + } +}); + +const rateLimitMonitor = (req: Request, res: Response, next: NextFunction) => { + const remaining = req.rateLimit?.remaining || 0; + const total = req.rateLimit?.limit || 0; + + if (remaining > 0 && remaining < total * 0.1) { + logger.warn(`Rate limit warning for ${req.ip} on ${req.method} ${req.url}: ${remaining}/${total} remaining`); + } + + if (remaining === 0) { + logger.error(`Rate limit exceeded for ${req.ip} on ${req.method} ${req.url}`); + } + + next(); +}; const sseTransports: Record = {}; let multiplexingTransport: MultiplexingSSEServerTransport | null = null; @@ -45,6 +146,57 @@ export function closeAllSseConnections() { export function createServer(mcpServer: McpServer, port: number): http.Server { const app = express(); + app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'"], + scriptSrc: ["'self'"], + imgSrc: ["'self'", "data:", "https:"], + connectSrc: ["'self'"], + fontSrc: ["'self'"], + objectSrc: ["'none'"], + mediaSrc: ["'self'"], + frameSrc: ["'none'"], + frameAncestors: ["'none'"], + baseUri: ["'self'"], + formAction: ["'self'"], + reportUri: config.cspReportUri || '', + }, + reportOnly: config.cspReportOnly, + }, + crossOriginEmbedderPolicy: false, // Para SSE compatibility + hsts: config.disableHsts ? false : { + maxAge: config.hstsMaxAge, // 1 año + includeSubDomains: true, + preload: true + } + })); + + app.use((req, res, next) => { + // Prevenir que la respuesta sea embebida en iframes + res.setHeader('X-Frame-Options', 'DENY'); + + // Prevenir MIME type sniffing + res.setHeader('X-Content-Type-Options', 'nosniff'); + + // Control de referrer + res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); + + // Política de permisos + res.setHeader('Permissions-Policy', + 'camera=(), microphone=(), geolocation=(), payment=()'); + + // Headers para SSE + if (req.path === '/sse') { + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Expires', '0'); + } + + next(); + }); + app.use(cors({ origin: config.corsAllowOrigin, methods: ['GET', 'POST', 'OPTIONS'], @@ -61,8 +213,15 @@ export function createServer(mcpServer: McpServer, port: number): http.Server { } } })); + + // Aplicar rate limiting general a todas las rutas + app.use(generalLimiter); + app.use(rateLimitMonitor); // Aplicar el monitor después del limiter app.get('/health', (req: express.Request, res: express.Response) => { + res.setHeader('Cache-Control', 'no-cache, max-age=0'); + res.setHeader('X-Robots-Tag', 'noindex, nofollow'); + res.status(200).json({ status: 'ok', timestamp: new Date().toISOString(), @@ -94,38 +253,69 @@ export function createServer(mcpServer: McpServer, port: number): http.Server { }); } - app.all('/mcp', (req: express.Request, res: express.Response) => { + app.all('/mcp', createUserLimiter(), (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug(`New Streamable HTTP request: ${req.method} ${req.url}`); if (req.method === 'OPTIONS') { res.status(200).end(); return; } - - const requestTimeout = setTimeout(() => { - if (!res.writableEnded) { - logger.error(`Request timeout for ${req.url}`); - res.status(408).json({ - error: 'Request timeout', - message: 'The request took too long to process' - }); - } - }, config.mcpTimeout); - - mcpStreamableTransport.handleRequest(req, res, req.body) - .then(() => clearTimeout(requestTimeout)) - .catch((error) => { - clearTimeout(requestTimeout); - logger.error(`Error handling Streamable HTTP request for ${req.url}:`, error); + + // Check for critical operations + const toolName = req.body?.toolName; // Assuming toolName is in the request body + if (toolName && isCriticalOperation(toolName)) { + criticalOperationsLimiter(req, res, () => { + // Continue with the original /mcp logic after criticalOperationsLimiter + const requestTimeout = setTimeout(() => { + if (!res.writableEnded) { + logger.error(`Request timeout for ${req.url}`); + res.status(408).json({ + error: 'Request timeout', + message: 'The request took too long to process' + }); + } + }, config.mcpTimeout); + + mcpStreamableTransport.handleRequest(req, res, req.body) + .then(() => clearTimeout(requestTimeout)) + .catch((error) => { + clearTimeout(requestTimeout); + logger.error(`Error handling Streamable HTTP request for ${req.url}:`, error); + if (!res.writableEnded) { + res.status(500).json({ + error: 'Error processing Streamable HTTP request', + message: error instanceof Error ? error.message : 'Unknown error' + }); + } + }); + }); + } else { + // Original /mcp logic if not a critical operation + const requestTimeout = setTimeout(() => { if (!res.writableEnded) { - res.status(500).json({ - error: 'Error processing Streamable HTTP request', - message: error instanceof Error ? error.message : 'Unknown error' + logger.error(`Request timeout for ${req.url}`); + res.status(408).json({ + error: 'Request timeout', + message: 'The request took too long to process' }); } - }); + }, config.mcpTimeout); + + mcpStreamableTransport.handleRequest(req, res, req.body) + .then(() => clearTimeout(requestTimeout)) + .catch((error) => { + clearTimeout(requestTimeout); + logger.error(`Error handling Streamable HTTP request for ${req.url}:`, error); + if (!res.writableEnded) { + res.status(500).json({ + error: 'Error processing Streamable HTTP request', + message: error instanceof Error ? error.message : 'Unknown error' + }); + } + }); + } }); - app.get('/sse', (req: express.Request, res: express.Response) => { + app.get('/sse', sseLimiter, (req: express.Request, res: express.Response) => { logger.info('New SSE connection request'); if (config.useMultiplexing && multiplexingTransport) { const clientSessionId = randomUUID(); @@ -192,7 +382,7 @@ export function createServer(mcpServer: McpServer, port: number): http.Server { } }); - app.post('/messages', (req: express.Request, res: express.Response) => { + app.post('/messages', messageLimiter, (req: express.Request, res: express.Response) => { try { const sessionIdSchema = z.string().uuid(); const sessionId = sessionIdSchema.parse(req.query.sessionId); @@ -243,6 +433,28 @@ export function createServer(mcpServer: McpServer, port: number): http.Server { res.status(404).json({ error: 'Not found' }); }); + if (process.env.NODE_ENV === 'development') { + app.use((req: Request, res: Response, next: NextFunction) => { + res.on('finish', () => { + const requiredHeaders = [ + 'x-frame-options', + 'x-content-type-options', + 'referrer-policy', + 'content-security-policy' + ]; + + const missingHeaders = requiredHeaders.filter(header => + !res.getHeader(header) + ); + + if (missingHeaders.length > 0) { + logger.warn(`Missing security headers: ${missingHeaders.join(', ')}`); + } + }); + next(); + }); + } + const httpServer = http.createServer(app); httpServer.timeout = config.sseTimeout; httpServer.keepAliveTimeout = config.sseTimeout; From 775278ada85238b1cdf44333e27a0968b4899c78 Mon Sep 17 00:00:00 2001 From: Emanuel Jesus Leiva Navarro Date: Sat, 6 Sep 2025 17:40:05 -0400 Subject: [PATCH 3/9] changes --- .env.example | 10 ++++++++-- package-lock.json | 34 +++++++++++++++++++++++++++++++--- package.json | 1 + src/server.ts | 8 ++++---- 4 files changed, 44 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index e5d8684..9ac93c5 100644 --- a/.env.example +++ b/.env.example @@ -14,6 +14,7 @@ MCP_TIMEOUT=180000 LOG_LEVEL=info # CORS Configuration +# Specifies the allowed origin for CORS. Use '*' for all origins or a specific URL (e.g., https://example.com). CORS_ALLOW_ORIGIN=* # Multiplexing SSE Transport Configuration @@ -29,11 +30,16 @@ RATE_LIMIT_MESSAGES_MAX=30 # Maximum number of messages allowed per minute per I DEFAULT_USER_RATE_LIMIT=1000 # Default number of requests allowed per hour for a user # Security Headers Configuration +# HSTS (HTTP Strict Transport Security) max-age in seconds. Default is 1 year (31536000). HSTS_MAX_AGE=31536000 +# Set to 'true' to only report Content Security Policy (CSP) violations without enforcing them. +# In development, you might want to set this to 'true'. CSP_REPORT_ONLY=false +# URL where CSP violation reports will be sent. CSP_REPORT_URI=https://your-domain.com/csp-report -# For development +# Environment Configuration +# Set to 'development' or 'production'. NODE_ENV=development +# Set to 'true' to disable HSTS, useful for local development without HTTPS. DISABLE_HSTS=true -CSP_REPORT_ONLY=true diff --git a/package-lock.json b/package-lock.json index 2687c33..bcc0232 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "dompurify": "^3.2.6", "dotenv": "^16.5.0", "express": "^5.1.0", + "express-rate-limit": "^8.1.0", "helmet": "^8.1.0", "http-terminator": "^3.2.0", "jsdom": "^26.1.0", @@ -1170,6 +1171,21 @@ "node": ">=18.0.0" } }, + "node_modules/@modelcontextprotocol/sdk/node_modules/express-rate-limit": { + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", + "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "license": "MIT", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -2911,10 +2927,13 @@ } }, "node_modules/express-rate-limit": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.1.tgz", - "integrity": "sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.1.0.tgz", + "integrity": "sha512-4nLnATuKupnmwqiJc27b4dCFmB/T60ExgmtDD7waf4LdrbJ8CPZzZRHYErDYNhoz+ql8fUdYwM/opf90PoPAQA==", "license": "MIT", + "dependencies": { + "ip-address": "10.0.1" + }, "engines": { "node": ">= 16" }, @@ -3527,6 +3546,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ip-address": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", + "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", diff --git a/package.json b/package.json index fc0a987..f7adc39 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "dompurify": "^3.2.6", "dotenv": "^16.5.0", "express": "^5.1.0", + "express-rate-limit": "^8.1.0", "helmet": "^8.1.0", "http-terminator": "^3.2.0", "jsdom": "^26.1.0", diff --git a/src/server.ts b/src/server.ts index 853a8a1..3fb979a 100644 --- a/src/server.ts +++ b/src/server.ts @@ -15,7 +15,7 @@ import { z } from 'zod'; import { config } from '#config/index'; import { logger } from '#core/logger'; import helmet from 'helmet'; -import rateLimit from 'express-rate-limit'; +import { rateLimit } from 'express-rate-limit'; import { Request, Response, NextFunction } from 'express'; declare global { @@ -40,7 +40,7 @@ const generalLimiter = rateLimit({ }, standardHeaders: true, legacyHeaders: false, - handler: (req, res) => { + handler: (req: express.Request, res: express.Response) => { logger.warn(`Rate limit exceeded for IP: ${req.ip} on ${req.method} ${req.url}`); res.status(429).json({ error: 'Rate limit exceeded', @@ -59,7 +59,7 @@ const messageLimiter = rateLimit({ windowMs: 60 * 1000, // 1 minuto max: config.rateLimitMessagesMax, message: 'Too many messages from this IP', // This will be overridden by handler - handler: (req, res) => { + handler: (req: express.Request, res: express.Response) => { logger.warn(`Message Rate limit exceeded for IP: ${req.ip}`); res.status(429).json({ error: 'Rate limit exceeded', @@ -92,7 +92,7 @@ const criticalOperationsLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hora max: 10, // Solo 10 operaciones críticas por hora message: 'Critical operation rate limit exceeded', // This will be overridden by handler - handler: (req, res) => { + handler: (req: express.Request, res: express.Response) => { logger.warn(`Critical operation rate limit exceeded for IP: ${req.ip}`); res.status(429).json({ error: 'Rate limit exceeded', From 228144d1dac53ba654457f317b01454c9b5476bd Mon Sep 17 00:00:00 2001 From: Emanuel Jesus Leiva Navarro Date: Sat, 6 Sep 2025 17:45:27 -0400 Subject: [PATCH 4/9] changes --- src/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server.ts b/src/server.ts index 3b18481..3ec76f3 100644 --- a/src/server.ts +++ b/src/server.ts @@ -14,7 +14,7 @@ import { z } from 'zod'; import { config } from '#config/index'; import { logger } from '#core/logger'; import helmet from 'helmet'; -import { rateLimit } from 'express-rate-limit'; +import rateLimit from 'express-rate-limit'; import express, { Request, Response, NextFunction } from 'express'; declare global { From 7378053704c8308e5a89428bae4cf84c55048ad1 Mon Sep 17 00:00:00 2001 From: Emanuel Jesus Leiva Navarro Date: Sat, 6 Sep 2025 17:51:24 -0400 Subject: [PATCH 5/9] changes --- package-lock.json | 8 -------- 1 file changed, 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4803739..8781880 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3001,10 +3001,6 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.1.0.tgz", "integrity": "sha512-4nLnATuKupnmwqiJc27b4dCFmB/T60ExgmtDD7waf4LdrbJ8CPZzZRHYErDYNhoz+ql8fUdYwM/opf90PoPAQA==", -<<<<<<< HEAD -======= - "dev": true, ->>>>>>> origin/main "license": "MIT", "dependencies": { "ip-address": "10.0.1" @@ -3650,10 +3646,6 @@ "version": "10.0.1", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", -<<<<<<< HEAD -======= - "dev": true, ->>>>>>> origin/main "license": "MIT", "engines": { "node": ">= 12" From 6eadbf33890fcf5d68de4821576348dc39591fca Mon Sep 17 00:00:00 2001 From: Emanuel Jesus Leiva Navarro Date: Sat, 6 Sep 2025 17:53:56 -0400 Subject: [PATCH 6/9] changes --- src/server.ts | 13 ------------- src/types/express.d.ts | 12 ++++++++++++ 2 files changed, 12 insertions(+), 13 deletions(-) create mode 100644 src/types/express.d.ts diff --git a/src/server.ts b/src/server.ts index 3ec76f3..14a9c93 100644 --- a/src/server.ts +++ b/src/server.ts @@ -17,19 +17,6 @@ import helmet from 'helmet'; import rateLimit from 'express-rate-limit'; import express, { Request, Response, NextFunction } from 'express'; -declare global { - namespace Express { - interface Request { - rateLimit?: { - limit: number; - current: number; - remaining: number; - resetTime?: Date; - }; - } - } -} - const generalLimiter = rateLimit({ windowMs: config.rateLimitWindowMs, max: config.rateLimitMaxRequests, diff --git a/src/types/express.d.ts b/src/types/express.d.ts new file mode 100644 index 0000000..c1ff386 --- /dev/null +++ b/src/types/express.d.ts @@ -0,0 +1,12 @@ +import { RateLimitInfo } from 'express-rate-limit'; + +declare global { + namespace Express { + interface Request { + rateLimit?: RateLimitInfo; + user?: any; + } + } +} + +export {}; From 63ecfde59a69fe041e1c421481f6b8e075518555 Mon Sep 17 00:00:00 2001 From: Emanuel Jesus Leiva Navarro Date: Sat, 6 Sep 2025 18:10:50 -0400 Subject: [PATCH 7/9] fix package.json --- .github/dependabot.yml | 35 +++++++++++++++++++++++++++++++++++ package.json | 1 - 2 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..00e610b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,35 @@ +# .github/dependabot.yml +version: 2 +updates: + # Enable version updates for npm + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + day: "monday" + time: "09:00" + open-pull-requests-limit: 10 + reviewers: + - "JesusMaster" + assignees: + - "JesusMaster" + labels: + - "dependencies" + - "security" + commit-message: + prefix: "chore" + prefix-development: "chore" + include: "scope" + + # Security updates (daily check) + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + open-pull-requests-limit: 5 + labels: + - "security" + - "critical" + commit-message: + prefix: "security" + include: "scope" diff --git a/package.json b/package.json index 5683d37..434de6d 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,6 @@ "@types/jsdom": "^21.1.7", "@types/node": "^22.14.1", "eventsource": "^4.0.0", - "express-rate-limit": "^8.1.0", "jest": "^30.0.5", "node-fetch": "^3.3.2", "supertest": "^7.1.4", From 0c5c0861b2de2faaad4df6311404fe84d8293cc1 Mon Sep 17 00:00:00 2001 From: Emanuel Jesus Leiva Navarro Date: Sat, 6 Sep 2025 18:17:27 -0400 Subject: [PATCH 8/9] fix package.json --- README.md | 48 ++++++++++++++--------- package-lock.json | 24 +----------- src/config/index.ts | 6 --- src/server.ts | 88 +++--------------------------------------- src/types/express.d.ts | 25 ++++++------ 5 files changed, 51 insertions(+), 140 deletions(-) diff --git a/README.md b/README.md index 32a19e6..3b25745 100644 --- a/README.md +++ b/README.md @@ -59,12 +59,37 @@ The project follows a modular, feature-based architecture. All source code is lo pnpm install ``` -3. Create a `.env` file by copying the example file: - ```bash - cp .env.example .env +3. Create a `.env` file in the root directory with the following content: + ``` + # GitHub MCP SSE Server Configuration + # GitHub API Token (required for API access) + # Generate a token at https://github.com/settings/tokens + GITHUB_TOKEN=your_github_token_here + + # Server Port Configuration + MCP_SSE_PORT=3200 + + # Timeout Configuration (in milliseconds) + MCP_TIMEOUT=180000 + + # Log Level (debug, info, warn, error) + LOG_LEVEL=info + + # CORS Configuration + CORS_ALLOW_ORIGIN=* + + # Multiplexing SSE Transport Configuration + # Set to 'true' to enable multiplexing SSE transport (handles multiple clients with a single transport) + # Set to 'false' to use individual SSE transport for each client (legacy behavior) + USE_MULTIPLEXING_SSE=false + + # Rate Limiting Configuration + RATE_LIMIT_WINDOW_MS=900000 # Time window for rate limiting in milliseconds (e.g., 900000 for 15 minutes) + RATE_LIMIT_MAX_REQUESTS=100 # Maximum number of requests allowed per window per IP + RATE_LIMIT_SSE_MAX=5 # Maximum number of SSE connections allowed per minute per IP + RATE_LIMIT_MESSAGES_MAX=30 # Maximum number of messages allowed per minute per IP + DEFAULT_USER_RATE_LIMIT=1000 # Default number of requests allowed per hour for a user ``` - The - n, edit the `.env` file to add your GitHub token and customize the other settings as needed. 4. Build the project: ```bash @@ -233,19 +258,6 @@ The server provides the following GitHub API tools: - `get_me` - Get details of the authenticated user -## Security - -This server implements several security best practices to protect against common web vulnerabilities. The security measures are configured in `src/server.ts` and include: - -- **Helmet**: A collection of 12 middleware functions that set various HTTP headers to secure the application. This includes protection against XSS, clickjacking, and other common attacks. -- **Content Security Policy (CSP)**: A policy that helps prevent XSS attacks by specifying which sources of content are allowed to be loaded on a page. -- **HTTP Strict Transport Security (HSTS)**: A policy that forces browsers to use HTTPS for all connections to the server. -- **X-Frame-Options**: A header that prevents the application from being embedded in iframes, which helps prevent clickjacking attacks. -- **X-Content-Type-Options**: A header that prevents browsers from MIME-sniffing a response away from the declared content-type. -- **Referrer-Policy**: A header that controls how much referrer information (sent via the Referer header) should be included with requests. -- **Permissions-Policy**: A header that allows you to control which features and APIs can be used in the browser. - -The security headers can be configured using environment variables. For more information, see the `.env.example` file. ### Rate Limiting This server implements a robust rate limiting strategy to ensure fair usage and protect against abuse. The rate limiting is configured in `src/server.ts` and includes several layers of protection: diff --git a/package-lock.json b/package-lock.json index 8781880..cd2a5b0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,8 +15,6 @@ "dompurify": "^3.2.6", "dotenv": "^16.5.0", "express": "^5.1.0", - "express-rate-limit": "^8.1.0", - "helmet": "^8.1.0", "http-terminator": "^3.2.0", "jsdom": "^26.1.0", "raw-body": "^3.0.0", @@ -26,7 +24,6 @@ "@types/cors": "^2.8.19", "@types/dompurify": "^3.0.5", "@types/express": "^5.0.1", - "@types/helmet": "^0.0.48", "@types/express-rate-limit": "^5.1.3", "@types/jest": "^30.0.0", "@types/jsdom": "^21.1.7", @@ -1408,16 +1405,6 @@ "@types/send": "*" } }, - "node_modules/@types/helmet": { - "version": "0.0.48", - "resolved": "https://registry.npmjs.org/@types/helmet/-/helmet-0.0.48.tgz", - "integrity": "sha512-C7MpnvSDrunS1q2Oy1VWCY7CDWHozqSnM8P4tFeRTuzwqni+PYOjEredwcqWG+kLpYcgLsgcY3orHB54gbx2Jw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/express": "*" - } - }, "node_modules/@types/http-errors": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", @@ -3001,6 +2988,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.1.0.tgz", "integrity": "sha512-4nLnATuKupnmwqiJc27b4dCFmB/T60ExgmtDD7waf4LdrbJ8CPZzZRHYErDYNhoz+ql8fUdYwM/opf90PoPAQA==", + "dev": true, "license": "MIT", "dependencies": { "ip-address": "10.0.1" @@ -3466,15 +3454,6 @@ "node": ">= 0.4" } }, - "node_modules/helmet": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", - "integrity": "sha512-jOiHyAZsmnr8LqoPGmCjYAaiuWwjAPLgY8ZX2XrmHawt99/u1y6RgrZMTeoPfpUbV96HOalYgz1qzkRbw54Pmg==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - } - }, "node_modules/html-encoding-sniffer": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", @@ -3646,6 +3625,7 @@ "version": "10.0.1", "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 12" diff --git a/src/config/index.ts b/src/config/index.ts index 4e505e6..8a50a35 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -29,10 +29,4 @@ export const config = { rateLimitSseMax: process.env.RATE_LIMIT_SSE_MAX ? parseInt(process.env.RATE_LIMIT_SSE_MAX, 10) : 5, // 5 SSE connections per minute rateLimitMessagesMax: process.env.RATE_LIMIT_MESSAGES_MAX ? parseInt(process.env.RATE_LIMIT_MESSAGES_MAX, 10) : 30, // 30 messages per minute defaultUserRateLimit: process.env.DEFAULT_USER_RATE_LIMIT ? parseInt(process.env.DEFAULT_USER_RATE_LIMIT, 10) : 1000, // 1000 requests per hour per user - // Security Headers Configuration - hstsMaxAge: process.env.HSTS_MAX_AGE ? parseInt(process.env.HSTS_MAX_AGE, 10) : 31536000, - cspReportOnly: process.env.CSP_REPORT_ONLY === 'true', - cspReportUri: process.env.CSP_REPORT_URI, - nodeEnv: process.env.NODE_ENV ?? 'development', - disableHsts: process.env.DISABLE_HSTS === 'true', }; diff --git a/src/server.ts b/src/server.ts index 14a9c93..696780f 100644 --- a/src/server.ts +++ b/src/server.ts @@ -13,7 +13,6 @@ import { z } from 'zod'; // Importar configuración y logger centralizados import { config } from '#config/index'; import { logger } from '#core/logger'; -import helmet from 'helmet'; import rateLimit from 'express-rate-limit'; import express, { Request, Response, NextFunction } from 'express'; @@ -26,11 +25,11 @@ const generalLimiter = rateLimit({ }, standardHeaders: true, legacyHeaders: false, - handler: (req: express.Request, res: express.Response) => { + handler: (req, res) => { logger.warn(`Rate limit exceeded for IP: ${req.ip} on ${req.method} ${req.url}`); res.status(429).json({ error: 'Rate limit exceeded', - retryAfter: Math.ceil((req.rateLimit?.resetTime?.getTime() || Date.now()) / 1000) + retryAfter: Math.ceil((req.rateLimit?.resetTime?.getTime() ?? Date.now()) / 1000) }); } }); @@ -45,7 +44,7 @@ const messageLimiter = rateLimit({ windowMs: 60 * 1000, // 1 minuto max: config.rateLimitMessagesMax, message: 'Too many messages from this IP', // This will be overridden by handler - handler: (req: express.Request, res: express.Response) => { + handler: (req, res) => { logger.warn(`Message Rate limit exceeded for IP: ${req.ip}`); res.status(429).json({ error: 'Rate limit exceeded', @@ -57,8 +56,7 @@ const messageLimiter = rateLimit({ const createUserLimiter = () => rateLimit({ windowMs: 60 * 60 * 1000, // 1 hora max: (req: Request) => { - // @ts-ignore - return req.user?.rateLimits?.requestsPerHour || config.defaultUserRateLimit; + return req.user?.rateLimits?.requestsPerHour ?? config.defaultUserRateLimit; }, message: 'User rate limit exceeded' }); @@ -78,7 +76,7 @@ const criticalOperationsLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hora max: 10, // Solo 10 operaciones críticas por hora message: 'Critical operation rate limit exceeded', // This will be overridden by handler - handler: (req: express.Request, res: express.Response) => { + handler: (req, res) => { logger.warn(`Critical operation rate limit exceeded for IP: ${req.ip}`); res.status(429).json({ error: 'Rate limit exceeded', @@ -132,57 +130,6 @@ export function closeAllSseConnections() { export function createServer(mcpServer: McpServer, port: number): http.Server { const app = express(); - app.use(helmet({ - contentSecurityPolicy: { - directives: { - defaultSrc: ["'self'"], - styleSrc: ["'self'", "'unsafe-inline'"], - scriptSrc: ["'self'"], - imgSrc: ["'self'", "data:", "https:"], - connectSrc: ["'self'"], - fontSrc: ["'self'"], - objectSrc: ["'none'"], - mediaSrc: ["'self'"], - frameSrc: ["'none'"], - frameAncestors: ["'none'"], - baseUri: ["'self'"], - formAction: ["'self'"], - reportUri: config.cspReportUri || '', - }, - reportOnly: config.cspReportOnly, - }, - crossOriginEmbedderPolicy: false, // Para SSE compatibility - hsts: config.disableHsts ? false : { - maxAge: config.hstsMaxAge, // 1 año - includeSubDomains: true, - preload: true - } - })); - - app.use((req, res, next) => { - // Prevenir que la respuesta sea embebida en iframes - res.setHeader('X-Frame-Options', 'DENY'); - - // Prevenir MIME type sniffing - res.setHeader('X-Content-Type-Options', 'nosniff'); - - // Control de referrer - res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin'); - - // Política de permisos - res.setHeader('Permissions-Policy', - 'camera=(), microphone=(), geolocation=(), payment=()'); - - // Headers para SSE - if (req.path === '/sse') { - res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); - res.setHeader('Pragma', 'no-cache'); - res.setHeader('Expires', '0'); - } - - next(); - }); - app.use(cors({ origin: config.corsAllowOrigin, methods: ['GET', 'POST', 'OPTIONS'], @@ -205,9 +152,6 @@ export function createServer(mcpServer: McpServer, port: number): http.Server { app.use(rateLimitMonitor); // Aplicar el monitor después del limiter app.get('/health', (req: express.Request, res: express.Response) => { - res.setHeader('Cache-Control', 'no-cache, max-age=0'); - res.setHeader('X-Robots-Tag', 'noindex, nofollow'); - res.status(200).json({ status: 'ok', timestamp: new Date().toISOString(), @@ -419,28 +363,6 @@ export function createServer(mcpServer: McpServer, port: number): http.Server { res.status(404).json({ error: 'Not found' }); }); - if (process.env.NODE_ENV === 'development') { - app.use((req: Request, res: Response, next: NextFunction) => { - res.on('finish', () => { - const requiredHeaders = [ - 'x-frame-options', - 'x-content-type-options', - 'referrer-policy', - 'content-security-policy' - ]; - - const missingHeaders = requiredHeaders.filter(header => - !res.getHeader(header) - ); - - if (missingHeaders.length > 0) { - logger.warn(`Missing security headers: ${missingHeaders.join(', ')}`); - } - }); - next(); - }); - } - const httpServer = http.createServer(app); httpServer.timeout = config.sseTimeout; httpServer.keepAliveTimeout = config.sseTimeout; diff --git a/src/types/express.d.ts b/src/types/express.d.ts index c1ff386..74d67cd 100644 --- a/src/types/express.d.ts +++ b/src/types/express.d.ts @@ -1,12 +1,15 @@ -import { RateLimitInfo } from 'express-rate-limit'; - -declare global { - namespace Express { - interface Request { - rateLimit?: RateLimitInfo; - user?: any; - } - } +declare namespace Express { + export interface Request { + rateLimit?: { + limit: number; + current: number; + remaining: number; + resetTime?: Date; + }; + user?: { + rateLimits?: { + requestsPerHour?: number; + }; + }; + } } - -export {}; From 49b37429d7d2f0f86bf2478549d432f239914c7c Mon Sep 17 00:00:00 2001 From: Emanuel Jesus Leiva Navarro Date: Sat, 6 Sep 2025 18:20:26 -0400 Subject: [PATCH 9/9] fix package.json --- src/server.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/server.ts b/src/server.ts index 696780f..fe7439f 100644 --- a/src/server.ts +++ b/src/server.ts @@ -86,8 +86,8 @@ const criticalOperationsLimiter = rateLimit({ }); const rateLimitMonitor = (req: Request, res: Response, next: NextFunction) => { - const remaining = req.rateLimit?.remaining || 0; - const total = req.rateLimit?.limit || 0; + const remaining = req.rateLimit?.remaining ?? 0; + const total = req.rateLimit?.limit ?? 0; if (remaining > 0 && remaining < total * 0.1) { logger.warn(`Rate limit warning for ${req.ip} on ${req.method} ${req.url}: ${remaining}/${total} remaining`);