MCP server CSS linear-gradient() security: background-clip:text transparent consent, ::before fade overlay, repeating-linear-gradient stripe pattern, and JS mousedown gradient injection

Published 2026-08-07 — SkillAudit Research

The CSS linear-gradient() function generates a smooth color transition between two or more stops along a straight axis. While gradient images are typically applied as decorative backgrounds, they can be weaponized against MCP consent disclosure elements through several mechanisms: using background-clip: text to make text render in gradient colors (including fully transparent), applying gradient overlays via pseudo-elements to fade consent text from view, and using repeating-linear-gradient() to create dense stripe patterns that block individual lines of consent text.

These attacks exploit the fact that gradients are applied as background-image values — properties that most consent-visibility scanners do not inspect when checking for text legibility. Standard checks for color, opacity, visibility, and display all pass normally; the gradient-based obscuration operates at the rendering layer. See also CSS radial-gradient() attacks, CSS background-image attacks, and CSS color:transparent attacks for related techniques.

background-clip:text technique: When background-clip: text (and its prefixed variant -webkit-background-clip: text) is combined with a transparent color value (or -webkit-text-fill-color: transparent), the element's text becomes a cutout showing the background through it. If that background is a transparent gradient, the text renders as transparent against whatever is behind the element — effectively invisible if the page background is a solid color behind it. This is the legitimate "gradient text" CSS technique; the attack repurposes it with a fully-transparent gradient.

Attack 1: background-clip:text + transparent linear-gradient — consent text rendered fully invisible (SA-CSS-LGRAD-001)

The consent element sets -webkit-text-fill-color: transparent (or color: transparent), -webkit-background-clip: text, background-clip: text, and background-image: linear-gradient(to right, transparent, transparent). The text characters are punched out of the element's background — but the background itself is a gradient from transparent to transparent. The text region shows what is behind the element: if the dialog sits on a white page, the "text" shows white — making it invisible. The element has a valid color computed value (the transparent color), non-zero dimensions, and visible layout. Standard color checks that read getComputedStyle(el).color see the transparent value but may not account for the -webkit-text-fill-color override, which takes precedence over color in WebKit/Blink engines.

/* MCP attack: */
.consent-disclosure {
  color: black;                                      /* computed color = black; passes color check */
  -webkit-text-fill-color: transparent;              /* overrides color in rendering — text transparent */
  -webkit-background-clip: text;
  background-clip: text;
  background-image: linear-gradient(to right, transparent 0%, transparent 100%);
  /* Text rendered as fully transparent cutout
     getComputedStyle(el).color returns 'rgb(0,0,0)' — misleads color scanners
     getComputedStyle(el).webkitTextFillColor returns 'rgba(0,0,0,0)' — the actual render color */
}

// Detection:
function detectGradientTextTransparent(el) {
  const cs = window.getComputedStyle(el);

  // Check -webkit-text-fill-color (takes precedence over color in Blink/WebKit)
  const textFillColor = cs.webkitTextFillColor || cs.getPropertyValue('-webkit-text-fill-color');
  const bgClip = cs.webkitBackgroundClip || cs.backgroundClip;
  const bgImage = cs.backgroundImage;

  if (bgClip === 'text' && bgImage && bgImage !== 'none') {
    // Gradient applied as text mask — check if gradient is transparent
    if (bgImage.includes('transparent') || textFillColor === 'rgba(0, 0, 0, 0)') {
      console.error('SA-CSS-LGRAD-001: background-clip:text with transparent gradient — consent text invisible', {
        el, textFillColor, bgClip, bgImage: bgImage.slice(0, 80)
      });
    }
  }
}

Attack 2: ::before pseudo-element with linear-gradient fade overlay — consent fades from view (SA-CSS-LGRAD-002)

The consent element's parent has a ::before pseudo-element positioned absolutely over the consent region with background: linear-gradient(to bottom, transparent 0%, white 35%) and pointer-events: none. The gradient fades from transparent at the top to opaque white at 35% of the consent height. Because the consent text is typically structured with the most critical "I agree to install and grant permissions" language in the first third of the element, a 35% fade precisely targets and obscures the key consent clause. The remaining text (generic boilerplate) is visible below the fade region, giving the appearance that the consent is readable. The pseudo-element cannot be detected by inspecting the consent element itself — its properties are on the parent or a sibling wrapper element's ::before pseudo-element.

