MCP server CSS background overlay consent security
Four SA-CSS-BGO consent attack patterns using CSS background properties, pseudo-elements, and backdrop-filter to place white or near-white overlays over consent text. A common misconception is that background-color: white on a text element covers its text — it does not, because CSS paints backgrounds before content in the box model. The real attacks use ::before and ::after pseudo-elements as white positioned layers stacked above consent text, or backdrop-filter: brightness() to wash out the visual contrast of the consent dialog.
The CSS box model paints in a specific order: background colors and images are painted first, then borders, then content (text). Setting background-color: white on an element that contains black text does not cover the text — the text is painted on top of the background. To create a white layer that actually covers text, an attacker must use a separate element (real or pseudo) that is positioned above the text in the stacking order. ::before and ::after pseudo-elements are ideal for this: they can be styled as white positioned boxes that stack above the text they are associated with, and they are largely invisible to audit tools that query only real DOM elements rather than computed pseudo-element styles.
SA-CSS-BGO-001: ::before white overlay covering consent text
The ::before pseudo-element is injected as the first child of the target element in the rendering tree. When positioned absolutely with inset: 0 (or explicit top/right/bottom/left) and a higher z-index than the element’s text content, and styled with a white or near-white background, it physically covers the text. The text remains in the DOM and is fully accessible via textContent, innerText, and accessibility APIs, but is visually obscured by the white pseudo-element layer.
.consent-text {
position: relative; /* establishes stacking context for ::before absolute position */
}
.consent-text::before {
content: "";
position: absolute;
inset: 0; /* top:0; right:0; bottom:0; left:0 — covers entire element */
background: white;
z-index: 1; /* above the element's content layer */
}
/* CSS painting order for a positioned element with ::before:
1. Element background (bottom layer)
2. Element borders
3. ::before content (if positioned with z-index ≥ 1, above text content)
4. Element text content (painted in the element's content layer)
5. ::after content
Wait — if ::before is painted before the text, how does it cover it?
Answer: z-index on ::before.
Without z-index (or z-index:auto):
- ::before is painted before text in document order → text renders on top → visible
- This is the normal case: ::before is a decorative layer BEHIND text
With z-index:1 on ::before (positive z-index):
- ::before is promoted to a new stacking layer
- Positive z-index: rendered ABOVE in-flow content
- ::before white layer is painted OVER the element's text content
- White overlay covers consent text
DOM accessibility:
- element.textContent / innerText: returns consent text (not affected by ::before)
- ARIA accessibility tree: text is accessible (::before with content:"" is not readable)
- Screen readers: read the consent text from the accessibility tree
getWindow().getComputedStyle(element, '::before').content: returns '""'
getWindow().getComputedStyle(element, '::before').backgroundColor: returns 'rgb(255, 255, 255)'
getWindow().getComputedStyle(element, '::before').position: returns 'absolute'
getWindow().getComputedStyle(element, '::before').inset: returns '0px'
getWindow().getComputedStyle(element, '::before').zIndex: returns '1'
Detection:
For all consent text elements:
1. Query pseudo-element styles: getComputedStyle(el, '::before')
2. If position is absolute/fixed AND background near-white AND zIndex > 0 AND
inset covers the element area (all 4 sides 0 or negative)
3. → WHITE OVERLAY ATTACK: HIGH to CRITICAL SA-CSS-BGO-001
Also check ::after pseudo-element with same criteria (SA-CSS-BGO-004) */
Pseudo-elements are invisible to element queries: document.querySelectorAll('*') does not return pseudo-elements. document.getElementById() returns the host element, not its pseudo-elements. The only way to inspect pseudo-element styles is via window.getComputedStyle(element, '::before') and window.getComputedStyle(element, '::after'). An audit that checks all DOM elements for white backgrounds will completely miss a white ::before overlay attack. Consent security audits must explicitly check both pseudo-elements on all consent text elements.
::before pseudo-element with background: white + position: absolute; inset: 0; z-index: 1 covers consent text; invisible to DOM element queries; detectable only via getComputedStyle(el, '::before'). Detection: check both pseudo-elements on all consent text elements for near-white background + positive z-index + inset:0 → CRITICAL.SA-CSS-BGO-002: backdrop-filter:brightness(2) washing consent dialog
The backdrop-filter property applies graphic effects to the area behind a semi-transparent element. When a consent dialog has a semi-transparent background (e.g., background: rgba(255,255,255,0.8)), backdrop-filter: brightness(2) applied to it amplifies the brightness of whatever is behind the dialog by a factor of 2. On a page with a gray or off-white background, this can produce a near-white backdrop that, when composited through the semi-transparent dialog background, results in an effective background behind the consent text that approaches white. If the consent text is dark on an expected slightly-gray background, the brightness amplification erases the contrast.
.consent-dialog {
background: rgba(255, 255, 255, 0.15); /* very transparent white */
backdrop-filter: brightness(2); /* amplify background brightness */
}
/* How backdrop-filter:brightness(2) erases consent text contrast:
Scenario: page background is rgb(220, 220, 220) (light gray)
consent-dialog background: rgba(255,255,255,0.15)
consent text: rgb(0, 0, 0) — black text
backdrop-filter:brightness(2) applied to region behind dialog:
Page bg after filter: rgb(220*2, 220*2, 220*2) = rgb(440, 440, 440) → clips to rgb(255,255,255)
(brightness values above 100% clip to 255 in each channel)
After clipping: the backdrop is effectively rgb(255,255,255) = white
Compositing:
Semi-transparent dialog (alpha=0.15) over white backdrop:
Dialog bg = rgba(255,255,255,0.15) over white:
R = 255 * 0.15 + 255 * (1-0.15) = 38.25 + 216.75 = 255
→ Dialog background appears white
Black consent text on this white background:
If the text color contrast was calculated for a gray page background,
the actual rendered contrast is now much lower or white-on-white.
Note: the consent text itself is rendered ON TOP of the dialog,
so the text color is not directly affected by backdrop-filter.
The attack works by making the effective background white,
which (if text was not expected to be on pure white) reduces contrast.
More direct attack: if the consent text uses a light color (e.g., rgba(0,0,0,0.4))
on an expected non-white background, and backdrop-filter:brightness(2) makes
the background white, the faded text color becomes nearly invisible.
Detection:
Check backdrop-filter on consent dialog container ancestors.
If backdrop-filter contains brightness() with value > 1.5 AND
dialog background is semi-transparent (alpha < 0.5) → risk of contrast erasure.
Compute effective background color: apply brightness filter to page background,
composite through dialog background alpha → check contrast of consent text color
against this effective background → SA-CSS-BGO-002 if below 4.5:1 WCAG AA */
backdrop-filter affects behind-element pixels, not element content: backdrop-filter processes the pixels behind a semi-transparent element, not the element’s own content. The attack path is indirect: amplify the brightness of what’s behind the dialog to make the effective dialog background color approach white, then rely on the consent text having low absolute color value (near-white or light gray) to become invisible. Detection must compute the effective dialog background after backdrop-filter compositing and check consent text contrast against that computed background.
backdrop-filter: brightness(2) on semi-transparent consent dialog: amplifies backdrop to near-white; light-colored consent text loses contrast against white effective background. Detection: compute effective bg after backdrop-filter + dialog alpha compositing; check text contrast against result → HIGH if below 4.5:1.SA-CSS-BGO-003: background-blend-mode:lighten with white background-image layer
The CSS background-blend-mode property blends multiple background layers of an element together, using the same blend formulas as mix-blend-mode and SVG feBlend. An element can have multiple background layers (background-image, linear-gradient, etc.) composited with background-blend-mode. Setting the first background layer to a white linear-gradient and background-blend-mode: lighten on the second layer (the actual visible background color) produces a lighten blend of white over the background color, which results in white. This creates an all-white background on the consent dialog container, which combined with black text may seem fine — but if the text was intentionally made near-white by other means, the white background erases its contrast.
.consent-container {
/* Multiple background layers:
Layer 1 (topmost): white gradient
Layer 2: the actual background-color equivalent (transparent, shows through) */
background-image: linear-gradient(white, white); /* first layer: white */
background-color: rgb(30, 30, 30); /* element background-color (bottom layer) */
background-blend-mode: lighten; /* blend first layer with background-color */
}
.consent-text {
color: rgba(255, 255, 255, 0.3); /* very light white text, 30% opacity */
}
/* background-blend-mode mechanics:
background-image layers and background-color are blended using background-blend-mode.
background-blend-mode:lighten:
Layer 1 (white): R=1.0, G=1.0, B=1.0
Layer 2 (background-color, dark): R=0.12, G=0.12, B=0.12
max(1.0, 0.12) = 1.0 → blended background = white
Result: container has an effectively white background.
consent-text color: rgba(255,255,255,0.3) → very light white text on white background
Contrast: near zero → invisible.
Why use background-blend-mode instead of just setting background:white?
- background-blend-mode with a dark background-color appears as a "legitimate" dark dialog
in screenshots taken without rendering (e.g., text-dump audits)
- The dark background-color is what getComputedStyle returns as the base background
- Only the composited rendering (dark + white lighten = white) produces the white output
- Static background-color inspectors see "rgb(30,30,30)" — dark — not white
Detection:
Check background-blend-mode on consent container ancestors.
If background-blend-mode includes lightening modes (lighten, screen, color-dodge)
AND there are multiple background layers including a near-white/white layer:
Compute the blended background color using the blend formula.
If result luminance > 0.9 AND consent text color luminance > 0.8
→ contrast is near zero → CRITICAL SA-CSS-BGO-003 */
getComputedStyle reports pre-blend background: getComputedStyle(el).backgroundColor returns the background-color CSS property value, not the composited result after background-blend-mode processing. An audit that reads background color via CSSOM will see the dark background-color and miss the fact that a background-blend-mode: lighten with a white background-image layer produces an all-white rendered background. Detection must inspect all background layers and apply the blend formula to compute the effective rendered background.
background-blend-mode: lighten with white background-image layer over dark background-color: rendered background becomes white; getComputedStyle reports dark background-color; near-white text becomes invisible. Detection: enumerate all background layers + blend mode; compute composited background; check text contrast → CRITICAL.SA-CSS-BGO-004: ::after pseudo-element white cover with inset:0
The ::after pseudo-element works identically to ::before for overlay attacks but is painted after the element’s text content in document order, making it the default topmost layer in non-positioned contexts. Without a z-index, ::after with position: absolute still renders above in-flow text because positioned elements create new stacking contexts at their position in the stacking order. This means a white ::after pseudo-element can cover consent text even without an explicit z-index declaration, making it harder to detect by a scanner that only checks for z-index > 0.
.consent-text {
position: relative;
}
.consent-text::after {
content: "";
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.97); /* near-opaque white */
/* No z-index declaration: ::after is still above in-flow text content
because ::before/::after flow in document order and are painted
after the element's content in the same stacking context,
unless text itself is z-index elevated */
}
/* ::after vs ::before stacking:
Both ::before and ::after with position:absolute and no z-index:
- Are painted in stacking context order based on position in the element tree
- ::after is generated after element content in the source order
- In the same stacking context: source order determines which appears on top
- ::after content (no z-index, position:absolute) renders on top of the
element's in-flow text content by default in modern browsers
This is the subtle point: no explicit z-index:1 is needed on ::after
if the text has no z-index (both are in the same stacking context;
::after in document order is "later" = on top).
Detection implication:
A scanner checking: getComputedStyle(el,'::after').zIndex === '1'
will miss a z-index-free ::after attack.
Correct check: any ::after with position absolute/fixed + near-white background
+ dimensions covering the parent element = overlay attack,
regardless of z-index value.
Additional evasion: inset computed as 0px vs declared as negative values
to extend slightly beyond the parent:
.consent-text::after {
inset: -2px; /* 2px outside parent boundaries: covers full text + margin */
}
→ getComputedStyle reports "-2px" for each edge; still detected by any-inset-coverage check
near-opaque detection:
background: rgba(255,255,255,0.97) — same as SA-SVG-FFL-003 evasion
WCAG contrast: effectively invisible against white page bg
Scanner threshold: flag any ::after background with opacity > 0.85 near white → HIGH */
Check ::after without assuming z-index: Detection for pseudo-element overlay attacks should not require a specific z-index value. The key signatures are: (1) position: absolute or position: fixed, (2) near-white background-color or background, and (3) dimensions that cover the parent element area (all four inset values ≤ 0px, or explicit width/height ≥ parent dimensions). The presence of these three properties in either ::before or ::after is sufficient to flag a potential overlay attack regardless of z-index value.
::after pseudo-element with near-white background: rgba(255,255,255,0.97) + position: absolute; inset: 0 covers consent text without explicit z-index; detectable only via getComputedStyle(el, '::after'). Detection: check ::after position + near-white background + inset coverage regardless of z-index → CRITICAL.Summary of SA-CSS-BGO findings
| ID | Severity | Technique | Mechanism | Detection gap |
|---|---|---|---|---|
| SA-CSS-BGO-001 | CRITICAL | ::before white overlay z-index:1 | Pseudo-element white layer above text; DOM element queries miss it | Must check getComputedStyle(el, '::before') |
| SA-CSS-BGO-002 | HIGH | backdrop-filter:brightness(2) | Amplifies backdrop to near-white; erases contrast for light-colored text | Must compute effective bg after backdrop compositing |
| SA-CSS-BGO-003 | CRITICAL | background-blend-mode:lighten + white bg-image layer | Lighten blend of white over dark bg → white; getComputedStyle reports dark bg | Must enumerate bg layers and apply blend formula |
| SA-CSS-BGO-004 | CRITICAL | ::after near-white cover without z-index | Pseudo-element covers text; no z-index needed; DOM queries miss it | Check ::after position + bg regardless of z-index |