MCP server CSS @supports consent bypass security
The CSS @supports rule (Feature Queries) applies a block of CSS declarations only in browsers that support a specified CSS feature. Because MCP server developers and installers use modern browsers by definition — these are the users capable of running Claude Code and consuming new skill APIs — @supports attacks that target widely-supported modern CSS features fire with near-100% reliability on the MCP installation population. Older CSS audit tools that do not evaluate @supports blocks treat the rules inside as dead code and never check the contained consent-hiding declarations.
Attack findings
Background: @supports feature detection and its security implications
CSS Feature Queries were designed as a progressive enhancement tool: write a fallback for older browsers outside the @supports block, and enhanced styles inside it for browsers that support the feature. For consent security, this mechanism creates an inversion of the intended progressive-enhancement pattern: @supports attacks degrade consent visibility in capable browsers while leaving it intact in incapable ones. Because the target population (developers who install MCP skills) uses capable browsers almost exclusively, these attacks achieve near-total coverage of the install population while appearing in the CSS as a modern enhancement pattern.
CSS audit tool gap: Many CSS static analysis tools parse @supports blocks but do not evaluate them — they treat the contents as conditionally dead code that may or may not apply. A tool checking "does the stylesheet contain consent display:none?" may skip this rule because it's inside an @supports block. The correct audit requires resolving which @supports conditions are true in the target browser environment and applying those rules.
Attack 1 — @supports (display:grid) universal modern browser hiding (SA-CSS-SUP-001)
CSS Grid Layout (display:grid) is supported by 97.8% of browsers globally as of 2026 (caniuse.com). Every major browser — Chrome, Firefox, Safari, Edge — in every version released since 2017 supports it. Setting @supports (display:grid) { .consent { display: none; } } effectively applies in every installation context an MCP developer will encounter. The choice of display:grid as the test feature is deliberate: it is so widely supported that the test always passes, but it is a legitimate CSS feature check that does not look suspicious in a modern stylesheet. An auditor that skips @supports blocks entirely misses this attack entirely.
/* Attack: grid-support check hides consent in all modern browsers */
@supports (display: grid) {
.consent-required {
display: none;
}
}
/* CSS grid: 97.8% global support (2026) — fires in every modern browser
Older browsers (IE11 and below) see the consent — not the install target
textContent: unaffected — element in DOM; getComputedStyle: display:'none'
Static audit without @supports evaluation: PASS (rule appears conditional/dead) */
SA-CSS-SUP-001 (High). Detection requires evaluating @supports conditions in a browser context and checking the active rules that result. CSS.supports('display', 'grid') returns true in all modern browsers, confirming the attack block is active. SkillAudit evaluates all @supports conditions using CSS.supports() and checks consent visibility under active rules.
/* Detection */
function checkSupportsConsent(el) {
// Check if any @supports rule containing display:none applies to the consent element
const cs = getComputedStyle(el);
if (cs.display === 'none' || cs.visibility === 'hidden' || parseFloat(cs.opacity) === 0) {
// Identify if the hiding comes from a @supports-scoped rule
// by checking CSS.supports() for the condition
const gridSupported = CSS.supports('display', 'grid');
const oklchSupported = CSS.supports('color', 'oklch(0 0 0)');
const overflowClipSupported = CSS.supports('overflow', 'clip');
return {
vuln: 'SA-CSS-SUP-001',
detail: `consent hidden; CSS.supports — grid:${gridSupported}, oklch:${oklchSupported}, clip:${overflowClipSupported}`
};
}
return null;
}
Attack 2 — @supports (color:oklch()) near-white text in modern browsers (SA-CSS-SUP-002)
The oklch() color function is supported in Chrome 111+, Firefox 113+, and Safari 15.4+ — covering every browser release from the past three years. Setting consent text to oklch(98% 0 0) (near-white) on a white or near-white background makes it visually invisible while the computed color value remains a valid, non-transparent color. Unlike color:transparent (which getComputedStyle returns as rgba(0,0,0,0)), an oklch near-white color returns as an rgb value with extremely high component values — a programmatic check for color === 'transparent' passes. The attack requires an additional contrast ratio check to detect: compute the luminance of both the text color and background color and verify the contrast exceeds 4.5:1.
/* Attack: oklch near-white consent text in modern browsers */
@supports (color: oklch(0 0 0)) {
.consent-text {
color: oklch(98% 0 0); /* near-white — L=98% */
background-color: oklch(99.5% 0 0); /* near-white background */
/* Luminance contrast: L98 vs L99.5 → ~1.03:1 — visually invisible */
/* getComputedStyle returns rgb(249,249,249) vs rgb(253,253,253) — not 'transparent' */
}
}
SA-CSS-SUP-002 (High). Detection requires luminance contrast calculation rather than a simple color-name check. Convert the computed text and background colors to relative luminance (per WCAG 2.1 formula) and flag any consent element where the contrast ratio is below 3:1. Standard WCAG AA requires 4.5:1 for normal text; consent text should be held to the same or higher standard.
Attack 3 — @supports (overflow:clip) clips consent in modern browsers (SA-CSS-SUP-003)
The overflow:clip value (distinct from overflow:hidden) was added to Chrome 90+, Firefox 81+, and Safari 16+. Inside an @supports (overflow:clip) block, setting overflow:clip and max-height:2px on the consent container clips it to 2px of visible height. In older browsers (and in auditors that do not support overflow:clip), the fallback is no max-height override — consent is visible at full height. The attack therefore specifically targets the modern browser population while appearing to be a progressive enhancement of the overflow handling. The text is technically rendered but clipped to two pixels — invisible to all users.
/* Attack: overflow:clip + max-height clips consent in modern browsers */
@supports (overflow: clip) {
.consent-container {
overflow: clip;
max-height: 2px; /* clips to 2px — text exists but invisible */
/* Fallback (browsers without overflow:clip): full height visible */
}
}
/* Chrome 90+, Firefox 81+, Safari 16+: consent clipped to 2px
IE, older browsers: overflow:clip unknown; full consent visible
textContent: full string; scrollHeight >> clientHeight (detectable) */
Attack 4 — @supports selector(:has(input:checked)) hides consent after checkbox (SA-CSS-SUP-004)
The CSS :has() pseudo-class is supported in Chrome 105+, Firefox 121+, and Safari 15.4+. The @supports selector(:has(input:checked)) block applies only in browsers that support :has(). Inside this block, an MCP server sets opacity:0 on a consent element when a sibling checkbox is checked: .consent-required:has(input:checked) + .consent-text { opacity: 0; }. The consent text is fully visible before the user checks the "I have read and agree" checkbox. The moment the user checks the box — confirming their purported agreement — the consent text disappears. It is unreadable at the exact moment the commitment is made. The interaction reads: present consent → user checks box to confirm → consent text immediately hides. The commitment is now irreversible; the consent is gone.
/* Attack: :has(input:checked) hides consent at confirmation moment */
@supports selector(:has(input:checked)) {
.consent-wrapper:has(input[type="checkbox"]:checked) .consent-text {
opacity: 0;
transition: opacity 0s; /* instant — no fade that might alert the user */
}
}
/* Flow:
1. Page loads — consent text visible
2. User reads consent, checks "I agree" checkbox
3. IMMEDIATELY: consent text opacity:0 — invisible
4. User clicks Install — has "agreed" but can no longer read what they agreed to
5. MutationObserver on checkbox change is the reliable detection path */
/* Detection */
document.querySelectorAll('input[type="checkbox"]').forEach(cb => {
cb.addEventListener('change', () => {
const consentEl = document.querySelector('.consent-text');
if (consentEl) {
const opacity = parseFloat(getComputedStyle(consentEl).opacity);
if (opacity < 0.5) flagTampering('SA-CSS-SUP-004');
}
});
});
SkillAudit detection: SkillAudit evaluates all @supports conditions using CSS.supports() and CSS.supports('selector(...)') to determine which rule blocks are active. It then checks computed consent visibility (display, visibility, opacity, color contrast) under active rules. For SA-CSS-SUP-004 it also simulates checkbox check/uncheck events and re-evaluates consent visibility after each state change. Run a free audit →
Detection summary
| Attack ID | @supports condition + property | Key detection signal |
|---|---|---|
| SA-CSS-SUP-001 | @supports (display:grid) → display:none | CSS.supports('display','grid') === true AND computed display === 'none' |
| SA-CSS-SUP-002 | @supports (color:oklch()) → oklch(98% 0 0) | CSS.supports('color','oklch(0 0 0)') === true AND consent luminance contrast < 3:1 |
| SA-CSS-SUP-003 | @supports (overflow:clip) → max-height:2px | CSS.supports('overflow','clip') === true AND scrollHeight >> clientHeight |
| SA-CSS-SUP-004 | @supports selector(:has(input:checked)) → opacity:0 after check | consent opacity drops to 0 after checkbox change event |