Topic: mcp server CSRF security
MCP server CSRF security — cross-site request forgery on HTTP transport
MCP servers that run over HTTP (rather than stdio) expose their tool endpoints to cross-site request forgery attacks. A malicious web page visited by a user with an active MCP session can silently trigger state-changing tool calls — deleting files, sending messages, modifying records — using the victim's own session credentials. The attack requires no code vulnerability in the MCP server itself: it exploits the browser's automatic credential forwarding behavior.
When CSRF applies to MCP servers
CSRF is relevant to MCP deployments where:
- The MCP server runs as an HTTP service accessible from a browser context (HTTP or Streamable HTTP transport, not stdio-only)
- Authentication is maintained via session cookies, HTTP Basic credentials stored in the browser, or other browser-forwarded credentials
- The server exposes state-changing tool endpoints (write operations, delete operations, message sends, code execution)
MCP servers deployed as local stdio processes (the most common Claude Code pattern) are not vulnerable to CSRF because there is no HTTP endpoint for a cross-origin request to target. But MCP servers deployed as shared team infrastructure — a self-hosted HTTP server behind a corporate SSO, a development server accessible on localhost with browser-forwarded cookies — are fully in scope.
The attack mechanism
A typical MCP HTTP server session flow:
- User authenticates to
https://mcp.corp.internal/— server sets a session cookiemcp_session=abc123 - User's Claude client sends tool call requests to
POST https://mcp.corp.internal/tools/callwith the session cookie - Server validates the session cookie and executes the tool
The attacker constructs a malicious web page:
<!-- Attacker's page at https://attacker.example -->
<script>
// Triggers automatically when the page loads
fetch('https://mcp.corp.internal/tools/call', {
method: 'POST',
credentials: 'include', // forwards the victim's mcp_session cookie
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
jsonrpc: '2.0',
id: 1,
method: 'tools/call',
params: {
name: 'delete_document',
arguments: { path: 'important-contracts/2026/' }
}
})
});
</script>
When the victim visits attacker.example while their MCP session cookie is valid, the browser sends the fetch request to mcp.corp.internal including the mcp_session cookie. The MCP server receives a valid authenticated request and executes the tool call.
CORS same-origin policy prevents the attacker's page from reading the response, but it does not prevent the browser from sending the request. The damage (the deletion, the message, the write) is done when the request is sent, not when the response is read.
CSRF via simple form submission (no CORS preflight)
The fetch with credentials: 'include' triggers a CORS preflight if the request uses non-simple headers like Content-Type: application/json. The server can (and should) reject the preflight via CORS policy. But a classic HTML form submission does not trigger a preflight:
<form action="https://mcp.corp.internal/tools/call" method="POST" id="csrf-form">
<input type="hidden" name="tool" value="send_email">
<input type="hidden" name="to" value="attacker@external.example">
<input type="hidden" name="body" value="Forwarded: all internal documents">
</form>
<script>document.getElementById('csrf-form').submit();</script>
If the MCP server accepts application/x-www-form-urlencoded bodies (some implementations do as a convenience) and uses cookie-based auth, this fires without a preflight and without the victim seeing any visible action.
Defense 1: SameSite cookie attribute
The most effective modern defense is setting the session cookie with SameSite=Strict or SameSite=Lax:
// Set MCP session cookie with SameSite protection
res.cookie('mcp_session', sessionToken, {
httpOnly: true,
secure: true,
sameSite: 'strict', // browser will NOT send this cookie on cross-site requests
// SameSite=lax is also acceptable: allows safe top-level navigations,
// blocks cross-site POST requests and subresource requests
});
SameSite=Strict prevents the session cookie from being sent on any cross-origin request, including the fetch and form submission patterns above. SameSite=Lax is slightly more permissive (allows top-level navigations) but still blocks the attacks shown. Both are effective defenses against standard CSRF.
SameSite=None — which is required for legitimate cross-site cookie use — must be paired with explicit CSRF token verification (see Defense 3) since it opts the cookie back into cross-origin forwarding.
Defense 2: Origin and Referer header validation
For state-changing requests, validate that the Origin header (always present in cross-site requests from modern browsers) matches an allowlist of legitimate origins:
function requireSameOrigin(req, res, next) {
const origin = req.headers['origin'];
const referer = req.headers['referer'];
const ALLOWED_ORIGINS = ['https://mcp.corp.internal'];
if (origin && !ALLOWED_ORIGINS.includes(origin)) {
return res.status(403).json({ error: 'CSRF: Origin not allowed' });
}
if (!origin && referer) {
const refererOrigin = new URL(referer).origin;
if (!ALLOWED_ORIGINS.includes(refererOrigin)) {
return res.status(403).json({ error: 'CSRF: Referer not allowed' });
}
}
if (!origin && !referer) {
// No origin or referer — reject for state-changing operations
return res.status(403).json({ error: 'CSRF: Missing origin headers' });
}
next();
}
app.post('/tools/call', requireSameOrigin, toolCallHandler);
Origin header validation is effective against browser-based CSRF because the browser sets and protects the Origin header — scripts cannot override it. Note: direct server-to-server calls (legitimate MCP clients) send no Origin header, so the "no origin → reject" rule must be tuned for deployments that need to support both browser and server clients.
Defense 3: CSRF token for legacy deployments
For deployments that cannot set SameSite cookies and serve a web-based MCP client, use synchronized CSRF tokens:
// Issue a CSRF token on session creation
app.post('/auth/login', async (req, res) => {
const sessionToken = generateSessionToken();
const csrfToken = crypto.randomBytes(32).toString('hex');
await storeSession(sessionToken, { userId: user.id, csrfToken });
res.cookie('mcp_session', sessionToken, { httpOnly: true, secure: true });
res.json({ csrfToken }); // return to client for inclusion in subsequent requests
});
// Validate CSRF token on every state-changing tool call
app.post('/tools/call', async (req, res) => {
const session = await loadSession(req.cookies.mcp_session);
const csrfToken = req.headers['x-csrf-token'] || req.body._csrf;
if (!csrfToken || !timingSafeEqual(
Buffer.from(csrfToken),
Buffer.from(session.csrfToken)
)) {
return res.status(403).json({ error: 'Invalid CSRF token' });
}
// proceed with tool call
});
The CSRF token is stored in JavaScript-accessible state (returned in the JSON response body, not in an HttpOnly cookie) so that the legitimate MCP client can include it as a custom header (X-CSRF-Token). A cross-origin attacker's page cannot read this token due to same-origin policy on the login response, so it cannot include the correct token in the forged request.
SkillAudit's security check for MCP servers running over HTTP transport flags the absence of SameSite cookie attributes on session cookies, the absence of Origin/Referer validation on state-changing endpoints, and the absence of CSRF token mechanisms. Servers that operate stdio-only are excluded from this check.
Check whether your HTTP-transport MCP server is protected against CSRF.
Run a free audit →