Guide · MCP Security
MCP server security headers
HTTP security headers are one-line defenses against entire categories of web attacks. Content-Security-Policy blocks XSS by restricting which scripts execute on your server's web UI. Strict-Transport-Security prevents HTTPS downgrade attacks. X-Frame-Options blocks clickjacking. These headers are cheap to add — a single middleware call or a few Caddy directives — and they significantly raise the cost of exploiting any XSS or injection bug your server might harbor. For MCP servers with web-facing endpoints or a status dashboard UI, they are a baseline requirement.
TL;DR
Add helmet() to your Express-based MCP server before any route handlers. Configure a Content-Security-Policy that restricts script sources to 'self' and explicitly listed CDNs. Set Strict-Transport-Security: max-age=31536000; includeSubDomains. Use X-Frame-Options: DENY unless you embed your UI in iframes. If your MCP server is behind Caddy (as on the factory VPS), add header directives in your Caddyfile instead — Caddy is your TLS terminator and injects headers before your Node process ever sees the request.
Security header reference
| Header | Protects against | Recommended value |
|---|---|---|
Content-Security-Policy |
XSS, data injection, malicious script loading | default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none' |
Strict-Transport-Security |
HTTPS downgrade, SSL stripping | max-age=31536000; includeSubDomains |
X-Frame-Options |
Clickjacking | DENY (or SAMEORIGIN if you embed your own UI) |
X-Content-Type-Options |
MIME type sniffing | nosniff |
Referrer-Policy |
URL leakage in Referer header | strict-origin-when-cross-origin |
Permissions-Policy |
Browser API abuse (camera, geolocation, payment) | camera=(), microphone=(), geolocation=(), payment=() |
Cross-Origin-Resource-Policy |
Cross-origin data reads | same-origin for API routes; cross-origin for public assets |
Cross-Origin-Opener-Policy |
Cross-origin window access, Spectre side-channel | same-origin |
Helmet setup for Express-based MCP servers
Helmet is a collection of small Express middleware functions that set security headers. Install it with npm install helmet:
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
const app = express();
// helmet() MUST come before your route handlers and before cors()
// helmet sets conservative defaults; override CSP to match your app's needs
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"], // No inline scripts, no external CDN
styleSrc: ["'self'", "'unsafe-inline'"], // Allow inline styles (common in status pages)
imgSrc: ["'self'", 'data:'], // data: for base64 inline images
connectSrc: ["'self'"], // AJAX/fetch only to your own origin
fontSrc: ["'self'"],
objectSrc: ["'none'"], // Block plugins (Flash etc.)
frameAncestors: ["'none'"], // Prevents embedding in iframes (clickjacking)
upgradeInsecureRequests: [], // Rewrite http:// links to https://
},
},
hsts: {
maxAge: 31_536_000, // 1 year in seconds
includeSubDomains: true,
preload: false, // Set preload: true only once you're ready for HSTS preload list submission
},
frameguard: { action: 'deny' },
noSniff: true, // X-Content-Type-Options: nosniff
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
permittedCrossDomainPolicies: false, // Block Adobe/Flash cross-domain
crossOriginResourcePolicy: { policy: 'same-origin' },
crossOriginOpenerPolicy: { policy: 'same-origin' },
}));
// CORS after helmet (CORS manages its own Allow-Origin headers)
app.use(cors(corsOptions));
Content-Security-Policy in depth
CSP is the most impactful and most complex security header. It instructs the browser on which sources are trusted for each type of resource. A strict CSP stops XSS from being exploitable even when an attacker can inject arbitrary HTML — injected scripts from external domains are blocked by the browser before they execute.
Common directives for MCP server status pages and web UIs:
// For an MCP server with a React/Vue frontend loading from a CDN:
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", 'https://cdn.example.com'], // Allow specific CDN
styleSrc: ["'self'", 'https://fonts.googleapis.com', "'unsafe-inline'"],
fontSrc: ["'self'", 'https://fonts.gstatic.com'],
imgSrc: ["'self'", 'data:', 'https://avatars.githubusercontent.com'],
connectSrc: ["'self'", 'https://api.yourserver.com'],
workerSrc: ["'none'"],
frameAncestors: ["'none'"],
reportUri: ['/csp-report'], // Collect CSP violations for debugging
},
}
Start with a report-only policy (Content-Security-Policy-Report-Only) to collect violations without blocking anything, then tighten the policy once you understand what your app actually loads:
// Report-only mode: log violations but don't block (for gradual rollout)
app.use((req, res, next) => {
res.setHeader(
'Content-Security-Policy-Report-Only',
"default-src 'self'; report-uri /csp-report"
);
next();
});
app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => {
console.log('CSP violation:', JSON.stringify(req.body));
res.status(204).end();
});
Configuring security headers in Caddy
If your MCP server is behind Caddy (the factory VPS setup), Caddy can set security headers at the reverse-proxy layer — before requests hit your Node process. This is cleaner for static sites and works even if your backend crashes:
alivemcp.com {
# TLS is automatic via Let's Encrypt
tls {
on_demand
}
# Security headers — applied to all responses
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()"
Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none'"
# Remove headers that leak server info
-Server
-X-Powered-By
}
# Static files
root * /srv/sites/agent-12
# Proxy API routes to Node backend
handle /api/* {
reverse_proxy localhost:3000
}
# Serve static files
file_server
}
Caddy's header directive with a - prefix removes a header rather than setting it. -Server removes Caddy's own server identification header; -X-Powered-By removes Node/Express fingerprinting if your backend sets it.
HSTS and preloading
Strict-Transport-Security tells browsers to always use HTTPS for your domain for max-age seconds, even if the user types http://. The browser caches this instruction — subsequent visits never attempt HTTP at all, eliminating SSL-stripping attacks on cached visitors.
The HSTS preload list goes further: browsers ship with a baked-in list of domains that are always HTTPS. To be included:
- Your domain and all subdomains must have a valid HTTPS certificate
- You must set
max-age >= 31536000(1 year) - You must include
includeSubDomainsandpreloadin the HSTS header - Submit at hstspreload.org
Do not set preload: true until you're certain all your subdomains support HTTPS — the preload list is very hard to remove from once submitted.
Testing security headers
Use curl to inspect the headers your server actually sends:
curl -s -I https://alivemcp.com | grep -E "(Content-Security|Strict-Transport|X-Frame|X-Content|Referrer|Permissions)"
# Expected output:
# content-security-policy: default-src 'self'; script-src 'self'; ...
# strict-transport-security: max-age=31536000; includeSubDomains
# x-frame-options: DENY
# x-content-type-options: nosniff
# referrer-policy: strict-origin-when-cross-origin
# permissions-policy: camera=(), microphone=(), ...
For a comprehensive automated check, securityheaders.com grades your headers and flags missing or misconfigured ones. Run it after every deploy that touches your Caddyfile or middleware configuration.
Security headers and MCP server uptime
Security headers don't affect uptime directly, but they protect the web UI and status pages that users consult when your server is experiencing issues. An XSS vulnerability in your status page — compromised by a missing CSP — could display false status information or steal session tokens from users checking on an outage. AliveMCP monitors the transport-layer health of your MCP endpoints; pair that with solid security headers to protect the information layer users see.
Further reading
- MCP server CORS configuration — origin allowlist and preflight hardening
- MCP server authentication — JWT, API keys, and session verification
- MCP server security monitoring — threat detection and alerting
- MCP server audit logging — capture and query tool call records
- MCP server SSRF prevention — block private IP access from tool handlers
- MCP server SSL certificate — HTTPS setup with Let's Encrypt and Caddy
- MCP server Nginx configuration — security headers and reverse proxy
- AliveMCP — uptime monitoring for HTTP-deployed MCP servers