@taazkareem/clickup-mcp-server
Version:
ClickUp MCP Server - Integrate ClickUp tasks with AI through Model Context Protocol
232 lines (231 loc) • 7.87 kB
JavaScript
/**
* SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
* SPDX-License-Identifier: MIT
*
* Security Middleware for ClickUp MCP Server
*
* This module provides optional security enhancements that can be enabled
* without breaking existing functionality. All security features are opt-in
* to maintain backwards compatibility.
*/
import rateLimit from 'express-rate-limit';
import cors from 'cors';
import config from '../config.js';
import { Logger } from '../logger.js';
const logger = new Logger('Security');
/**
* Origin validation middleware - validates Origin header against whitelist
* Only enabled when ENABLE_ORIGIN_VALIDATION=true
*/
export function createOriginValidationMiddleware() {
return (req, res, next) => {
if (!config.enableOriginValidation) {
next();
return;
}
const origin = req.headers.origin;
const referer = req.headers.referer;
// For non-browser requests (like n8n, MCP Inspector), origin might be undefined
// In such cases, we allow the request but log it for monitoring
if (!origin && !referer) {
logger.debug('Request without Origin/Referer header - allowing (likely non-browser client)', {
userAgent: req.headers['user-agent'],
ip: req.ip,
path: req.path
});
next();
return;
}
// Check if origin is in allowed list
if (origin && !config.allowedOrigins.includes(origin)) {
logger.warn('Blocked request from unauthorized origin', {
origin,
ip: req.ip,
path: req.path,
userAgent: req.headers['user-agent']
});
res.status(403).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Forbidden: Origin not allowed'
},
id: null
});
return;
}
// If referer is present, validate it too
if (referer) {
try {
const refererOrigin = new URL(referer).origin;
if (!config.allowedOrigins.includes(refererOrigin)) {
logger.warn('Blocked request from unauthorized referer', {
referer,
refererOrigin,
ip: req.ip,
path: req.path
});
res.status(403).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Forbidden: Referer not allowed'
},
id: null
});
return;
}
}
catch (error) {
logger.warn('Invalid referer URL', { referer, error: error.message });
// Continue processing if referer is malformed
}
}
logger.debug('Origin validation passed', { origin, referer });
next();
};
}
/**
* Rate limiting middleware - protects against DoS attacks
* Only enabled when ENABLE_RATE_LIMIT=true
*/
export function createRateLimitMiddleware() {
if (!config.enableRateLimit) {
return (_req, _res, next) => next();
}
return rateLimit({
windowMs: config.rateLimitWindowMs,
max: config.rateLimitMax,
message: {
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Too many requests, please try again later'
},
id: null
},
standardHeaders: true,
legacyHeaders: false,
handler: (req, res) => {
logger.warn('Rate limit exceeded', {
ip: req.ip,
path: req.path,
userAgent: req.headers['user-agent']
});
res.status(429).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Too many requests, please try again later'
},
id: null
});
}
});
}
/**
* CORS middleware - configures cross-origin resource sharing
* Only enabled when ENABLE_CORS=true
*/
export function createCorsMiddleware() {
if (!config.enableCors) {
return (_req, _res, next) => next();
}
return cors({
origin: (origin, callback) => {
// Allow requests with no origin (like mobile apps, Postman, etc.)
if (!origin)
return callback(null, true);
if (config.allowedOrigins.includes(origin)) {
callback(null, true);
}
else {
logger.warn('CORS blocked origin', { origin });
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'mcp-session-id', 'Authorization'],
exposedHeaders: ['mcp-session-id']
});
}
/**
* Security headers middleware - adds security-related HTTP headers
* Only enabled when ENABLE_SECURITY_FEATURES=true
*/
export function createSecurityHeadersMiddleware() {
return (req, res, next) => {
if (!config.enableSecurityFeatures) {
return next();
}
// Add 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('Referrer-Policy', 'strict-origin-when-cross-origin');
// Only add HSTS for HTTPS
if (req.secure || req.headers['x-forwarded-proto'] === 'https') {
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
}
logger.debug('Security headers applied');
next();
};
}
/**
* Request logging middleware for security monitoring
*/
export function createSecurityLoggingMiddleware() {
return (req, res, next) => {
if (!config.enableSecurityFeatures) {
return next();
}
const startTime = Date.now();
res.on('finish', () => {
const duration = Date.now() - startTime;
const logData = {
method: req.method,
path: req.path,
statusCode: res.statusCode,
duration,
ip: req.ip,
userAgent: req.headers['user-agent'],
origin: req.headers.origin,
sessionId: req.headers['mcp-session-id']
};
if (res.statusCode >= 400) {
logger.warn('HTTP error response', logData);
}
else {
logger.debug('HTTP request completed', logData);
}
});
next();
};
}
/**
* Input validation middleware - validates request size and content
*/
export function createInputValidationMiddleware() {
return (req, res, next) => {
// Always enforce reasonable request size limits
const contentLength = req.headers['content-length'];
if (contentLength && parseInt(contentLength) > 50 * 1024 * 1024) { // 50MB hard limit
logger.warn('Request too large', {
contentLength,
ip: req.ip,
path: req.path
});
res.status(413).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Request entity too large'
},
id: null
});
return;
}
next();
};
}