CSS @supports and @media as MCP Consent Bypass Vectors
CSS conditional rules were invented for progressive enhancement — a principled way to write styles that adapt to the browser's capabilities and the user's environment. MCP server authors have repurposed this mechanism as a precision targeting system: rules that fire specifically in the device configurations and browser versions where developers actually install skills, while remaining invisible to the auditors and scanners that run in controlled environments. This post maps nine canonical attack patterns across @media and @supports, explains the combination techniques that widen the attack surface further, and provides detection code for each vector.
In this post
- Why conditional CSS is particularly dangerous
- @media(min-width:1024px) — desktop targeting
- @media(hover:hover) — pointer device targeting
- @media(max-width:1023px) + color:transparent — cross-device blackout
- @media(color-gamut:p3) — near-white on developer displays
- @media(prefers-color-scheme:dark) — dark background promotion
- @supports(display:grid) — universal modern browser hiding
- @supports(color:oklch()) — wide-gamut near-white
- @supports selector(:has(input:checked)) — post-confirmation vanish
- Cross-condition combinations
- Detection strategy
Why conditional CSS is particularly dangerous
Standard consent-hiding attacks — display:none, visibility:hidden, opacity:0, sub-pixel font sizes — apply unconditionally. They fire in every browser, on every device, at every viewport size. This makes them relatively easy to detect: audit the stylesheet, find the hidden property on the consent element, flag it. The consent element's computed style is the same whether the auditor checks it from a headless Chromium in CI or a developer opens the page at their desk.
Conditional CSS attacks break this symmetry. The rule body — the consent-hiding declaration — is wrapped inside a condition that must be true for the rule to apply. The condition is evaluated at runtime by the browser, using current device capabilities and environment values that differ between the auditor's context and the attacker's target environment. An audit tool running at a fixed 1280×800 viewport on a non-retina Linux machine will not match @media(color-gamut:p3). A static analysis tool that parses stylesheets but does not evaluate conditions treats every @media and @supports block as conditionally dead code and skips the enclosed declarations entirely.
The targeting inversion: Unconditional attacks hide consent everywhere. Conditional attacks hide consent exactly where it matters — in modern browsers, on developer hardware, at desktop viewports, on trackpad-equipped machines — the precise environment of every MCP installation. They are invisible in the audit environment and active in the deployment environment.
The nine attacks below are organized by the condition used: five @media variants targeting device/environment characteristics, and four @supports variants targeting CSS feature capability.
@media(min-width:1024px) — desktop targeting High
SA-CSS-MQ-001 — consent hidden specifically on desktop viewports where developers install MCP skills
The majority of MCP skill installations happen on desktop or laptop machines at viewport widths above 1024px. @media(min-width:1024px) { .consent { display: none; } } sets consent to hidden at exactly these widths. On mobile viewports (phones, tablets in portrait) the consent is visible — but developers are not installing skills on mobile devices. The attack achieves near-100% coverage of the actual installation population while leaving the consent visible to any audit or test run at a constrained viewport.
/* Attack: hide consent on desktop — where MCP installs happen */
@media (min-width: 1024px) {
.consent-required {
display: none;
}
}
/* Mobile and tablet portrait: consent visible
Desktop (≥1024px): consent hidden
MCP install context: always desktop
Static audit at fixed 1280px: fires immediately
Static audit at 375px (mobile test): consent visible — PASS */
Detection checklist
- Evaluate consent visibility at multiple viewport widths: 375px, 768px, 1024px, 1280px, 1920px
- If consent is hidden at any width ≥1024px, flag SA-CSS-MQ-001 regardless of visibility at smaller widths
- In headless audit: resize the browser viewport in a loop and re-check
getComputedStyle(consentEl).displayafter each resize
See the full reference: @media consent bypass security — all five media query attack patterns.
@media(hover:hover) — pointer device targeting High
SA-CSS-MQ-002 — consent hidden on devices with hover-capable pointers (mice, trackpads) — developer workstations exclusively
The hover:hover media feature matches devices where the primary input mechanism can hover over elements without activating them — mice and trackpads, not touchscreens. The entire population of developers installing MCP skills uses a hover-capable device. @media(hover:hover) { .consent { opacity: 0; } } hides consent on every developer machine while leaving it visible on phones and tablets. Any audit run on a mobile emulation profile or a touch-first test environment will report the consent as visible.
/* Attack: hide consent on hover-capable devices (all developer workstations) */
@media (hover: hover) {
.consent-text {
opacity: 0;
pointer-events: none;
}
}
/* Touch devices (phones, tablets): opacity 1 — consent visible
Hover devices (mice, trackpads): opacity 0 — consent hidden
All MCP install contexts: hover:hover
Audit on mobile emulator: PASS
Audit in headless Chromium (desktop mode): opacity:0 — FAIL */
Detection checklist
- Audit must run in desktop browser mode (not mobile emulation) —
hover:hoverrequires a real desktop context - Check
window.matchMedia('(hover: hover)').matchesto confirm audit environment matches attack condition - Re-evaluate consent computed opacity in a hover-capable context;
pointer-events:noneon consent is itself a flag
@media(max-width:1023px) + color:transparent — cross-device blackout High
SA-CSS-MQ-003 — combining desktop-hide and mobile-transparent creates a cross-device blackout with no visible consent at any width
The single-condition attacks above each have a gap: consent is visible at the excluded viewport range. The combination technique closes this gap. MQ-001 hides consent at viewports ≥1024px via display:none. MQ-003 adds a second rule hiding consent at viewports ≤1023px via color:transparent. Together they cover every possible viewport: consent is hidden everywhere. An audit that checks visibility at a single viewport width will always pick one side of the boundary and miss the other condition.
/* Attack combination: cross-device blackout */
@media (min-width: 1024px) {
.consent-required {
display: none; /* Desktop: removed from layout */
}
}
@media (max-width: 1023px) {
.consent-required {
color: transparent; /* Mobile: text invisible but in DOM */
user-select: none; /* Cannot copy-paste to check */
}
}
/* Any single viewport: catches only one rule
Full audit at both 375px and 1280px: both rules fire
Overlap at exactly 1024px: MQ-001 fires; below 1024px: MQ-003 fires
No viewport where consent is unaffected */
Detection checklist
- Test at a minimum of two viewports: one below and one above each media query breakpoint in the stylesheet
- Check both
displayproperty AND text color contrast independently — different rules use different hiding mechanisms - Enumerate all
@mediabreakpoints in the stylesheet and test one viewport in each range
@media(color-gamut:p3) — near-white on developer displays High
SA-CSS-MQ-004 — consent text near-white on wide-gamut displays (MacBook Pro Retina, high-end monitors) — developer hardware
The color-gamut:p3 media feature matches displays capable of rendering colors in the DCI-P3 color space — effectively all modern MacBooks with Retina displays and most high-end developer monitors produced after 2018. Setting consent text to a high-lightness oklch(97% 0 0) or a narrow-range rgb(250,250,250) on a white background creates invisible text specifically on developer displays. On a standard sRGB monitor (Linux VM, budget laptop, older machine), the consent is rendered in the sRGB context and the exact color computation differs enough to remain barely readable. The attack reliably targets the hardware most commonly used for MCP skill installation.
/* Attack: near-white text on P3-capable developer displays */
@media (color-gamut: p3) {
.consent-text {
color: oklch(97% 0 0); /* very high lightness, no chroma */
}
.consent-wrapper {
background-color: oklch(99% 0 0); /* near-white background */
}
/* Luminance contrast: L97 vs L99 → ~1.04:1 — invisible on P3 display */
/* On sRGB display: colors clamp differently — slight contrast remains */
}
Detection checklist
- Check
window.matchMedia('(color-gamut: p3)').matchesin the audit browser context - Compute luminance contrast for any consent element with
oklch()or high-valuergb()colors - Flag consent where luminance contrast ratio is below 4.5:1 (WCAG AA for normal text)
@media(prefers-color-scheme:dark) — dark background promotion High
SA-CSS-MQ-005 — in dark mode (most developer defaults), consent container background is promoted to near-black, hiding dark-colored text
Dark mode is the default system setting for a majority of developers — surveys consistently show developer dark-mode adoption above 70%. The prefers-color-scheme:dark media feature matches this system setting. The consent text color is left unchanged at a dark neutral — #1a1a1a or rgb(20,20,20) — which is visible on the light-mode background. Inside the dark-mode media block, the consent container background is promoted to near-black (#0d0d0d), creating effectively zero contrast. The consent text exists in the DOM, has a non-transparent color, and its computed style has a positive luminance — but placed on a background of equal luminance it is invisible. Contrast-checking tools that only check computed color without computing the background color miss this attack entirely.
/* Attack: dark background promotion creates zero contrast in dark mode */
.consent-text {
color: #1a1a1a; /* Dark text: visible on light background */
}
@media (prefers-color-scheme: dark) {
.consent-container {
background-color: #0d0d0d; /* Near-black: matches text color */
/* getComputedStyle(consentEl).color === "rgb(26,26,26)" — non-transparent */
/* getComputedStyle(consentEl).backgroundColor === "rgb(13,13,13)" */
/* Contrast ratio: ~1.04:1 — invisible */
}
}
/* Light mode: dark text on white background — ~17:1 contrast — visible */
/* Dark mode: dark text on near-black — ~1.04:1 — invisible */
Detection checklist
- Audit must also run with
prefers-color-scheme:darkemulated — checkpage.emulateMediaFeatures([{name:'prefers-color-scheme',value:'dark'}])in Playwright/Puppeteer - Compute contrast ratio between consent text color AND the effective background color (walk the DOM tree for the nearest background-color: non-transparent ancestor)
- Flag any consent element with contrast ratio below 4.5:1 in either color scheme mode
Where @media attacks target the user's environment, @supports attacks target the browser's capabilities. Since MCP developers use modern, evergreen browsers by definition, @supports conditions testing for modern CSS features fire reliably in the entire installation population. The four patterns below escalate from "hides in 97.8% of browsers" to "hides only at the exact moment of consent confirmation."
@supports(display:grid) — universal modern browser hiding High
SA-CSS-SUP-001 — CSS grid support (97.8% global, 100% in modern browsers) fires in every MCP installation context
CSS Grid Layout is supported by 97.8% of all browsers globally (2026 data) and by every browser version released since 2017. @supports (display: grid) is therefore not a meaningful conditional — it evaluates to true in essentially every browser any user will encounter. Wrapping a consent display:none inside this block looks like a legitimate feature check in the stylesheet while providing no practical conditionality. Static CSS analysis tools that parse stylesheets but skip @supports blocks as "conditional/dead code" will never evaluate the enclosed rule. The attack achieves universal coverage without triggering the obvious pattern of an unconditional display:none on the consent element.
/* Attack: grid-support wraps unconditional consent hiding */
@supports (display: grid) {
.consent-required {
display: none;
}
}
/* CSS.supports('display','grid') === true in Chrome, Firefox, Safari, Edge
IE11 only exception — not an MCP installation platform
Audit tool that skips @supports blocks: PASS (sees no unconditional display:none)
Audit tool that evaluates CSS.supports(): detects display:none under active rule */
See the full reference: @supports consent bypass security — all four feature-query attack patterns.
@supports(color:oklch()) — wide-gamut near-white High
SA-CSS-SUP-002 — oklch() color support matches every modern browser; near-white text evades transparent-color checks
The oklch() color function is supported in Chrome 111+, Firefox 113+, and Safari 15.4+ — the complete set of modern developer browsers. Combined with the @supports check, it limits the near-white attack to modern browsers (which is the entire MCP installation population) while remaining syntactically invisible to auditors that don't support oklch parsing. The near-white value oklch(98% 0 0) on a white background produces a contrast ratio of approximately 1.03:1 — visually invisible. Unlike color:transparent (which getComputedStyle returns as rgba(0,0,0,0) and is trivially flagged), an oklch near-white computes to rgb(249,249,249) in the browser — a non-zero, non-transparent color value that bypasses simple color-equality checks.
/* Attack: oklch near-white in modern browsers — bypasses transparent check */
@supports (color: oklch(0 0 0)) {
.consent-text {
color: oklch(98% 0 0); /* L=98% — near-white */
background-color: oklch(99.5% 0 0); /* L=99.5% — near-white background */
}
}
/* getComputedStyle().color: "rgb(249,249,249)" — NOT "rgba(0,0,0,0)"
Simple audit check (color === transparent): PASS
Contrast ratio L98 vs L99.5: ~1.03:1
Contrast-ratio check (< 4.5:1): FAIL — correctly flagged */
SA-CSS-SUP-002 (High). Detection requires a contrast-ratio calculation, not a color-name check. Compute the relative luminance of both the consent text color and its background, then verify the ratio exceeds 4.5:1. Standard WCAG tooling handles this; auditors checking only for transparent, rgba(0,0,0,0), or display:none miss it entirely.
@supports selector(:has(input:checked)) — post-confirmation vanish Critical
SA-CSS-SUP-004 — consent visible before confirmation, disappears the moment the user checks the agreement checkbox
The :has() pseudo-class is supported in Chrome 105+, Firefox 121+, and Safari 15.4+. The @supports selector(:has(input:checked)) block limits the rule to these modern browsers. Inside, a rule fires when a sibling or ancestor checkbox is checked: .consent-wrapper:has(input[type="checkbox"]:checked) .consent-text { opacity: 0; }. The consent text is fully readable while the user reads it. The checkbox labeled "I have read and agree to the above terms" is visible. The user checks the box. At that instant — the moment of commitment — the consent text disappears. It is unreadable at exactly the point in the flow where it should be most verifiable.
This pattern is uniquely insidious because it passes static audits completely: consent is present in the DOM, visible at page load, not hidden by any unconditional rule. A page scanner that checks visibility at load time reports no issue. The attack only triggers through user interaction, at a behavioral level. Only a behavioral auditor — one that simulates the checkbox check and re-evaluates consent visibility after the state change — catches it.
/* Attack: consent disappears at the moment of agreement */
@supports selector(:has(input:checked)) {
.consent-wrapper:has(input[type="checkbox"]:checked) .consent-text {
opacity: 0;
transition: opacity 0s; /* Instant — no visible fade */
pointer-events: none; /* Cannot re-select text to copy */
user-select: none;
}
}
/* At load: consent fully visible — static audit: PASS
After checkbox check: consent opacity:0 — invisible
DOM: text node still present (innerText returns content)
Behavioral audit: check → re-evaluate opacity → FAIL */
/* Detection: MutationObserver + checkbox change listener */
function auditPostCheckboxVisibility(consentEl, form) {
const checkboxes = form.querySelectorAll('input[type="checkbox"]');
checkboxes.forEach(cb => {
cb.addEventListener('change', () => {
const op = parseFloat(getComputedStyle(consentEl).opacity);
if (op < 0.5) {
reportFinding('SA-CSS-SUP-004',
'Consent became invisible after checkbox state change');
}
});
});
}
SA-CSS-SUP-004 (Critical). This attack is undetectable by static CSS analysis. It requires an auditor that interacts with the consent form — checking and unchecking all checkboxes — and re-evaluates consent visibility after each state change. SkillAudit's behavioral simulation layer covers this interaction in every audit.
Cross-condition combinations
Each individual conditional attack has a gap: a browser configuration, viewport size, or device type where the condition doesn't fire and consent remains visible. Cross-condition combinations close these gaps by layering two or more conditions that together cover every relevant configuration.
@media desktop-hide + @media mobile-transparent (MQ-001 + MQ-003)
As shown in attack 3, combining @media(min-width:1024px) { display:none } with @media(max-width:1023px) { color:transparent } produces a viewport-complete blackout: no single viewport width shows an unaffected consent element. Because each rule uses a different hiding mechanism, an auditor checking only for one type of hiding (only display:none, or only transparent text color) further increases the chance of missing the combined attack.
@media hover + @supports grid (MQ-002 + SUP-001)
Combining @media(hover:hover) { opacity:0 } with @supports(display:grid) { display:none } creates redundancy: the @media rule fires on all developer workstations (hover-capable), and the @supports rule fires in all modern browsers. Either condition alone would hide the consent in the target environment. The combination ensures that even if one condition is evaluated and blocked (e.g., an auditor that emulates touch input to test hover:hover), the other condition still fires.
@media dark-mode + @supports oklch (MQ-005 + SUP-002)
Combining dark-mode background promotion with oklch near-white creates two independent text-invisibility paths that require different detection approaches. The dark-mode attack requires contrast-ratio checking against the promoted background. The oklch attack requires contrast-ratio checking against the element's own background. An auditor that only checks one of the two will miss the other.
Combination detection principle: Each condition in a combined attack requires an independent audit check. Testing at one viewport width, in one color scheme mode, on one device type, using one hiding-mechanism check, is not sufficient. A complete audit evaluates each dimension independently and combines the results.
Detection strategy
A complete detection strategy for conditional CSS consent attacks must address the symmetry break between auditor environment and attacker target environment. The audit must either simulate the target environment or evaluate all possible environments in a single pass.
For @media attacks: multi-environment evaluation
The audit must run at a minimum of four environment configurations: desktop viewport (1280px), mobile viewport (375px), hover-capable device mode, touch device mode, light color scheme, dark color scheme, P3 display gamut, sRGB display gamut. For each configuration, the audit evaluates consent visibility using computed style, contrast ratio, and dimensional checks (scrollHeight, clientHeight, getBoundingClientRect()). A consent element is considered hidden if any configuration produces a visibility failure.
/* Audit: multi-viewport consent check */
async function auditAllViewports(page, consentSelector) {
const configs = [
{ width: 1280, height: 800, colorScheme: 'light', hasHover: true },
{ width: 375, height: 812, colorScheme: 'light', hasHover: false },
{ width: 1280, height: 800, colorScheme: 'dark', hasHover: true },
{ width: 375, height: 812, colorScheme: 'dark', hasHover: false },
];
const findings = [];
for (const cfg of configs) {
await page.setViewportSize({ width: cfg.width, height: cfg.height });
await page.emulateMediaFeatures([
{ name: 'prefers-color-scheme', value: cfg.colorScheme },
]);
const visible = await page.$eval(consentSelector, el => {
const cs = getComputedStyle(el);
if (cs.display === 'none') return false;
if (parseFloat(cs.opacity) < 0.1) return false;
const rect = el.getBoundingClientRect();
return rect.width > 0 && rect.height > 8;
});
if (!visible) findings.push({ config: cfg, finding: 'consent-hidden' });
}
return findings;
}
For @supports attacks: CSS.supports() evaluation + behavioral simulation
The audit evaluates all @supports conditions in the stylesheet using CSS.supports() and CSS.supports('selector(...)') to determine which rule blocks are active in the current browser. It then checks consent computed style under the active rules. For the behavioral :has(input:checked) attack, the audit additionally simulates all checkbox interactions and re-evaluates consent visibility after each state change.
/* Detect active @supports blocks hiding consent */
function getActiveSupportsRules(stylesheet) {
const active = [];
for (const rule of stylesheet.cssRules) {
if (rule instanceof CSSSupportsRule) {
const condStr = rule.conditionText;
// Evaluate the condition using CSS.supports()
const isActive = CSS.supports(condStr) ||
CSS.supports('selector', condStr.replace(/^selector\(|\)$/g, ''));
if (isActive) {
// Collect declarations inside active blocks
for (const innerRule of rule.cssRules) {
active.push({ selector: innerRule.selectorText,
declarations: innerRule.style.cssText });
}
}
}
}
return active;
}
The principle: environment-aware, behavioral audit
Conditional CSS consent attacks reveal a structural weakness in static analysis: no static tool can evaluate a CSS condition without knowing the runtime environment. The only reliable approach is a behavioral auditor that executes the page in a real or fully-emulated browser context, sweeps the relevant environment configurations, interacts with the consent flow, and verifies consent visibility after each interaction. SkillAudit's scanner covers all nine attack patterns in this post — and the combinations — as part of its standard consent-bypass detection. Run a free audit →
Summary
| ID | Condition | Effect | Audit requirement |
|---|---|---|---|
| SA-CSS-MQ-001 | @media(min-width:1024px) | display:none on desktop | Test at ≥1024px viewport |
| SA-CSS-MQ-002 | @media(hover:hover) | opacity:0 on pointer devices | Run in desktop hover mode |
| SA-CSS-MQ-003 | @media(max-width:1023px) | color:transparent on mobile | Test at ≤1023px AND ≥1024px |
| SA-CSS-MQ-004 | @media(color-gamut:p3) | near-white on P3 displays | Emulate P3 + contrast check |
| SA-CSS-MQ-005 | @media(prefers-color-scheme:dark) | background promoted to near-black | Run in dark mode; contrast check |
| SA-CSS-SUP-001 | @supports(display:grid) | display:none (97.8% browsers) | Evaluate CSS.supports() conditions |
| SA-CSS-SUP-002 | @supports(color:oklch()) | oklch near-white text | Contrast ratio vs background |
| SA-CSS-SUP-003 | @supports(overflow:clip) | max-height:2px clip | scrollHeight >> clientHeight check |
| SA-CSS-SUP-004 | @supports selector(:has(input:checked)) | opacity:0 after checkbox | Behavioral: check → re-evaluate |
SkillAudit detects all nine patterns and their cross-condition combinations — multi-viewport evaluation, media feature emulation, CSS.supports() evaluation, behavioral interaction simulation. Run a free audit to see which patterns are present in your MCP server's stylesheet.