Security Guide
MCP server CSS prefers-reduced-transparency security — backdrop-filter consent reveal disabled with no static fallback, glass-effect consent invisible on opaque fallback background, opacity-transition permanently hidden, JS matchMedia transparency swap
CSS @media (prefers-reduced-transparency: reduce) matches users who have enabled Reduce Transparency on macOS or iOS — a significant portion of users with vestibular disorders, attention difficulties, or motion sensitivity who rely on accessibility settings. An MCP server that designs its consent element as a glass overlay loses its blurred-backdrop legibility cue under this setting. If the fallback opaque background matches the page color, the consent panel becomes invisible with no change to display, opacity, or visibility.
CSS prefers-reduced-transparency media feature — overview
@media (prefers-reduced-transparency) is defined in CSS Media Queries Level 5. It queries the OS "Reduce Transparency" accessibility preference. On macOS: System Settings → Accessibility → Display → Reduce Transparency. On iOS: Settings → Accessibility → Display & Text Size → Reduce Transparency. When enabled, the OS reduces or eliminates the use of translucent, blurred backgrounds in system UI. The CSS media query fires for the reduce value when this setting is active. Devices and users that match: macOS users with vestibular disorders or attention difficulties, iOS users with the same, users who enable the setting for battery savings (blur effects are GPU-intensive). Standard audit environments report no-preference and never match reduce. Related: prefers-reduced-motion, prefers-contrast, prefers-reduced-data.
Attack 1: backdrop-filter consent reveal disabled — no opaque fallback
A consent element designed as a glass overlay places the consent text over a blurred version of the page content using backdrop-filter: blur(). The element itself has a semi-transparent background (rgba(255,255,255,0.1)) — nearly invisible without the blur — and the blurred backdrop creates visual separation from the page. Under prefers-reduced-transparency: reduce, browsers stop applying backdrop-filter, or the MCP server explicitly removes it. The semi-transparent background is now rendered against the solid-color page, producing a near-invisible panel. The consent text may still be present in the DOM but visually invisible.
/* Base: glass consent panel — visible because of backdrop blur */
.consent-panel {
background: rgba(255, 255, 255, 0.08); /* near-transparent */
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.15);
/* On a dark page background, the blur produces a frosted-glass panel.
The panel boundary is visible due to blur rendering, not background opacity. */
}
/* Attack: reduce transparency removes the blur, leaving near-transparent background */
@media (prefers-reduced-transparency: reduce) {
.consent-panel {
backdrop-filter: none;
-webkit-backdrop-filter: none;
/* No opaque fallback background provided.
rgba(255,255,255,0.08) against dark page: nearly invisible.
Border rgba(255,255,255,0.15): near-invisible.
Panel is present in DOM, correct dimensions, opacity:1.
Visually: indistinguishable from page background. */
}
}
// Detection: backdrop-filter removal under prefers-reduced-transparency
function auditReducedTransparencyBackdropRemoval(consentEl) {
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (rule.type !== CSSRule.MEDIA_RULE) continue;
const mq = rule.conditionText || rule.media.mediaText;
if (!/prefers-reduced-transparency\s*:\s*reduce/.test(mq)) continue;
for (const inner of rule.cssRules) {
if (inner.type !== CSSRule.STYLE_RULE) continue;
try { if (!consentEl.matches(inner.selectorText)) continue; }
catch (e) { continue; }
const s = inner.style;
const removesBlur = s.backdropFilter === 'none' || s.webkitBackdropFilter === 'none'
|| s.filter === 'none';
if (removesBlur) {
// Check if base background is semi-transparent
const baseBg = getComputedStyle(consentEl).backgroundColor;
const alphaMatch = baseBg.match(/rgba\(\d+,\s*\d+,\s*\d+,\s*([\d.]+)\)/);
if (alphaMatch && parseFloat(alphaMatch[1]) < 0.3) {
console.warn('[SkillAudit] prefers-reduced-transparency:reduce removes backdrop-filter from consent element;',
'base background is semi-transparent (alpha:', alphaMatch[1], ');',
'consent panel may be visually invisible under reduced-transparency;',
'no opaque fallback background found;',
'selector:', inner.selectorText, 'element:', consentEl);
}
}
}
}
} catch (e) {}
}
}
The glass UI trap: Glass/frosted UI is popular in modern MCP consent banners. The backdrop blur is what makes the panel visually distinct from the page. When reduced-transparency removes the blur, a panel with rgba(255,255,255,0.1) background becomes 90% transparent against the page — invisible without any display or opacity change. Standard consent scanners checking only getComputedStyle().display and opacity will pass this completely.
Attack 2: glass-effect consent — translucent background matches page under opacity fallback
The more targeted variant explicitly sets the consent element's background to match the page under prefers-reduced-transparency: reduce. When the backdrop blur is removed and the fallback is a solid color that the author controls, the solid color is set to the page background color — making the panel invisible. The border and box-shadow are also removed in the same block.
/* Attack: fallback background matches page background */
.consent-panel {
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(24px);
}
@media (prefers-reduced-transparency: reduce) {
.consent-panel {
backdrop-filter: none;
background: #1a1a1a; /* matches page background exactly */
border: none;
box-shadow: none;
/* Consent panel: same color as page, no border, no shadow.
Invisible. opacity:1, display:block. */
}
}
body { background-color: #1a1a1a; } /* same as fallback */
// Detection: consent background matches page background under reduced-transparency
function auditReducedTransparencyColorMatch(consentEl) {
const isReduced = window.matchMedia('(prefers-reduced-transparency: reduce)').matches;
if (!isReduced) {
// CSSOM scan: look for prefers-reduced-transparency background rules
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (rule.type !== CSSRule.MEDIA_RULE) continue;
const mq = rule.conditionText || rule.media.mediaText;
if (!/prefers-reduced-transparency\s*:\s*reduce/.test(mq)) continue;
for (const inner of rule.cssRules) {
if (inner.type !== CSSRule.STYLE_RULE) continue;
try { if (!consentEl.matches(inner.selectorText)) continue; }
catch (e) { continue; }
if (inner.style.backgroundColor) {
console.warn('[SkillAudit] prefers-reduced-transparency:reduce overrides consent element background;',
'verify background does not match page background under reduced-transparency;',
'set background-color:', inner.style.backgroundColor,
'selector:', inner.selectorText);
}
}
}
} catch (e) {}
}
return;
}
// If currently in reduced-transparency mode: check computed colors
const cs = getComputedStyle(consentEl);
const pageBg = getComputedStyle(document.body).backgroundColor;
if (cs.backgroundColor === pageBg) {
console.warn('[SkillAudit] consent element background matches page background under prefers-reduced-transparency:reduce;',
'consent panel is visually invisible;',
'background:', cs.backgroundColor, 'element:', consentEl);
}
}
Attack 3: opacity-transition consent — transition disabled under reduced-transparency, base opacity stays 0
A consent element that fades in using a CSS transition (opacity: 0 → opacity: 1 over 300ms) relies on the transition completing to become visible. The CSS prefers-reduced-motion and prefers-reduced-transparency settings both provide a reason to disable transitions. When the transition is disabled and the base opacity is 0, the consent element never becomes visible — the transition that would trigger visibility never fires.
/* Attack: opacity transition disabled, base stays 0 */
.consent-panel {
opacity: 0;
transition: opacity 300ms ease-in;
}
.consent-panel.visible {
opacity: 1;
}
@media (prefers-reduced-transparency: reduce) {
.consent-panel {
transition: none; /* "respecting user preferences" */
/* base opacity: 0 — not overridden in this block.
JS code adds .visible class, but without transition opacity stays at 0 immediately.
Wait: .visible sets opacity:1 directly — so transition:none alone won't hide it
UNLESS the author also forgot to add opacity:1 to the no-transition fallback.
Combined with attack: base opacity stays 0 and .visible is never applied: */
}
/* Subtler variant: */
.consent-panel.visible {
opacity: 0; /* override: visible class doesn't restore opacity under reduced-transparency */
}
}
// Detection: opacity:0 base with transition under prefers-reduced-transparency
function auditReducedTransparencyOpacityTransition(consentEl) {
const cs = getComputedStyle(consentEl);
// Check if consent element has opacity:0 base
if (parseFloat(cs.opacity) === 0) {
// Check for prefers-reduced-transparency rule that overrides opacity
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (rule.type !== CSSRule.MEDIA_RULE) continue;
const mq = rule.conditionText || rule.media.mediaText;
if (!/prefers-reduced-transparency/.test(mq)) continue;
for (const inner of rule.cssRules) {
if (inner.type !== CSSRule.STYLE_RULE) continue;
try { if (!consentEl.matches(inner.selectorText)) continue; }
catch (e) { continue; }
if (inner.style.opacity === '0' || inner.style.transition === 'none') {
console.warn('[SkillAudit] prefers-reduced-transparency rule on consent element:',
'transition:none with base opacity:0 may prevent consent from becoming visible;',
'media:', mq, 'selector:', inner.selectorText, 'current opacity:', cs.opacity);
}
}
}
} catch (e) {}
}
}
}
Attack 4: JS matchMedia(prefers-reduced-transparency) consent swap
JavaScript can query window.matchMedia('(prefers-reduced-transparency: reduce)') directly and replace the consent button with a non-interactive element for transparency-reduction users. A change listener allows the swap to fire dynamically if the user toggles the setting while the page is open.
// Attack: JS prefers-reduced-transparency detection + consent removal
const reducedTransparencyMQL = window.matchMedia('(prefers-reduced-transparency: reduce)');
function applyTransparencyMode(isReduced) {
const consentEl = document.querySelector('.consent-banner');
if (!consentEl) return;
if (isReduced) {
// "Simplified layout for accessibility..."
const simple = document.createElement('div');
simple.className = 'consent-simplified'; // no click handlers
simple.setAttribute('aria-hidden', 'true');
consentEl.replaceWith(simple);
}
}
applyTransparencyMode(reducedTransparencyMQL.matches);
reducedTransparencyMQL.addEventListener('change', e => applyTransparencyMode(e.matches));
// Detection: JS source scan for prefers-reduced-transparency + consent manipulation
function auditReducedTransparencyJS() {
for (const script of document.querySelectorAll('script')) {
const src = script.textContent;
if (!src) continue;
if (!/prefers-reduced-transparency/.test(src)) continue;
if (!/consent|banner|modal|btn|permission/i.test(src)) continue;
const hasManipulation = [
/replaceWith|replaceChild|createElement/,
/\.remove\(\)/,
/style\.(display|opacity|visibility)\s*=/,
/setAttribute.*disabled/,
/pointer-events.*none/,
].some(p => p.test(src));
if (hasManipulation) {
console.warn('[SkillAudit] script uses prefers-reduced-transparency matchMedia with consent DOM manipulation;',
'current reduced-transparency state:', reducedTransparencyMQL.matches,
'script:', script.src || '(inline)');
}
}
}
Findings summary
SkillAudit audits prefers-reduced-transparency media rules on consent elements, checks for backdrop-filter removal without opaque fallback, and scans JavaScript for matchMedia transparency checks combined with DOM manipulation. Run a free audit on your MCP server.