MCP server CSS filter:blur and filter:grayscale consent security
Four SA-CSS-BLR consent attack patterns that use CSS filter functions to make consent text visually unreadable, remove urgency signals from warning text, or produce black-on-black invisibility via inversion. Unlike color or opacity attacks, CSS filter attacks preserve the element's computed color property — getComputedStyle(el).color reports the correct dark text color even when the text is spatially blurred to the point of complete illegibility. This is the core reason filter-based attacks evade first-generation consent auditors: the auditable CSS property (color) is correct; the unaudited property (filter) destroys the visual result.
CSS filter functions apply pixel-level transformations to an element's rendered output after it has been painted. getComputedStyle does reflect the filter property — getComputedStyle(el).filter returns the filter string — but most consent audit engines do not inspect this property, making filter attacks a low-detection-rate vector in current tooling.
SA-CSS-BLR-001: filter:blur(10px) — consent text spatially unreadable
filter: blur(10px) on consent text applies a Gaussian blur with a 10-pixel sigma to the element's rendered pixels. The text glyphs are spread into overlapping halos: individual letterforms become indistinguishable at standard reading distances (60–80cm from a desktop monitor). The WCAG contrast ratio calculation uses the CSS color and background-color computed values — neither of which changes under filter: blur. A black-on-white consent text with 21:1 contrast ratio, blurred to complete illegibility, still reports 21:1 to a WCAG auditor.
.consent-text {
color: #111111; /* dark, WCAG compliant: 18.4:1 contrast */
background: white;
filter: blur(10px); /* text pixels spread to illegible halo */
}
/* WCAG automated checker result:
Contrast ratio: 18.4:1 ✓ passes AAA
Color: rgb(17, 17, 17) ✓ no anomaly
Opacity: 1 ✓ no anomaly
Visibility: visible ✓ no anomaly
→ NO violations reported
Visual result: completely illegible blur at 10px sigma
Detection:
getComputedStyle(el).filter → contains 'blur(' → parse sigma value
sigma > 2px on text element with font-size < 24px → CRITICAL
sigma > 1px on text element with font-size < 12px → CRITICAL */
WCAG passes, text is illegible: The WCAG contrast algorithm is defined in terms of CSS computed color values, not pixel-level readability. A filter: blur(40px) on white text would cause the text to disappear entirely into the background — the blur radius exceeds the glyph size — while reporting a passing contrast ratio based on the underlying color values. No WCAG automated checker today measures blur sigma against readability.
filter: blur(10px) on consent text: spatially illegible at standard reading distance; WCAG contrast passes on underlying color. Detection: getComputedStyle(el).filter contains blur( with sigma > 2px on normal body text → CRITICAL.SA-CSS-BLR-002: filter:blur(2px) on 8px font — small text threshold
The threshold at which blur causes illegibility scales with font size. Large display text (72px) at filter: blur(2px) is still legible: the 2px spread is small relative to the glyph height. Small body text at 8px with filter: blur(2px) is at the exact threshold where hairline glyph strokes (which may be 0.5–1px wide in the rendered font) spread to the full glyph height. The detection threshold must be relative: for a consent text element with small font size, a smaller blur radius is sufficient to cause illegibility.
.consent-text {
font-size: 8px; /* already at the lower end of readability */
color: #333;
filter: blur(2px); /* 2px blur on 8px font: glyph strokes fully spread */
}
/* Why this is used compound:
font-size: 8px alone: technically readable under magnification → MEDIUM
filter: blur(2px) on font-size:14px: mildly soft → LOW
font-size: 8px + filter: blur(2px): compound → letters indistinguishable → CRITICAL
Blur-to-font-size ratio detection:
const fontSize = parseFloat(getComputedStyle(el).fontSize);
const blurMatch = getComputedStyle(el).filter.match(/blur\((\d+\.?\d*)px\)/);
if (blurMatch) {
const sigma = parseFloat(blurMatch[1]);
const ratio = sigma / fontSize;
if (ratio > 0.2) → CRITICAL (blur exceeds 20% of font height)
} */
sigma / fontSize > 0.2 → CRITICAL.SA-CSS-BLR-003: filter:grayscale(1) — urgency signal removal from warning text
filter: grayscale(1) converts all color values in the filtered element to their luminance-equivalent grayscale. Red warning text — which carries high urgency signal due to the cultural and neurological association of red with danger — becomes a neutral gray with identical luminance. The consent text is still legible (no blur, no hidden pixels), but the visual urgency signal that would cause a user to read carefully and pause before proceeding is removed. A consent dialog with red warning text about “granting permanent file access” at 16:1 contrast becomes a gray-on-white text that reads as routine informational text.
.consent-warning {
color: #dc2626; /* red warning: urgency signal to user */
filter: grayscale(1); /* red → gray: urgency signal removed */
}
/* After grayscale filter:
Visual color: approximately rgb(75, 75, 75) luminance equivalent
Computed getComputedStyle(el).color: still rgb(220, 38, 38) (the CSS color)
→ CSS color check: red, no anomaly
→ Visual rendering: gray, urgency removed
The deception is semantic: not hiding the text, but removing the psychological
signal that would cause the user to read it attentively.
Detection:
getComputedStyle(el).filter contains 'grayscale(1)' or 'grayscale(100%)'
AND el color is a non-neutral hue (hue saturation > 30%) → HIGH
Reasoning: grayscale on text that was intentionally colored for semantic emphasis
removes that semantic emphasis; on already-neutral text → LOW */
Urgency manipulation is consent manipulation: The color red on warning text is not decoration — it is semantic information about severity. A user who reads gray-on-white text labeled “you authorize permanent file access” has a different cognitive and behavioral response than a user who reads the same text in red. Stripping urgency signal while preserving readability is a consent attack at the semantic layer, not the visual layer.
filter: grayscale(1) on semantically colored warning text: urgency signal removed; CSS color remains red but visual rendering is gray. Detection: grayscale in filter + CSS color hue saturation > 30% → HIGH.SA-CSS-BLR-004: filter:invert(1) on white text in white container — black-on-black
filter: invert(1) on an element inverts all pixel values: white (#fff = rgb(255,255,255)) becomes black (#000), dark text becomes light text, and the entire color map is reversed. The attack exploits a specific scenario: an element with white text (color: white) inside a white container. Applied naively, filter: invert(1) would produce black text on a still-white background — VISIBLE. But when the filter is applied to the consent text element (not just the text node), the element's background is also inverted: the white background becomes black. The result is black text on a black background — invisible.
.consent-text {
color: white; /* white text */
background: white; /* white background — identical, hidden as baseline */
/* Without filter: white text on white background → already invisible */
/* This is the baseline invisible state */
filter: invert(1); /* inverts: text → black; background → black */
/* Result: black text on black background → still invisible */
}
/* More subtle compound variant: */
.consent-dialog {
background: white; /* dialog background: white */
}
.consent-text {
color: #eee; /* near-white text — barely visible on white */
/* getComputedStyle: color = rgb(238,238,238) — detectable as near-white */
filter: invert(1); /* #eee inverts to #111 (dark) */
/* BUT: filter also inverts the local background rendering → white → black */
/* Result: #111 text on black background → invisible */
}
/* Detection:
getComputedStyle(el).filter contains 'invert(' → check inversion amount
invert(1) or invert(100%) on any text element → HIGH
Combined with color that is near-white (lightness > 80%) → CRITICAL
(near-white text + invert = near-black text on near-black background) */
filter: invert(1) on near-white text in white container: text color inverts to dark, background also inverts to dark → dark-on-dark invisible. Detection: invert(1) in filter + CSS color lightness > 80% → CRITICAL.Summary of SA-CSS-BLR findings
| ID | Severity | Filter function | Attack mechanism | Detection |
|---|---|---|---|---|
| SA-CSS-BLR-001 | CRITICAL | blur(10px) | Text spatially spread to illegible halo; WCAG contrast passes on underlying color | filter contains blur() with sigma > 2px on body text |
| SA-CSS-BLR-002 | CRITICAL | blur(2px) on 8px font | Blur sigma exceeds 20% of font height; hairline strokes spread to full glyph | sigma / fontSize > 0.2 |
| SA-CSS-BLR-003 | HIGH | grayscale(1) | Removes red/colored urgency signal; CSS color unchanged; semantic emphasis lost | grayscale in filter + CSS color hue saturation > 30% |
| SA-CSS-BLR-004 | CRITICAL | invert(1) | Near-white text + container background both inverted to near-black; invisible | invert(1) in filter + CSS color lightness > 80% |