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:

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:

  1. User authenticates to https://mcp.corp.internal/ — server sets a session cookie mcp_session=abc123
  2. User's Claude client sends tool call requests to POST https://mcp.corp.internal/tools/call with the session cookie
  3. 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 →