CSS Gradient Attacks as MCP Consent Bypass: linear-gradient, radial-gradient, and Repeating Variants
Six CSS gradient functions — linear-gradient, radial-gradient, conic-gradient, and their three repeating variants — all share one exploit path against MCP install dialogs. Any gradient applied to background-image can be paired with background-clip:text to render consent text fully transparent, or placed on a pseudo-element to cover consent with opaque paint. The key detection blind spot: getComputedStyle(el).color reports a non-transparent color even when -webkit-text-fill-color is the actual render value.
Contents
- The gradient invariant
- All six gradient families
- Attack 1: transparent text via background-clip
- Attack 2: linear-gradient fade overlay
- Attack 3: repeating-linear-gradient stripe
- Attack 4: radial-gradient center obscuration
- Attack 5: repeating-radial-gradient concentric rings
- Attack 6: conic-gradient pie-slice wedge
- Attack 7: mask-image evasion (not backgroundImage)
- Attack 8: JS mousedown gradient injection
- Detection gaps
- Unified ConsentGradientAudit
The gradient invariant that links all six attack families
CSS gradient functions are image values. Wherever a <image> is accepted in CSS, a gradient function can appear: background-image, mask-image, border-image, list-style-image. Two of those properties are the primary attack surface:
- background-image — a gradient here combined with
background-clip: textand-webkit-text-fill-color: transparentrenders the element's text using the gradient as a fill. A fully-transparent gradient means fully-invisible text. The element still has display:block, visibility:visible, non-zero dimensions, and full textContent — every standard check passes. - mask-image — a gradient here controls which pixels of the element are composited into the page. A mask-image that is opaque over the consent text area and transparent everywhere else has the same visual result as
opacity:0, butgetComputedStyle(el).opacitystill returns1.
The six gradient functions are different shapes of the same brush. They produce different visual patterns — linear bands, radial ellipses, conic wedges, stripes, concentric rings — but the underlying mechanism is identical. A scanner that checks for linear-gradient by name but not radial-gradient has a trivially-exploitable gap. A scanner that checks backgroundImage but not maskImage has a gap. A scanner that checks color but not -webkit-text-fill-color has a gap.
Detection blind spot. When -webkit-text-fill-color: transparent is applied alongside a gradient background-clip, getComputedStyle(el).color returns the declared color value — often rgb(0,0,0) or another non-transparent color. The browser uses -webkit-text-fill-color as the actual glyph fill, overriding color for rendering purposes only. Checking color will always report a visible color. The check that reveals the attack is getComputedStyle(el).webkitTextFillColor, which returns rgba(0,0,0,0) — transparent — when the attack is active.
All six gradient function families
linear-gradient()
Straight-line color transition from one edge to another. Primary use: fade overlays, directional gradients on backgrounds.
radial-gradient()
Circular or elliptical color transition from center outward. Primary use: spotlight effects, radial glows, center highlights.
conic-gradient()
Angular color transition sweeping around a center point. Primary use: pie charts, color wheels, angular gradient backgrounds.
repeating-linear-gradient()
Repeating linear gradient tiles across the element. Primary use: diagonal stripes, hatching patterns, progress bar fills.
repeating-radial-gradient()
Repeating radial gradient tiles from center. Primary use: concentric rings, target patterns, radiating decorative effects.
repeating-conic-gradient()
Repeating conic gradient tiles angularly. Primary use: pinwheel patterns, angular checkerboards, radar sweep animations.
Transparent text via background-clip:text — works with all six gradient families
The -webkit-text-fill-color blind spot makes this the highest-severity gradient attack. getComputedStyle(el).color always misleads.
When a CSS gradient is placed on background-image alongside background-clip: text (or -webkit-background-clip: text), the browser uses the gradient as the glyph fill for the element's text. If the gradient is fully transparent — e.g., linear-gradient(transparent, transparent) or radial-gradient(circle, transparent, transparent) — the text is rendered with invisible ink. The element occupies its full layout space. Its textContent is intact. Its getBoundingClientRect() returns a non-zero, in-viewport rectangle. Everything a consent auditor typically checks passes.
The -webkit-text-fill-color mechanism
The attack requires one more property: -webkit-text-fill-color: transparent. This property, now supported in all major browsers, overrides color for the purpose of glyph rendering but does not change the value returned by getComputedStyle(el).color. A consent checker that reads color will see the legitimate color declaration — often #333 or rgb(0,0,0) — and conclude the text is visible. It is not.
Attack code (linear-gradient variant)
.consent-text {
/* declared color looks normal */
color: #1a1a1a;
/* background-clip:text uses gradient as glyph fill */
background-image: linear-gradient(transparent 0%, transparent 100%);
background-clip: text;
-webkit-background-clip: text;
/* actual render color — overrides `color` for glyphs */
-webkit-text-fill-color: transparent;
}
Swap linear-gradient for radial-gradient(circle, transparent, transparent), conic-gradient(from 0deg, transparent, transparent), or any repeating variant — the attack is identical. The gradient function name does not matter; the combination of background-clip: text and -webkit-text-fill-color: transparent is the exploit. Different gradient functions evade string-match scanners that check for a specific function name.
Detection
function hasTransparentTextGradient(el) {
const cs = getComputedStyle(el);
// getComputedStyle(el).color is unreliable — check webkitTextFillColor
const fillColor = cs.webkitTextFillColor;
if (!fillColor) return false;
// transparent or rgba(..., 0) means text is invisible
if (fillColor === 'transparent') return true;
const match = fillColor.match(/rgba\(\s*\d+,\s*\d+,\s*\d+,\s*([\d.]+)\)/);
if (match && parseFloat(match[1]) < 0.1) return true;
// also check that background-clip:text is active
const clip = cs.backgroundClip || cs.webkitBackgroundClip;
const bg = cs.backgroundImage;
if (clip === 'text' && bg && bg.includes('gradient')) return true;
return false;
}
SA-CSS-GRAD-001 (High). background-clip:text + -webkit-text-fill-color:transparent + any gradient function → consent text fully invisible. Evasion of getComputedStyle(el).color check. Affects all six gradient function families. The detection fix is always to check webkitTextFillColor separately.
linear-gradient fade overlay via ::before pseudo-element
A white-to-transparent gradient on a pseudo-element fades consent from view. Standard element checks cannot see pseudo-elements.
A pseudo-element positioned over a consent container can carry a linear-gradient that fades from opaque white to transparent. This creates a visual effect that looks like a legitimate "fade to load more" pattern — common in card components — while actually obscuring the consent text. The consent element itself has no suspicious properties. All checks on the consent element pass. The pseudo-element is invisible to most consent audit code because pseudo-elements are not in the DOM tree and do not appear in el.children or el.querySelectorAll('*').
.install-dialog {
position: relative;
}
.install-dialog::before {
content: '';
position: absolute;
inset: 0;
/* fades the top 45% of the dialog from white to transparent */
background: linear-gradient(to bottom, white 0%, white 45%, transparent 45%);
pointer-events: none;
z-index: 10;
}
The consent paragraph inside .install-dialog passes every check: display:block, visibility:visible, opacity:1, BCR in-viewport, non-zero height. The ::before layer — which covers it — is detected only by reading getComputedStyle(el, '::before') on the parent and checking whether the pseudo's computed size, position, and background-image can produce an overlay over the consent area.
See the linear-gradient consent bypass reference for detailed attack variants including repeating-linear-gradient stripe patterns and JS mousedown injection.
SA-CSS-GRAD-002 (High). ::before/::after pseudo-element with linear-gradient overlay covering consent area. pointer-events:none prevents click interference. Standard DOM traversal misses pseudo-elements. Detection requires getComputedStyle(parent, '::before') and geometric intersection check against consent BCR.
repeating-linear-gradient stripe pattern — each text line blocked individually
White horizontal bands at text-line intervals block each line of consent independently. The pattern resembles a decorative watermark.
A repeating-linear-gradient with a stripe period matching the consent element's line-height can place an opaque white band over every line of consent text simultaneously. At a 20px line-height, a gradient with 14px white bands and 6px transparent gaps tiles perfectly to block all text. From a distance the consent area looks like it has a faint striped watermark — the kind of decorative texture used in receipt or form designs. The consent text is inaccessible even though neither the text's color nor its opacity is set.
.consent-text {
/* white stripes every 20px, blocking 14px of each 20px text line */
background-image: repeating-linear-gradient(
to bottom,
rgba(255,255,255,0.97) 0px,
rgba(255,255,255,0.97) 14px,
transparent 14px,
transparent 20px
);
/* no background-clip:text — the gradient is a background layer covering the text */
background-attachment: local;
}
The detection signal here is the backgroundImage property containing repeating-linear-gradient with color stops near white/opaque, and a period that correlates with the element's line-height. A simple check for any repeating- gradient on a consent element's background catches this pattern without requiring geometric calculation.
SA-CSS-GRAD-003 (High). repeating-linear-gradient on consent element background with opaque stripe period ≈ line-height. Text not removed from DOM; not transparent; not overflow-clipped. Only backgroundImage inspection reveals the attack. Check: backgroundImage.includes('repeating-linear-gradient') and any stop near rgba(255,255,255,0.9) or white.
radial-gradient center obscuration — ellipse covers key consent clause
A radial gradient with an opaque white ellipse at center covers the most critical part of the consent while leaving the borders visible — making the dialog appear populated.
A radial-gradient can be parameterized to produce an opaque ellipse of arbitrary size and position within the element. An MCP server places a white ellipse centered on the consent container, sized to cover 80% of the width and 60% of the height. The consent text at the edges of the dialog remains visible — the "I agree" opening and the boilerplate clause numbers at the bottom are readable. The operative sentence in the center — "grant this server access to read, write, and execute files" — is covered by white paint. The dialog appears to have content. A human reviewer sees consent text. The key clause is invisible.
.install-dialog::before {
content: '';
position: absolute;
inset: 0;
/* white ellipse 80% wide × 60% tall, centered */
background: radial-gradient(
ellipse 80% 60% at center,
white 45%,
transparent 45%
);
pointer-events: none;
z-index: 10;
}
This is a notably deceptive pattern because it is designed to be partially visible. A consent auditor checking "is there text visible?" will see text at the edges and report passing. The attack is selective: it targets only the sentences that matter. Detection requires checking the pseudo-element's gradient shape and correlating it with the consent text's line positions.
See the radial-gradient consent bypass reference for the full attack matrix including the background-clip:text variant and JS mousedown injection.
SA-CSS-GRAD-004 (High). radial-gradient ellipse pseudo-element covering center of consent while leaving edges visible. Partial visibility misleads human reviewers. Detection: getComputedStyle(parent, '::before').backgroundImage for radial-gradient + geometric intersection with consent BCR center.
repeating-radial-gradient concentric rings — rotationally symmetric obstruction
Concentric white rings tile the entire consent area in a pattern that resembles a decorative watermark or "ripple" background effect.
repeating-radial-gradient tiles a radial gradient radially outward from the center. With a 4px white band and a 9px period, every 9px of radial distance gets another white ring. At 300×100px consent dimensions, this produces approximately 16 concentric rings from the center outward. The entire consent text is covered. The pattern is rotationally symmetric — it looks identical at every rotation, making it visually consistent with a decorative "ripple" or "target" background effect sometimes used in marketing UI. No single ring is obviously placed over a specific sentence. The entire surface is uniformly obstructed.
.consent-text {
/* concentric white rings — 4px white, 5px transparent, repeat */
background-image: repeating-radial-gradient(
circle at center,
rgba(255,255,255,0.96) 0px,
rgba(255,255,255,0.96) 4px,
transparent 4px,
transparent 9px
);
}
This variant evades detectors that only scan for repeating-linear-gradient. A robust scanner must check for any gradient function — linear, radial, conic, and their repeating variants — when appearing on consent element background-image or ancestor pseudo-element backgrounds. The specific function family does not change the exploit mechanism.
The radial-gradient security page covers the full detection code for radial and repeating-radial gradient patterns.
SA-CSS-GRAD-005 (High). repeating-radial-gradient on consent background with opaque ring period creating full-surface obstruction. Visually resembles decorative watermark. Detection: backgroundImage includes 'repeating-radial-gradient' and contains any near-opaque color stop.
conic-gradient pie-slice wedge and repeating-conic-gradient sweep pattern
Angular gradient attacks that evade both linear and radial gradient scanners by using a distinct function family.
conic-gradient sweeps colors around a center point, like sectors of a pie. An MCP server can use a conic gradient to produce a large white wedge covering the majority of the consent text. The wedge shape looks like a design artifact — perhaps a corner highlight or an angular shadow — rather than an obvious overlay. repeating-conic-gradient tiles this wedge repeatedly, producing a pinwheel or checkerboard-like pattern that covers the consent uniformly.
.consent-text {
/* white sector from 30deg to 330deg — 300 of 360 degrees opaque white */
background-image: conic-gradient(
from 0deg at center,
transparent 0deg 30deg,
white 30deg 330deg,
transparent 330deg 360deg
);
}
.consent-text {
/* pinwheel: alternating 20deg white and transparent sectors */
background-image: repeating-conic-gradient(
white 0deg 20deg,
transparent 20deg 40deg
);
}
Both forms are distinct string values from linear-gradient and radial-gradient. A scanner checking backgroundImage.includes('linear-gradient') || backgroundImage.includes('radial-gradient') will not detect conic attacks. The correct check is a pattern that matches any gradient function family. See the conic-gradient consent bypass reference for the full attack matrix including the background-clip:text variant.
SA-CSS-GRAD-006 (High). conic-gradient / repeating-conic-gradient on consent background. Evades linear-only and radial-only gradient scanners. Detection: regex check for any gradient function — /(?:repeating-)?(?:linear|radial|conic)-gradient/ — in backgroundImage.
mask-image gradient evasion — attack lives outside backgroundImage
A gradient on mask-image achieves the same visual outcome as opacity:0 but getComputedStyle(el).opacity returns 1. Evades all backgroundImage-based scanners.
CSS mask-image accepts the same gradient functions as background-image. A mask controls alpha compositing: where the mask is opaque (white), the element is fully visible; where the mask is transparent (black), the element is fully hidden. An MCP server sets mask-image: linear-gradient(transparent, transparent) on the consent element. The element is fully masked-out — invisible — while maintaining display:block, visibility:visible, opacity:1, and full textContent. Neither backgroundImage nor color nor any geometry check reveals the attack.
.consent-text {
/* fully transparent mask = consent invisible */
mask-image: linear-gradient(transparent 0%, transparent 100%);
-webkit-mask-image: linear-gradient(transparent 0%, transparent 100%);
/* opacity still returns 1; backgroundImage is empty; color is #333 */
}
Partial mask attacks are also viable: a mask that is opaque at the bottom (showing install button) and transparent at the top (hiding consent) uses a linear-gradient(transparent 0%, white 60%) mask. The button is interactive and visible. The consent is hidden. The install button works normally because it is outside the masked area or has its own layout.
See the mask-image consent bypass reference for detection of mask-image, -webkit-mask-image, and mask-composite attacks. See the background-image consent bypass reference for gradient-on-background detection patterns.
SA-CSS-GRAD-007 (Critical). mask-image with transparent gradient = consent invisible; opacity:1 unchanged; backgroundImage empty. All standard checks pass. Detection: getComputedStyle(el).maskImage and webkitMaskImage — check for gradient functions and any fully-transparent stop covering the consent area.
JS mousedown gradient injection — attack fires at install click
Static CSS analysis at page load sees nothing. The gradient is injected via inline style at the mousedown event on the install button — the frame before the click is processed.
A well-timed gradient attack does not require any suspicious CSS in the initial page state. The MCP server registers a mousedown listener on the install button. At the start of the mousedown event — before the click event fires and before the user perceives anything has changed — the listener injects a gradient-based overlay onto the consent element. The install click is processed, the user believes they consented to what they read at page load, and the injected gradient is removed in a cleanup function. Static analysis of the page's CSS finds nothing. A consent audit run at DOMContentLoaded finds nothing.
installBtn.addEventListener('mousedown', () => {
// inject transparent-text gradient at click moment
consentEl.style.backgroundImage = 'linear-gradient(transparent, transparent)';
consentEl.style.backgroundClip = 'text';
consentEl.style.webkitBackgroundClip = 'text';
consentEl.style.webkitTextFillColor = 'transparent';
// no setTimeout needed: mousedown fires before click is processed
});
Detecting JS-injected gradient attacks requires a MutationObserver on the consent element (and its ancestors' pseudo-element proxies) throughout the interaction window — from page load until after the install click resolves. A MutationObserver watching style attribute changes can catch inline style injection. A periodic requestAnimationFrame re-check can catch class-based injection where a class containing gradient CSS is added to the consent element.
const mo = new MutationObserver(() => {
// re-audit consent element on any DOM change
if (hasGradientAttack(consentEl)) {
flagConsentTampering('gradient-injection');
installBtn.disabled = true;
}
});
mo.observe(consentEl, { attributes: true, attributeFilter: ['style', 'class'] });
mo.observe(consentEl.parentElement, { childList: true, subtree: true });
SA-CSS-GRAD-008 (Critical). JS mousedown injects gradient via inline style; attack fires in click frame. Static CSS analysis at page load is insufficient — requires MutationObserver on consent element and ancestors throughout interaction window. Disable install button if tampering detected.
Detection gaps: what standard consent checkers miss
| Check | Catches gradient attacks? | Reason |
|---|---|---|
getComputedStyle(el).color |
No | -webkit-text-fill-color overrides color for rendering; color value is unchanged and non-transparent |
getComputedStyle(el).opacity |
No | Opacity is 1; mask-image and background-clip:text don't affect opacity |
getBoundingClientRect() |
No | Element still has non-zero in-viewport dimensions; geometry is unaffected |
el.textContent |
No | Text is in the DOM; only its visual rendering is affected |
getComputedStyle(el).visibility |
No | visibility:visible unchanged in all gradient attacks |
backgroundImage check (linear-gradient only) |
Partial | Misses radial, conic, repeating variants, and mask-image attacks |
getComputedStyle(el).webkitTextFillColor |
Yes | Returns transparent/rgba(0,0,0,0) when background-clip:text attack is active |
backgroundImage with full gradient regex |
Yes | /(?:repeating-)?(?:linear|radial|conic)-gradient/ catches all six families |
maskImage / webkitMaskImage check |
Yes | Detects mask-image gradient attacks that evade backgroundImage scanners |
Pseudo-element check via getComputedStyle(parent, '::before') |
Yes | Detects ::before/::after overlay attacks; pseudo-elements not in DOM tree |
| MutationObserver on consent element | Yes | Catches JS mousedown injection that would otherwise evade static analysis |
Unified ConsentGradientAudit detector
The following class combines all gradient attack detections into a single auditor. It checks the consent element directly, walks up the ancestor chain for pseudo-element overlays, checks mask-image, and installs a MutationObserver for runtime injection attacks.
class ConsentGradientAudit {
// regex matching any CSS gradient function
static GRADIENT_RE = /(?:repeating-)?(?:linear|radial|conic)-gradient/i;
static auditElement(el) {
const findings = [];
const cs = getComputedStyle(el);
// 1. Check -webkit-text-fill-color (blind spot for background-clip:text attacks)
const fillColor = cs.webkitTextFillColor;
if (fillColor) {
if (fillColor === 'transparent' || /rgba\([^)]+,\s*0\b/.test(fillColor)) {
findings.push({ id: 'SA-CSS-GRAD-001', severity: 'high',
detail: `webkitTextFillColor:${fillColor} — text transparent via background-clip:text` });
}
}
// 2. Check backgroundImage for any gradient function on the consent element
const bg = cs.backgroundImage;
if (bg && this.GRADIENT_RE.test(bg)) {
findings.push({ id: 'SA-CSS-GRAD-002', severity: 'high',
detail: `backgroundImage contains gradient: ${bg.slice(0, 80)}` });
}
// 3. Check mask-image (evades backgroundImage checks)
const mask = cs.maskImage || cs.webkitMaskImage;
if (mask && mask !== 'none' && this.GRADIENT_RE.test(mask)) {
findings.push({ id: 'SA-CSS-GRAD-007', severity: 'critical',
detail: `maskImage contains gradient: ${mask.slice(0, 80)}` });
}
// 4. Check ancestor pseudo-elements for overlay attacks
let ancestor = el.parentElement;
while (ancestor && ancestor !== document.body) {
for (const pseudo of ['::before', '::after']) {
const pcs = getComputedStyle(ancestor, pseudo);
const pbg = pcs.backgroundImage;
if (pbg && this.GRADIENT_RE.test(pbg)) {
// check if pseudo-element overlaps consent area
const consentRect = el.getBoundingClientRect();
const aRect = ancestor.getBoundingClientRect();
const overlaps = !(consentRect.bottom < aRect.top || consentRect.top > aRect.bottom);
if (overlaps) {
findings.push({ id: 'SA-CSS-GRAD-003', severity: 'high',
detail: `${ancestor.tagName}${pseudo} gradient overlapping consent area` });
}
}
}
ancestor = ancestor.parentElement;
}
return findings;
}
static installRuntimeMonitor(el, installBtn, onTamper) {
const mo = new MutationObserver(() => {
const findings = this.auditElement(el);
if (findings.length > 0) {
onTamper(findings);
installBtn.disabled = true;
}
});
mo.observe(el, { attributes: true, attributeFilter: ['style', 'class'] });
mo.observe(el.parentElement, { childList: true, subtree: true, attributes: true });
return mo;
}
}
Run ConsentGradientAudit.auditElement(consentEl) at page load and call ConsentGradientAudit.installRuntimeMonitor(consentEl, installBtn, handler) to catch JS mousedown injection. Together these two calls cover all eight gradient attack families documented above.
SkillAudit runs this analysis automatically. When you submit an MCP server URL, SkillAudit's consent audit walks the DOM for all six gradient function families across backgroundImage, maskImage, and pseudo-element backgrounds. It simulates the mousedown interaction to catch JS-injected attacks. Results appear in the security report under the "Consent UI" category. Try a free audit →
Summary
CSS gradient functions are a uniform threat family against MCP consent dialogs. The six function types — linear, radial, conic, and their repeating variants — all enable the same two attack paths: transparent text via background-clip:text and opaque overlays via pseudo-element backgrounds. The critical detection blind spot is getComputedStyle(el).color, which always reports the declared color value regardless of -webkit-text-fill-color. A scanner that checks only backgroundImage for linear-gradient names misses radial, conic, repeating variants, and all mask-image attacks. A robust consent auditor must check: webkitTextFillColor, all six gradient function names via regex, maskImage/webkitMaskImage, ancestor pseudo-elements, and runtime injection via MutationObserver.