MCP server CSS repeating-linear-gradient security: stripe-average contrast wash, sub-pixel physical stripe, diagonal camouflage, and DPR-calibrated attacks on consent disclosures

Published 2026-09-18 — SkillAudit Research

CSS repeating-linear-gradient() extends linear-gradient() by tiling the defined color-stop pattern indefinitely from the starting edge to the ending edge of the element. Where a plain linear-gradient(white, transparent) creates a single fade across the element, repeating-linear-gradient(white 0 2px, transparent 2px 4px) creates a pattern that repeats every 4px, filling the entire element with alternating 2px white and 2px transparent stripes.

For MCP server attacks on consent disclosures, this tiling behavior creates two distinct threat models. First, fine stripe patterns at high repetition rates average to a near-uniform background — the visual average of white and the underlying page background — which degrades text contrast without any element becoming hidden. Second, hard-stop stripe patterns can be calibrated to precisely match text positions at specific viewport sizes or device pixel ratios, placing white stripes exactly over text glyphs.

Browser support: repeating-linear-gradient() is supported in all modern browsers and has been since Chrome 26 / Firefox 16 / Safari 6.1. It is available wherever linear-gradient() is available. No experimental flags required.

Attack 1: fine stripe averaging — contrast wash via high-frequency white/transparent stripes

