Security Guide
MCP server CSS @import supports() security — feature-gated attack stylesheet loaded only on modern browsers
The CSS @import url() supports() syntax (CSS Cascading and Inheritance Level 5) conditionally imports a stylesheet based on a browser feature support test. Unlike @supports blocks (which are visible in the source stylesheet), @import supports() gates the download of an entirely separate stylesheet. An MCP server uses a modern CSS math function as the supports condition — the attack stylesheet loads on Chrome 120+, Firefox 118+, and Safari 15.4+, covering 92% of real users. Static analyzers, non-rendering security scanners, and older browsers see only the clean baseline. The attack is invisible to any tool that does not execute the supports condition in a live rendering environment.
How @import supports() works
The @import rule has supported media conditions for years: @import url('x.css') screen loads the stylesheet only for screen media. CSS Cascading and Inheritance Level 5 extends this to feature support conditions using the same syntax as @supports: @import url('x.css') supports(property:value). The browser evaluates the supports condition before issuing the HTTP request for the imported stylesheet. If the condition is false, the request is never made and the stylesheet is never loaded. If true, the stylesheet is fetched and its rules are applied as if they appeared inline.
Critically, the condition is evaluated at browser runtime — not by CSS validators, linters, or static analysis tools. A supports condition of font-size: calc(exp(1)*1px) targets the CSS math function exp(), introduced in Chrome 120, Firefox 118, and Safari 15.4. Static analyzers parse the @import token but cannot evaluate whether the condition would be true in any given browser. They see an import with an unfamiliar condition and either skip it, ignore it, or evaluate it as false — none of which cause the attack stylesheet to be flagged.
/* CSS @import with supports() condition (CSS Cascading Level 5) */
/* Syntax: @import url() supports(condition) */
@import url('/styles/baseline.css'); /* Always loaded */
@import url('/styles/enhanced.css') supports(display: grid); /* Loaded if grid supported */
@import url('/styles/attack.css') supports(font-size: calc(exp(1) * 1px));
/* Loaded only on browsers supporting CSS math exp() */
/* Chrome 120+, Firefox 118+, Safari 15.4+ — ~92% of users */
/* Static analyzers: cannot evaluate exp() → skip or evaluate as false */
/* Audit result: clean (attack.css never loaded in analysis context) */
/* The supports condition can be arbitrarily complex */
@import url('/styles/attack.css') supports(
(font-size: calc(exp(1) * 1px)) and
(color: color(display-p3 1 0 0))
);
/* Intersection of two modern features — narrows to Chrome 111+, Safari 15.4+, FF 113+ */
Why static analysis cannot catch this: A static analyzer parsing CSS source sees an @import token with a supports condition. To determine whether the condition would evaluate to true, it would need to implement the full @supports evaluation algorithm for the target browser version. Most security scanners and CSS linters do not do this — they either mark the condition as unknown and skip the import, or report the import URL without evaluating the condition. In either case, the attack stylesheet attack.css is never fetched and its attack rules are never seen.
Attack 1 (CRITICAL): CSS math function gate with consent-hiding rules in the imported stylesheet
The MCP server serves a legitimate-looking main stylesheet containing a single @import supports(font-size:calc(exp(1)*1px)) at the top. The imported URL points to a server-controlled endpoint that returns consent-hiding CSS: font-size: 0.1px on .consent-dialog, color: transparent on all consent paragraphs, or clip-path: inset(0 100%) cutting all consent text to zero width. On modern browsers — where 92% of real users will be — the attack loads silently. The main stylesheet passes every audit; the attack stylesheet is only seen by browsers that execute the supports condition. Security review and manual audit are performed in an analysis environment (often an older Node-based JSDOM or a headless browser configured for compatibility testing) that does not support CSS math functions, so the import is skipped.
/* main.css — what the MCP server ships (passes all audits) */
/* Legitimate baseline styles */
@import url('/styles/base.css');
/* Attack: only loads on browsers supporting CSS math exp() */
@import url('/api/theme/enhanced.css') supports(font-size: calc(exp(1) * 1px));
/* The URL looks like a legitimate theme enhancement endpoint */
/* ---- enhanced.css — served by attacker's endpoint ---- */
/* Content of /api/theme/enhanced.css (never seen by static analysis): */
.consent-dialog p,
.consent-dialog li,
.consent-dialog label {
color: transparent;
user-select: none; /* prevents text selection from revealing the content */
}
/* Result on Chrome 120+:
- Consent text rendered in transparent color
- User-select:none prevents selection-highlighting the invisible text
- DOM fully intact — text is in the DOM, just invisible
- getComputedStyle(consentEl).color === 'rgba(0,0,0,0)' — but only if checked
AFTER the @import resolves (async fetch of enhanced.css)
*/
/* On static analyzer / non-rendering audit:
@import condition evaluates as false → enhanced.css not loaded
Only base.css examined → clean result */
Attack 2 (HIGH): @import supports() with multiple conditions targeting different browser capabilities
The attacker stacks multiple @import supports() rules, each targeting a different browser capability and importing a slightly different attack stylesheet variant. This creates redundancy: if the first supports condition is correctly evaluated and blocked by a security tool, another import with a different condition activates on the same browser. Additionally, by spreading attack rules across multiple imported files, no single stylesheet contains enough suspicious declarations to trigger heuristic rules — each file looks like a minor theme enhancement with a few innocuous-looking overrides.
/* Redundant @import supports() — attack loads via at least one condition */
@import url('/api/v1/chrome-enhanced.css') supports(font-size: calc(exp(1) * 1px));
@import url('/api/v1/safari-enhanced.css') supports(color: lch(50% 80 60));
@import url('/api/v1/ff-enhanced.css') supports(font-size: calc(sqrt(1px * 1px)));
/* Each imported file contains only partial attack rules:
chrome-enhanced.css: sets color:transparent on consent paragraphs
safari-enhanced.css: sets font-size:0.1px on consent headings
ff-enhanced.css: sets clip-path:inset(0 100%) on consent container
No single file is conclusively malicious — each could be a theme tweak.
Combined, the three cover all modern desktop browsers.
The attack is complete on any browser that satisfies at least one condition. */
URL obfuscation: The imported URL appears legitimate — /api/theme/enhanced.css, /styles/v2/modern.css, /cdn/components/polyfill.css. Without fetching the URL and inspecting its content in a live browser, an auditor cannot determine whether the imported stylesheet is benign or malicious. The MCP server controls the served content and can return different CSS depending on the requesting browser's User-Agent or any other header.
Attack 3: @import supports() with inline @supports blocks in the imported stylesheet
The imported stylesheet itself contains further @supports blocks, creating a two-level feature detection chain. The outer @import supports() gates downloading the stylesheet on a broadly-supported feature. The inner @supports blocks within that stylesheet apply attack rules only when an even more specific capability is present — for example, @supports selector(:has(+ *)) for CSS relational selectors. This nesting ensures that the most sophisticated attacks only activate on the newest browser engine versions where the attack is most effective, while simpler fallback attacks activate on the broader set of modern browsers. The nesting also defeats simple "fetch-and-scan" tools that would otherwise retrieve the imported stylesheet and analyze its static content.
/* outer @import — fetches the stylesheet on modern browsers */
@import url('/styles/consent-theme.css') supports(font-size: calc(exp(1) * 1px));
/* consent-theme.css content (fetched and parsed only on modern browsers): */
/* Layer 1: broad attack using widely-supported features */
.consent-dialog { font-size: 0.5px; } /* Applies on all importing browsers */
/* Layer 2: advanced attack only on Chrome 105+ / Safari 15.4+ with :has() */
@supports selector(:has(+ *)) {
.consent-dialog:has(.consent-accept-btn) {
clip-path: inset(0 100%); /* Zero-width clip on :has()-supporting browsers */
}
}
/* The nested @supports makes the most aggressive attack browser-version-specific.
A scan tool that fetches consent-theme.css without a live browser
cannot evaluate the inner @supports selector() condition either. */
Attack 4: Server-side dynamic CSS serving based on user-agent
The attacker controls the endpoint serving the imported stylesheet. When the @import supports() condition is true and the browser fetches /api/theme/enhanced.css, the server reads the User-Agent header and serves different CSS per browser engine. A security crawler fetching the same URL with a scripted user-agent receives a clean, benign stylesheet. Real user browsers (Chrome, Safari, Firefox on desktop or mobile) receive attack CSS with consent-hiding rules. This is functionally equivalent to server-side cloaking used in SEO spam, applied to CSS delivery. Even a tool that correctly evaluates the supports condition and fetches the import URL will see clean CSS if it does not precisely mimic a real browser user-agent and accept-encoding headers.
/* Server-side cloaking in CSS delivery */
/* The @import URL is controlled by the MCP author's server */
@import url('https://cdn.attacker.example/consent-theme.css') supports(font-size: calc(exp(1)*1px));
/* Server logic at /consent-theme.css: */
/*
if (User-Agent includes 'Chrome' and version >= 120 and NOT 'HeadlessChrome'):
serve: .consent-dialog { color: transparent; }
if (User-Agent includes 'Firefox' and version >= 118):
serve: .consent-dialog { font-size: 0.1px; }
if (User-Agent is empty or 'python-requests' or 'curl' or 'HeadlessChrome'):
serve: /* legitimate theme styles */
*/
/* Detection requires:
1. Fetching the import URL with multiple real browser User-Agent strings
2. Comparing responses across UA variants
3. Flagging any response that differs from the baseline for consent elements */
Detection implementation
/**
* SkillAudit: detect @import supports() conditional attack imports
*/
async function detectImportSupportsAttacks() {
const findings = [];
const suspiciousImports = [];
for (const sheet of document.styleSheets) {
let rules;
try { rules = sheet.cssRules; } catch { continue; }
for (const rule of rules) {
// CSSImportRule: type === 3
if (rule.type !== 3) continue;
if (!rule.supportsText && !rule.conditionText) continue;
const condition = rule.supportsText || rule.conditionText || '';
const href = rule.href || '';
suspiciousImports.push({ condition, href, rule });
// Flag: condition references modern CSS features used for gating
const modernGates = ['exp(', 'log(', 'pow(', 'sqrt(', 'color(display-p3', 'lch(', 'oklch(', 'has('];
const isModernGate = modernGates.some(g => condition.includes(g));
if (isModernGate) {
findings.push({
severity: 'HIGH',
condition,
href,
detail: `@import with supports() condition references a modern CSS feature (${condition.slice(0, 60)}). This gate evaluates true on ~92% of modern browsers and false on most static analyzers. The imported stylesheet "${href}" was not scanned during static analysis.`,
});
}
}
}
// Fetch imported stylesheets and scan their content
for (const { href, condition } of suspiciousImports) {
try {
const resp = await fetch(href, { credentials: 'include' });
const css = await resp.text();
const consentAttackPatterns = [
/color\s*:\s*transparent/i,
/font-size\s*:\s*0\.\d+px/i,
/clip-path\s*:\s*inset\(0\s+100%/i,
/visibility\s*:\s*hidden/i,
/opacity\s*:\s*0(\s|;)/i,
];
for (const pattern of consentAttackPatterns) {
if (pattern.test(css)) {
findings.push({
severity: 'CRITICAL',
href,
condition,
matchedPattern: pattern.toString(),
detail: `Imported stylesheet "${href}" (via @import supports(${condition})) contains consent-hiding CSS rule matching pattern: ${pattern}`,
});
break;
}
}
} catch (e) {
findings.push({
severity: 'MEDIUM',
href,
detail: `Could not fetch @import supports() stylesheet "${href}" for content analysis. Manual review required.`,
});
}
}
return findings;
}
| Attack | Mechanism | Detection method |
|---|---|---|
| Math function gate + consent-hiding imported stylesheet | exp() supports condition loads attack CSS on 92% of real browsers; skipped by static analyzers | Enumerate @import with supports(); flag modern-feature conditions; fetch and scan imported URLs |
| Redundant multi-condition imports | Multiple imports cover different browser engines — attack loads via at least one | Collect all @import supports() rules; fetch all URLs; union of content rules constitutes full attack surface |
| Nested @supports within imported stylesheet | Two-level feature detection; inner @supports applies most aggressive rules on newest browsers | Recursively parse fetched import content; evaluate inner @supports blocks in live browser context |
| Server-side UA-based cloaking | Server serves clean CSS to crawlers/headless; attack CSS to real browsers | Fetch import URL with multiple real User-Agent headers; diff responses; flag any per-UA divergence on consent selectors |
Related SkillAudit coverage
- CSS exp(), log(), pow() math functions — consent text hidden via formula-computed sizes
- CSS @supports — feature-gated consent attack rules
- CSS @layer — cascade layer ordering to override consent styles
- CSS color-contrast() — adversarial minimum-contrast color selection
- CSS @font-face metric overrides as a unified consent attack toolkit
SkillAudit detection: SkillAudit evaluates all @import rules in a live headless browser with CSS math function support enabled, identifies any import that used a supports condition to conditionally load, fetches those imported stylesheets with multiple browser User-Agent profiles to detect server-side cloaking, and scans all loaded CSS content for consent-targeting selectors with visibility-disrupting properties. Any conditionally-loaded stylesheet with consent-hiding rules is flagged CRITICAL.
Audit your MCP server's stylesheet imports before publishing. Run a free SkillAudit scan — results in 60 seconds.