MCP Server Security · MIME Sniffing
MCP server content-type sniffing security — MIME type sniffing, X-Content-Type-Options, polyglot file attacks, and Content-Disposition for safe file serving in MCP servers
MIME type sniffing is a browser behavior where, when a server returns a resource without a Content-Type header (or with an incorrect one), the browser inspects the first bytes of the response body to guess the content type. An MCP server that serves user-uploaded files or tool-generated output without proper Content-Type and X-Content-Type-Options: nosniff headers is vulnerable to polyglot file attacks: a file that is simultaneously a valid image (or PDF) and valid HTML — which browsers will execute as HTML when sniffing determines that's what it "really" is.
How MIME sniffing enables XSS from non-HTML files
Internet Explorer pioneered aggressive MIME sniffing as a compatibility feature. Modern browsers have reduced sniffing, but it still applies in specific contexts. The dangerous case: a file starting with <!-- or HTML-looking bytes is served with Content-Type: image/png but no X-Content-Type-Options: nosniff. Chrome and Firefox will still serve the declared MIME type for explicit <img> tags, but if the URL is navigated to directly (which happens when a user opens the file link in a new tab), older or non-standard sniffing logic may kick in and render the HTML.
The canonical polyglot attack constructs a file that satisfies two parsers simultaneously:
// A valid PNG that also contains executable HTML
// PNG magic bytes keep image parsers happy
// Browser navigating to the file URL sees HTML before the binary PNG data corrupts rendering
const polyglot = Buffer.concat([
// PNG header (8 bytes)
Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]),
// PNG IHDR chunk — minimal valid PNG structure
Buffer.from([0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52]),
Buffer.from([0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01]),
Buffer.from([0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53, 0xDE]),
// HTML payload appended after valid PNG structure
// When browser navigates to file URL and sniffs, the HTML tag triggers HTML rendering
Buffer.from('<script>alert(document.cookie)</script>'),
]);
// If served from your domain without nosniff: XSS with your origin's cookies
MCP servers that accept file uploads from tools are high-value targets for polyglot attacks. A tool that writes a user-provided file to disk and then serves it back via a URL on the MCP server's domain is vulnerable. The attacker doesn't need server-side code execution — they just need to get their polyglot file stored and then trick the user into navigating to its URL.
X-Content-Type-Options: nosniff
The primary defense is sending X-Content-Type-Options: nosniff on all responses that serve files. This header instructs browsers to use only the declared Content-Type header and never to sniff the body. With nosniff set, a file served as image/png will never be executed as HTML regardless of its content.
// Express middleware: apply nosniff to all file-serving routes
app.use('/files', (req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
next();
});
// Or globally for all responses (recommended):
app.use((req, res, next) => {
res.setHeader('X-Content-Type-Options', 'nosniff');
next();
});
// File serving with explicit Content-Type based on validated file type
app.get('/files/:fileId', async (req, res) => {
const file = await getFileMetadata(req.params.fileId);
// Validate the stored MIME type — use file-type library to detect actual type
// Do not trust the Content-Type the uploader declared
const { fileTypeFromBuffer } = await import('file-type');
const buffer = await readFile(file.storagePath);
const detected = await fileTypeFromBuffer(buffer);
// Only serve safe MIME types directly; force download for everything else
const SAFE_INLINE_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp', 'application/pdf']);
const contentType = detected?.mime ?? 'application/octet-stream';
res.setHeader('X-Content-Type-Options', 'nosniff');
res.setHeader('Content-Type', contentType);
if (!SAFE_INLINE_TYPES.has(contentType)) {
// Force download for any type that could be interpreted as executable
res.setHeader('Content-Disposition', `attachment; filename="${file.originalName}"`);
} else {
// For safe types, still set Content-Disposition: inline to be explicit
res.setHeader('Content-Disposition', `inline; filename="${file.originalName}"`);
}
res.send(buffer);
});
Content-Disposition: attachment for unsafe types
Content-Disposition: attachment instructs the browser to download the file rather than display it inline. For any content type that browsers can execute (HTML, JavaScript, SVG, XML, text/plain), serving with attachment disposition ensures the content is never rendered in the browser — it's written to disk as a download. Even if the MIME type is sniffed as HTML, attachment disposition prevents in-browser rendering.
| Content type | Risk | Recommended disposition |
|---|---|---|
text/html | XSS — always executed as HTML | attachment always, or serve from sandboxed subdomain |
text/javascript, application/javascript | Script execution | attachment |
image/svg+xml | SVG can contain inline scripts | attachment or serve with sandbox iframe |
text/plain | Some browsers render <html> in text/plain | attachment for user-uploaded files |
application/pdf | PDF JavaScript execution | inline acceptable — PDF JS is sandboxed in modern browsers |
image/png, image/jpeg | Low — but verify with file-type library | inline with verified MIME type |
Serve user-uploaded files from a separate subdomain or domain. Even with perfect nosniff and Content-Disposition headers, a stored XSS from a file served on skillaudit.dev gives the attacker access to all skillaudit.dev cookies and localStorage. Serving uploads from user-content.skillaudit.dev (a separate origin with no cookies or sessions) limits the blast radius: XSS in the upload subdomain gets nothing of value.
SkillAudit findings for MIME sniffing in MCP servers
X-Content-Type-Options: nosniff — polyglot files can be executed as HTML in the application's origin, enabling cookie and session token theftContent-Type: image/png for an HTML file; nosniff is bypassed because the declared type is honoredContent-Disposition: inline — SVG supports inline <script> elements; inline SVG rendered in browser executes attacker scripts in the file-serving originX-Content-Type-Options: nosniff present on API responses but absent on file download routes — file-serving paths are the actual attack surface for MIME sniffingSee also: SVG injection · Multipart upload security · PDF parsing security