/* MCP attack: */
.consent-wrapper {
  position: relative;
}
.consent-wrapper::before {
  content: '';
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 40%;                  /* covers top 40% of consent — where key clause appears */
  background: linear-gradient(to bottom,
    transparent 0%,
    rgba(255, 255, 255, 0.97) 35%
  );                            /* nearly-opaque white at 35% mark */
  pointer-events: none;         /* clicks still reach consent element */
  z-index: 2;                   /* sits above consent text */
}
.consent-disclosure { position: relative; z-index: 1; }

// Detection — check parent/sibling pseudo-element overlays:
function detectPseudoElementGradientOverlay(consentEl) {
  // Pseudo-elements can't be directly inspected; check parent's ::before/::after
  const parent = consentEl.parentElement;
  if (!parent) return;

  const beforeCS = window.getComputedStyle(parent, '::before');
  const afterCS  = window.getComputedStyle(parent, '::after');

  for (const [label, pcs] of [['::before', beforeCS], ['::after', afterCS]]) {
    if (pcs.content === 'none' || pcs.content === '') continue;
    const bgImage = pcs.backgroundImage;
    if (!bgImage || bgImage === 'none') continue;

    if (bgImage.startsWith('linear-gradient') || bgImage.startsWith('-webkit-linear-gradient')) {
      if (pcs.position === 'absolute' || pcs.position === 'fixed') {
        console.error('SA-CSS-LGRAD-002: parent ' + label + ' gradient overlay covers consent', {
          parent, pseudoCS: { bgImage: bgImage.slice(0, 80), position: pcs.position, zIndex: pcs.zIndex }
        });
      }
    }
  }
}

Attack 3: repeating-linear-gradient white stripe pattern — each text line blocked (SA-CSS-LGRAD-003)

repeating-linear-gradient(180deg, white 0px, white 14px, transparent 14px, transparent 20px) applied to a pseudo-element overlay creates horizontal white stripes repeating at every 20px interval — 14px of opaque white followed by 6px of transparent. Given typical line heights of 18–22px for body text, each 14px white band covers one complete text line while the 6px transparent gap falls between lines. The consent text is completely obstructed by the white stripes at the line level: from a distance, the element appears as a white block with barely-visible text gaps, passing any pixel-area-based legibility check that doesn't examine the actual gradient pattern. The stripe cycle is tunable to match the precise line-height of the consent element's text.

/* MCP attack: */
.consent-wrapper {
  position: relative;
}
.consent-wrapper::after {
  content: '';
  position: absolute;
  inset: 0;
  background-image: repeating-linear-gradient(
    180deg,
    rgba(255, 255, 255, 0.96) 0px,
    rgba(255, 255, 255, 0.96) 14px,   /* white band covers one text line */
    transparent 14px,
    transparent 20px                   /* gap at inter-line space */
  );
  pointer-events: none;
  z-index: 10;
}

// Detection — check for repeating-linear-gradient with dense opaque bands:
function detectStripeOverlay(consentEl) {
  const toCheck = [consentEl, consentEl.parentElement].filter(Boolean);

  for (const el of toCheck) {
    for (const pseudo of ['', '::before', '::after']) {
      const cs = pseudo ? window.getComputedStyle(el, pseudo)
                        : window.getComputedStyle(el);

      if (pseudo && (cs.content === 'none' || cs.content === '')) continue;

      const bgImage = cs.backgroundImage || '';
      if (!bgImage.startsWith('repeating-linear-gradient')) continue;

      // Stripe pattern with opaque stops and short repeat cycle = text-blocking
      const hasOpaque = /rgba?\(\s*\d+,\s*\d+,\s*\d+,\s*0\.[789]\d*\s*\)|rgba?\(\s*255,\s*255,\s*255\s*\)/.test(bgImage);
      if (hasOpaque) {
        console.error('SA-CSS-LGRAD-003: repeating-linear-gradient stripe overlay blocks consent text lines', {
          el: el, pseudo: pseudo || 'element itself',
          bgImage: bgImage.slice(0, 100)
        });
      }
    }
  }
}

Attack 4: JS mousedown injects opaque linear-gradient overlay — consent covered at install click (SA-CSS-LGRAD-004)

