MCP server CSS background-origin security
background-origin controls which box (border-box, padding-box, content-box) CSS gradient coordinates are relative to. MCP servers exploit this to shift transparent gradients onto consent text using background-clip:text, or to displace opaque gradients onto the consent area by adjusting the coordinate origin. Detectors that check absolute gradient positions fail when background-origin changes the reference frame.
Attack findings
Attack 1 — content-box origin + background-clip:text transparent gradient (SA-CSS-BORG-001)
When background-origin: content-box is combined with background-clip: text and a fully-transparent gradient, the gradient fills the text glyphs with transparency. The background-origin property determines where gradient pixel coordinates start — content-box means the gradient starts at the inner edge of the padding, not the border. This makes the transparent region of the gradient land precisely on the content area where the text is, regardless of padding size.
/* Attack: background-origin shifts gradient to content-box alignment with text */
.consent-text {
color: #1a1a1a; /* getComputedStyle.color reports normal */
padding: 20px;
background-origin: content-box; /* gradient coordinates relative to content box */
background-image: linear-gradient(transparent 0%, transparent 100%);
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
/* Detection gap: getComputedStyle(el).color = "rgb(26,26,26)" — non-transparent */
/* Detection fix: getComputedStyle(el).webkitTextFillColor = "rgba(0,0,0,0)" */
Key evasion: Adding background-origin does not change the detection fix — webkitTextFillColor still returns transparent — but it makes the CSS look like a legitimate content-box alignment pattern (e.g., ensuring a background image starts at the text edge). A reviewer auditing the CSS sees a plausible layout property alongside what looks like a gradient text effect, rather than an obvious attack.
/* Detection */
function checkBackgroundOriginTextAttack(el) {
const cs = getComputedStyle(el);
const fill = cs.webkitTextFillColor;
const origin = cs.backgroundOrigin;
const clip = cs.backgroundClip || cs.webkitBackgroundClip;
const bg = cs.backgroundImage;
// primary signal: text fill is transparent
if (fill && (fill === 'transparent' || /rgba\([^)]+,\s*0\b/.test(fill))) {
return { vuln: 'SA-CSS-BORG-001', detail: `webkitTextFillColor:${fill}, backgroundOrigin:${origin}` };
}
// secondary signal: background-clip:text with any gradient
if (clip === 'text' && bg && bg !== 'none' && /gradient/.test(bg)) {
return { vuln: 'SA-CSS-BORG-001', detail: `background-clip:text with gradient backgroundImage, origin:${origin}` };
}
return null;
}
Attack 2 — padding-box origin + opaque gradient displacement (SA-CSS-BORG-002)
background-origin: padding-box (the default) starts gradient coordinates at the inner edge of the border, inside the padding area. When an element has large padding and background-position: 0 0, the gradient starts at the start of the padding. An MCP server creates a consent container with 80px of top padding and a gradient that starts with 80px of white (the padding) then fades to transparent. The gradient positions opaque white paint exactly over the consent text, which sits in the content area after the padding. The technique looks like a subtle background coloring issue — a gradient that starts "too high up" — but the result is that the consent text is painted over.
/* Attack: large padding + gradient starting at padding-box edge covers text */
.consent-container {
padding-top: 80px;
background-origin: padding-box; /* default — gradient starts at padding edge */
background-image: linear-gradient(
to bottom,
white 0px, /* covers padding (0 to 80px from padding edge) */
white 80px, /* still white at 80px — where text starts */
white 160px, /* covers full consent text area */
transparent 161px
);
}
Detection: Check backgroundOrigin value alongside element paddingTop/paddingLeft and gradient stop positions. If the gradient has an opaque region whose pixel extent correlates with the element's padding — suggesting the opaque region covers exactly the content area — flag as suspicious. The more reliable check: compute whether the gradient's opaque region overlaps the consent text's layout position within the element.
Attack 3 — border-box origin + thick border gradient shift (SA-CSS-BORG-003)
background-origin: border-box starts gradient coordinates at the outer edge of the border. When the element has a thick border (e.g., 40px), the gradient's 0% position starts 40px outside the padding edge. An MCP server uses this to shift an opaque gradient region from what looks like the "decorative border area" into the actual content area. The gradient code appears to target the border — a seemingly legitimate pattern — but the thick border pushes the opaque paint region into where the consent text is rendered.
/* Attack: border-box origin + thick border shifts opaque region to text area */
.consent-text {
border: 40px solid rgba(240,240,240,0.3); /* thick, semi-transparent border */
background-origin: border-box;
background-image: linear-gradient(
to bottom,
transparent 0px,
/* 40px offset from border edge = start of padding = start of text */
white 40px,
white 200px,
transparent 201px
);
}
/* Background appears to target border region; actually covers text content area */
Attack 4 — JS mousedown background-origin switch (SA-CSS-BORG-004)
At page load, the consent element has no background-origin and no gradient. At the mousedown event on the install button, JS sets both inline styles simultaneously — changing background-origin to content-box and adding a transparent gradient with background-clip:text and -webkit-text-fill-color:transparent. The combination makes consent text invisible in the click frame. Static CSS audit at page load finds nothing suspicious.
/* Attack: runtime injection of background-origin + gradient combo */
installBtn.addEventListener('mousedown', () => {
consentEl.style.backgroundOrigin = 'content-box';
consentEl.style.backgroundImage = 'linear-gradient(transparent,transparent)';
consentEl.style.backgroundClip = 'text';
consentEl.style.webkitBackgroundClip = 'text';
consentEl.style.webkitTextFillColor = 'transparent';
});
/* Detection: MutationObserver watching style attribute changes */
const mo = new MutationObserver(() => {
const cs = getComputedStyle(consentEl);
const fill = cs.webkitTextFillColor;
if (fill === 'transparent' || /rgba\([^)]+,\s*0\b/.test(fill || '')) {
flagTampering('SA-CSS-BORG-004');
installBtn.disabled = true;
}
});
mo.observe(consentEl, { attributes: true, attributeFilter: ['style'] });
SkillAudit detection: SkillAudit's consent audit checks webkitTextFillColor, backgroundOrigin, backgroundClip, and backgroundImage as a compound signal. It also simulates mousedown to catch runtime injection. Run a free audit →
Detection summary
| Attack ID | CSS properties involved | Key detection signal |
|---|---|---|
| SA-CSS-BORG-001 | background-origin:content-box + background-clip:text + transparent gradient + -webkit-text-fill-color:transparent | webkitTextFillColor = transparent |
| SA-CSS-BORG-002 | background-origin:padding-box + large padding + opaque gradient covering text area | gradient opaque region overlaps content area after padding offset |
| SA-CSS-BORG-003 | background-origin:border-box + thick border + gradient shifted into content area | gradient stop position < border-width places opaque region in content |
| SA-CSS-BORG-004 | JS mousedown inline style injection of background-origin + transparent gradient combo | MutationObserver style change → webkitTextFillColor check |