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 typeRiskRecommended disposition
text/htmlXSS — always executed as HTMLattachment always, or serve from sandboxed subdomain
text/javascript, application/javascriptScript executionattachment
image/svg+xmlSVG can contain inline scriptsattachment or serve with sandbox iframe
text/plainSome browsers render <html> in text/plainattachment for user-uploaded files
application/pdfPDF JavaScript executioninline acceptable — PDF JS is sandboxed in modern browsers
image/png, image/jpegLow — but verify with file-type libraryinline 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

CRITICAL −22User-uploaded files served from the main application domain without X-Content-Type-Options: nosniff — polyglot files can be executed as HTML in the application's origin, enabling cookie and session token theft
HIGH −18File serving uses uploader-declared Content-Type without server-side validation — attacker declares Content-Type: image/png for an HTML file; nosniff is bypassed because the declared type is honored
HIGH −16SVG files served with Content-Disposition: inline — SVG supports inline <script> elements; inline SVG rendered in browser executes attacker scripts in the file-serving origin
MEDIUM −12X-Content-Type-Options: nosniff present on API responses but absent on file download routes — file-serving paths are the actual attack surface for MIME sniffing
MEDIUM −8User-uploaded files served from same origin as authenticated MCP UI — even correctly typed files create exfiltration risk; separate origin (subdomain) isolates upload storage from session data

See also: SVG injection · Multipart upload security · PDF parsing security

Run a free SkillAudit on your MCP server →