At page load the consent is fully visible with no gradient applied. When the user presses the install button, a mousedown listener fires before the click event and sets an overlay element's background to linear-gradient(to bottom, white, white) — a fully opaque white "gradient" (functionally a solid white cover). The overlay element is positioned absolutely over the consent region with pointer-events: none. As the user's mouse button is held, the consent disappears under the white overlay. The browser registers the subsequent mouseup/click event while the consent is covered. MutationObserver on the overlay or consent's parent detects the style change; a snapshot comparison of the consent's rendered pixels before and after the mousedown event confirms the injection.

/* Setup: hidden overlay element pre-created: */
const overlay = document.createElement('div');
overlay.style.cssText = `
  position: absolute; inset: 0; pointer-events: none;
  z-index: 100; background: none;
`;
document.querySelector('.consent-wrapper').appendChild(overlay);

// MCP JS — fires at mousedown:
document.querySelector('#install-btn').addEventListener('mousedown', () => {
  overlay.style.background = 'linear-gradient(to bottom, white 0%, white 100%)';
  /* Consent instantly covered by solid white overlay
     Appears as if dialog progressed to next step */
}, { capture: true });

// Detection:
function detectDynamicGradientInjection(consentEl) {
  const wrappers = [consentEl, consentEl.parentElement].filter(Boolean);

  for (const el of wrappers) {
    new MutationObserver((mutations) => {
      for (const mut of mutations) {
        if (mut.type === 'attributes' && mut.attributeName === 'style') {
          const cs = window.getComputedStyle(el);
          const bgImage = cs.backgroundImage || '';
          if (bgImage.includes('linear-gradient')) {
            console.error('SA-CSS-LGRAD-004: linear-gradient injected dynamically on consent area', {
              el, bgImage: bgImage.slice(0, 80), attribute: mut.attributeName
            });
          }
        }
        // Also watch for child overlay element insertion
        if (mut.type === 'childList') {
          for (const node of mut.addedNodes) {
            if (node.nodeType === 1) {
              const addedCS = window.getComputedStyle(node);
              if ((addedCS.backgroundImage || '').includes('linear-gradient') &&
                  addedCS.position === 'absolute') {
                console.error('SA-CSS-LGRAD-004: absolute gradient overlay element injected over consent', { node });
              }
            }
          }
        }
      }
    }).observe(el, { attributes: true, attributeFilter: ['style'], childList: true });
  }

  document.querySelector('#install-btn, [data-action="install"]')
    ?.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
}

Root detection method: Inspect getComputedStyle(el).backgroundImage and check for linear-gradient or repeating-linear-gradient containing transparent stops. Separately check getComputedStyle(el).webkitTextFillColor (the -webkit-text-fill-color property) — if transparent, text is invisible regardless of color value. Check parent ::before and ::after pseudo-elements for gradient overlays. SkillAudit checks all four gradient attack vectors on every consent element and its ancestors.

Attack summary

IDTechniquecolor checkopacity checkbackground-image checkSeverity
SA-CSS-LGRAD-001background-clip:text + transparent gradient — text invisiblemisleads (reports black)passesrequiredHigh
SA-CSS-LGRAD-002::before pseudo gradient fade overlay 35% — key clause obscuredpassespassesparent pseudo checkHigh
SA-CSS-LGRAD-003repeating-linear-gradient white stripe — each text line blockedpassespassesrequiredHigh
SA-CSS-LGRAD-004JS mousedown injects opaque gradient overlay at install clickpassespassesrequired (dynamic)High

Consolidated findings

High SA-CSS-LGRAD-001 — background-clip:text; -webkit-text-fill-color:transparent; background-image:linear-gradient(transparent,transparent); text invisible; getComputedStyle.color misleads (reports non-transparent)
High SA-CSS-LGRAD-002 — parent ::before; position:absolute; background:linear-gradient(transparent 0%, white 35%); covers top 35% of consent; key "I agree" clause obscured; pointer-events:none
High SA-CSS-LGRAD-003 — repeating-linear-gradient; 14px opaque white bands at 20px intervals; each text line blocked; looks like a striped background; entire consent unreadable
High SA-CSS-LGRAD-004 — JS mousedown; injects linear-gradient(white,white) overlay; consent covered at install click; MutationObserver + child insertion watch required

See also: CSS radial-gradient() attacks | CSS background-image attacks | CSS color:transparent attacks | CSS backdrop-filter attacks | SkillAudit — free MCP server audit