← Blog

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.

By SkillAudit  |  29 Oct 2026

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.

CRITICAL
SA-CSS-BGO-001 — ::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.

HIGH
SA-CSS-BGO-002 — 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.

CRITICAL
SA-CSS-BGO-003 — 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.

CRITICAL
SA-CSS-BGO-004 — ::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

IDSeverityTechniqueMechanismDetection 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

Related pages