A repeating-linear-gradient with alternating white and transparent stripes at a very small repeat interval produces a visual blend. At 1px white + 1px transparent repeat, the human eye perceives a uniform light grey — the average of white (#fff) and whatever is behind the element. If the page background is white or light, the stripe average approximates white. Text with any color lighter than pure black becomes illegible on this averaged background:

/* Attack 1: high-frequency stripe average destroys text contrast */

/* 1px white / 1px transparent — visual average ≈ white at reading distance */
.consent-dialog {
  background: repeating-linear-gradient(
    to right,
    white 0 1px,
    transparent 1px 2px
  );
}

/* Slightly less aggressive: 2px white / 2px transparent */
.consent-dialog {
  background: repeating-linear-gradient(
    to right,
    rgba(255, 255, 255, 0.92) 0 2px,
    rgba(255, 255, 255, 0.04) 2px 4px
  );
  /* Both stops are near-white — the average is extremely light.
     Text with color: #aaa or any light color becomes invisible.
     Text with color: #333 (dark grey, not pure black) has contrast ratio
     against the averaged background of approximately 1.4:1 — below the 4.5:1
     minimum for WCAG AA. */
}

/* Background-matching stripe: stripes alternate between white and the page background */
/* If the page background is rgb(248, 248, 248) = #f8f8f8: */
.consent-dialog {
  background: repeating-linear-gradient(
    to bottom,
    #f8f8f8 0 1px,    /* page background color */
    white   1px 2px   /* white */
  );
  /* Average of #f8f8f8 and #ffffff = #fcfcfc ≈ near-white.
     Text color: #aaa = rgb(170,170,170). Contrast against #fcfcfc ≈ 1.2:1. Invisible. */
}

// Detection: check backgroundImage for repeating-linear-gradient with white or near-white stops
function detectRepeatingLinearGradientWash(el) {
  const cs = window.getComputedStyle(el);
  const bg = cs.backgroundImage || '';
  if (!bg.includes('repeating-linear-gradient(')) return false;

  const hasLightStop = bg.match(/white|#f{3,6}|rgba?\(25[0-5],\s*25[0-5],\s*25[0-5]/i);
  if (hasLightStop) {
    console.warn('SECURITY: consent element has repeating-linear-gradient with near-white stops', {
      element: el,
      backgroundImage: bg.substring(0, 200),
      textColor: cs.color
    });
    return true;
  }
  return false;
}

Attack 2: sub-pixel physical stripe on high-DPR Retina displays

On a 2× DPR (Retina) screen, 0.5px CSS = 1 physical pixel. A repeating-linear-gradient with 0.5px white stripe and 0.5px transparent will render as 1-physical-pixel alternating stripes. At 3× DPR (iPhone Pro), 0.33px CSS = 1 physical pixel. Sub-pixel stripes render as genuine physical pixel stripes on Retina — creating a fine-grained halftone background at the pixel level. On a 1× audit monitor, 0.5px CSS rounds to 1px and the pattern looks like 1:1 stripes. But on a 2× device, the visual density doubles, and the text contrast degradation is much more severe:

/* Attack 2: sub-pixel stripe calibrated for 2× and 3× DPR */

/* On a 2× DPR screen: 0.5px CSS = 1 physical pixel */
.consent-dialog {
  background: repeating-linear-gradient(
    to right,
    white      0    0.5px,  /* 1 physical pixel on 2× */
    transparent 0.5px  1px  /* 1 physical pixel transparent */
  );
  /* On 1× screen: both strips round to 1px, pattern looks normal.
     On 2× screen: alternating 1-physical-pixel white/transparent stripes.
     Human visual system perceives the averaged color — near-white wash.
     Text contrast ratio drops from readable (e.g., 7:1) to near-unreadable (1.5:1). */
}

/* 3× DPR variant for iPhone Pro / Pixel Pro */
.consent-dialog {
  background: repeating-linear-gradient(
    to right,
    white       0      0.33px,  /* 1 physical pixel on 3× */
    transparent 0.33px 0.66px
  );
}

/* JS detection: check current DPR and flag sub-pixel repeating gradients */
function detectSubPixelRepeatingGradient(el) {
  const cs = window.getComputedStyle(el);
  const bg = cs.backgroundImage || '';
  if (!bg.includes('repeating-linear-gradient(')) return false;

  const dpr = window.devicePixelRatio || 1;
  // Look for stop positions that are sub-pixel at the current DPR
  // A value of 0.5px is sub-pixel on 1× but renders as 1px physical on 2×
  const subPixelMatch = bg.match(/[\d.]+px/g);
  if (subPixelMatch) {
    const hasSubPixel = subPixelMatch.some(val => {
      const px = parseFloat(val);
      return px > 0 && px < (1 / dpr) + 0.1; // at or below 1 physical pixel
    });
    if (hasSubPixel) {
      console.warn('SECURITY: repeating-linear-gradient with sub-pixel stop on consent element', {
        element: el,
        dpr,
        backgroundImage: bg.substring(0, 200)
      });
      return true;
    }
  }
  return false;
}

Attack 3: diagonal stripe camouflage pattern

Most automated audits that check background images look for horizontal or vertical stripes. A repeating-linear-gradient at a diagonal angle (e.g., 45°) creates a diagonal stripe pattern. At a fine pitch (2–4px repeat), the diagonal stripes average to the same near-white wash as horizontal stripes. But the diagonal angle makes the pattern visually resemble a decorative texture — a common UI pattern for loading states, disabled elements, or watermarks — reducing the likelihood that a human reviewer flags it as an attack:

/* Attack 3: diagonal stripe camouflage */

/* Classic 45° diagonal stripe at 4px pitch */
.consent-dialog {
  background: repeating-linear-gradient(
    45deg,
    white       0 2px,
    transparent 2px 4px
  );
  /* Diagonal stripes at 2px white / 2px transparent.
     Averages to near-white. Looks like a common CSS loading/disabled texture.
     Text with color: #ccc becomes invisible. */
}

/* More deceptive: near-background color alternating with background color */
/* If page background is #fafafa: */
.consent-dialog {
  background: repeating-linear-gradient(
    -45deg,
    #fafafa 0 1px,  /* page background */
    #ffffff 1px 2px /* white */
  );
  /* The element's background blends with the page background.
     The stripes are virtually invisible against the page color.
     Any text not in pure black loses contrast with this background. */
}

/* Audit bypass: 45deg patterns are often excluded from gradient security scanners
   because they visually look like intended textures, not consent attacks.
   A CSS scanner looking only for display:none or opacity:0 will not flag this. */

// Detection
function detectDiagonalRepeatingGradient(el) {
  const cs = window.getComputedStyle(el);
  const bg = cs.backgroundImage || '';
  if (!bg.includes('repeating-linear-gradient(')) return false;

  // Flag diagonal gradients on consent elements regardless of stop colors
  if (bg.match(/\d+deg/) || bg.includes('to left') || bg.includes('to right') ||
      bg.includes('to top') || bg.includes('to bottom')) {
    const textLum = getLuminance(cs.color);
    // If any stripe stop is near-white and text is not pure black, flag it
    if (bg.match(/white|#f{3,6}/i)) {
      console.warn('SECURITY: repeating-linear-gradient diagonal camouflage on consent element', {
        element: el,
        backgroundImage: bg.substring(0, 200)
      });
      return true;
    }
  }
  return false;
}

function getLuminance(colorStr) {
  const m = colorStr.match(/\d+/g);
  if (!m || m.length < 3) return 1;
  const [r, g, b] = m.map(Number);
  const toLinear = c => { c /= 255; return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); };
  return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
}

Attack 4: background-color scanner bypass — gradient conceals actual fill

getComputedStyle(el).backgroundColor returns only the background-color CSS property — it does not sample the rendered background, which includes background-image. An element with background-color: transparent and a repeating-linear-gradient white stripe background-image will report a backgroundColor of rgba(0,0,0,0) (transparent) to CSS auditing tools. Scanners that check only backgroundColor to detect white backgrounds will not flag this attack. The gradient appears on screen as a near-white wash but is invisible to the CSS API's background-color accessor:

/* Attack 4: scanner bypass — backgroundColor is transparent, gradient creates white wash */

.consent-dialog {
  background-color: transparent;  /* getComputedStyle().backgroundColor = 'rgba(0, 0, 0, 0)' */
  background-image: repeating-linear-gradient(
    to right,
    rgba(255, 255, 255, 0.9)  0 1px,
    rgba(255, 255, 255, 0.02) 1px 2px
  );
  /* To the auditing tool: "no background color" — passes all background-color checks.
     To the user's eyes: near-white wash, consent text illegible. */
}

/* Compound attack: background-color is a dark color, but gradient overrides it for the
   part of the element where disclosure text is rendered */
.consent-dialog {
  background-color: #1a1a2e;  /* dark navy — looks "fine" to a background-color check */
  background-image: repeating-linear-gradient(
    to bottom,
    rgba(255, 255, 255, 0.0) 0px 40px,   /* top 40px: transparent overlay — dark bg shows */
    rgba(255, 255, 255, 0.9) 40px 80px,  /* next 40px: near-white overlay covers disclosure */
    rgba(255, 255, 255, 0.0) 80px 120px  /* remaining: transparent again */
    /* Non-repeating portion: only one cycle visible if dialog is 120px tall */
  );
  /* background-color: #1a1a2e passes color contrast checks (dark background, white text ✓)
     But the gradient overlay on the 40–80px vertical range covers the disclosure text
     with a near-white layer. Text there = near-white on near-white = invisible. */
}

// Detection: must check backgroundImage, not just backgroundColor
function auditRepeatingLinearGradientAttacks(el) {
  const cs = window.getComputedStyle(el);
  const bgImage = cs.backgroundImage || '';

  // WRONG approach (misses gradient attacks):
  // if (cs.backgroundColor === 'rgb(255, 255, 255)') { ... }

  // CORRECT approach:
  if (bgImage.includes('repeating-linear-gradient(')) {
    // Flag all repeating-linear-gradient instances on consent elements for review
    console.warn('SECURITY: consent element has repeating-linear-gradient background-image', {
      element: el,
      backgroundImage: bgImage.substring(0, 300),
      backgroundColor: cs.backgroundColor,
      color: cs.color
    });
    return true;
  }
  return false;
}

Scanner gap: getComputedStyle(el).backgroundColor does not include the gradient background image. An element can have a transparent background-color but a fully opaque white background-image gradient. Always check backgroundImage on consent elements — a repeating-linear-gradient with white or near-white stops is a high-confidence consent-bypass indicator regardless of the background-color value.

Attack summary

Attack Technique Effect Detection Severity
Fine stripe contrast wash repeating-linear-gradient(white 0 1px, transparent 1px 2px) Near-white average background destroys text contrast Flag white/near-white stops in repeating-linear-gradient on consent elements High
Sub-pixel DPR stripe 0.5px or 0.33px CSS stops → 1 physical pixel on 2×/3× screens Halftone wash on Retina passes 1× audit but degrades on mobile Check stop values vs devicePixelRatio; sub-pixel stops on consent elements are suspicious Medium
Diagonal camouflage 45deg or -45deg stripe pattern Looks like UI texture; human reviewer less likely to flag Flag any repeating-linear-gradient with angular direction and white/near-white stops Medium
Background-color scanner bypass background-color: transparent; background-image: gradient Passes all backgroundColor CSSOM checks while rendering white wash Always check backgroundImage, not just backgroundColor High

Consolidated findings

High CSS repeating-linear-gradient contrast-wash attack: MCP server applies background-image: repeating-linear-gradient(to right, white 0 1px, transparent 1px 2px) to the consent dialog. The fine stripe pattern averages to near-white at reading distance, destroying consent text contrast. getComputedStyle().backgroundColor returns transparent — the attack is invisible to background-color-only scanners. Detection: check backgroundImage for repeating-linear-gradient with white or near-white (#f{3,6}, rgb(25X, 25X, 25X)) stops on any consent element.
Medium CSS repeating-linear-gradient sub-pixel DPR stripe attack: MCP server uses stop positions smaller than 1 / devicePixelRatio CSS pixels (e.g., 0.5px on 2× DPR) to create a physical-pixel-level halftone background. The pattern degrades text contrast on Retina/high-DPR devices while appearing coarser (less damaging) on 1× audit screens. Detection: check gradient stop pixel values against current window.devicePixelRatio; flag when any stop width equals or is smaller than one physical pixel at the current DPR.
Medium CSS repeating-linear-gradient diagonal camouflage attack: MCP server applies a 45° or custom-angle repeating stripe with white/near-white stops. The diagonal texture resembles intentional UI decoration (loading indicator, watermark, disabled-state crosshatch), reducing human reviewer suspicion. Detection: flag all repeating-linear-gradient instances on consent elements regardless of angle; any non-zero-angle gradient with near-white stops warrants manual contrast audit.

← Blog  |  linear-gradient attacks  |  repeating-radial-gradient attacks