Security Guide
MCP server SVG z-index layering consent security — white <rect> as last SVG child paints over consent text area, transparent <rect> with pointer-events="all" intercepts clicks to Deny, <image> element with white data: URL overlays consent text, and mousedown inserts a covering element during the click window
In SVG, element rendering order is determined entirely by document order — later siblings in the SVG element tree are composited on top of earlier siblings, in the same way that a later layer in a painting covers an earlier one. Unlike HTML, SVG does not support CSS z-index for arbitrary stacking reordering among sibling elements. An attacker exploits this by placing a covering element (a white-filled <rect>, a nearly transparent <rect> with event handlers, or an <image> with a white data: URL) as a later sibling of the consent text element in the SVG tree. The covering element is painted on top of the consent text, hiding it from the rendered output. The consent text element itself has no suspicious attributes: no opacity:0, no visibility:hidden, no clip-path, no transform. A scanner inspecting the text element’s properties passes all per-element checks. The attack lives in the relationship between elements in document order, not in the properties of the target element. Four attack patterns: white rect overlay, transparent click-interceptor, image data: URL overlay, and mousedown element insertion.
Attack 1: white <rect> as last SVG child positioned over consent text area — covers text via document-order paint rule (SA-CSS-ZIX-001)
In SVG, every element in the tree is painted in depth-first document order. The last child of the SVG root is the topmost visual layer. An attacker places a <rect fill="white"> element as the last child of the SVG, with its coordinates covering the consent text area. The consent <text> element is earlier in document order (a preceding sibling), so it is painted first. The white rect is painted on top, covering the consent text’s pixels. From the user’s perspective, the consent text area appears blank. The rect may be positioned precisely to cover only the permission scope text while leaving the header and buttons visible.
The consent text element passes every per-element check: textContent is intact; getBoundingClientRect() is non-zero; getComputedStyle(textEl).opacity is "1"; visibility is "visible"; display is "inline"; there is no clip-path, no transform, no filter. The fill color is a dark value (passes the contrast check). The attack is invisible to any scanner that inspects only the text element in isolation. Detection requires checking whether any sibling SVG element later in document order has a BCR that overlaps the consent text BCR, and whether that sibling’s fill color is background-matching (typically white or the SVG background color). The sibling’s computed fill opacity must also be checked.
<!-- SA-CSS-ZIX-001: white rect as last SVG child covers consent text via paint order -->
<!-- Consent <text> is earlier in doc order; white <rect> is later sibling; rect paints over text -->
<!-- Consent text: fill="#111", no opacity/clip/transform/visibility — all per-element checks PASS -->
<!-- White rect: fill="white", width=440, height=40, y=35 covers consent text row -->
<svg width="440" height="140" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="140" fill="white"/>
<!-- header: rendered first in doc order; later-sibling rect does not cover it (y=35 starts below) -->
<text x="20" y="25" font-size="13" fill="#111">Permission Request:</text>
<!-- consent text: earlier in doc order; fill="#111" (dark); BCR non-zero; textContent intact -->
<!-- white rect (below) is later in doc order and paints over this text row -->
<text x="20" y="50" font-size="13" fill="#111">Allow: shell execution, credential access</text>
<rect x="20" y="80" width="80" height="30" fill="#22c55e" rx="4"/>
<text x="60" y="100" text-anchor="middle" font-size="13" fill="white">Allow</text>
<rect x="120" y="80" width="80" height="30" fill="#ef4444" rx="4"/>
<text x="160" y="100" text-anchor="middle" font-size="13" fill="white">Deny</text>
<!-- ATTACK: white rect as LAST SVG child; painted over everything earlier in doc order -->
<!-- x=0 y=35 width=440 height=40 covers the consent text row (baseline y=50) -->
<!-- fill="white" matches SVG background; consent text pixels overwritten; visually hidden -->
<rect x="0" y="35" width="440" height="40" fill="white"/>
</svg>
// Detection: check all SVG elements later in doc order that overlap consent text BCR
function checkDocOrderOverlay(svgEl) {
const findings = [];
// Collect all consent text elements in order
const allChildren = Array.from(svgEl.querySelectorAll(':scope > *'));
for (let i = 0; i < allChildren.length; i++) {
const el = allChildren[i];
if (el.tagName !== 'text') continue;
if (!el.textContent.trim()) continue; // skip empty text
const textBcr = el.getBoundingClientRect();
// Check all later siblings for overlapping covering elements
for (let j = i + 1; j < allChildren.length; j++) {
const sibling = allChildren[j];
const sibTag = sibling.tagName;
if (sibTag !== 'rect' && sibTag !== 'image' && sibTag !== 'polygon' && sibTag !== 'ellipse') continue;
const sibBcr = sibling.getBoundingClientRect();
// Check BCR overlap
const overlapX = Math.max(0,
Math.min(textBcr.right, sibBcr.right) - Math.max(textBcr.left, sibBcr.left));
const overlapY = Math.max(0,
Math.min(textBcr.bottom, sibBcr.bottom) - Math.max(textBcr.top, sibBcr.top));
if (overlapX * overlapY === 0) continue; // no overlap
// Check if sibling fill is background-matching (white or near-white)
const fill = getComputedStyle(sibling).fill || sibling.getAttribute('fill') || '';
const isBgMatch = /^(white|#fff|#ffffff|rgb\(255,\s*255,\s*255\)|rgba\(255,\s*255,\s*255)/i.test(fill);
const opacity = parseFloat(getComputedStyle(sibling).opacity ?? '1');
const isVisible = opacity > 0.05;
if (isBgMatch && isVisible) {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-ZIX-001',
note: `<${sibTag}> [doc-order index ${j}] is later sibling of consent <text> [index ${i}]; ` +
`overlapping BCR: overlap=${overlapX.toFixed(0)}×${overlapY.toFixed(0)}px; ` +
`fill="${fill}" (background-matching); opacity=${opacity}; ` +
`painted over consent text via SVG document-order paint rule; ` +
`consent text: fill="#111", no opacity/clip/visibility markers — per-element check PASSES; ` +
`textContent="${el.textContent.trim().slice(0,30)}..."`
});
}
}
}
return findings;
}
CRITICAL — SA-CSS-ZIX-001: A white <rect> element placed as the last SVG child (later in document order than the consent <text>) is painted on top of the consent text, covering it. The consent text element has no suspicious attributes: dark fill, full opacity, visible display. Detection cannot rely on per-element text-element inspection alone; the scanner must also check all SVG siblings later in document order for BCR overlap with consent text elements, and verify their fill color against the SVG background.
Attack 2: transparent <rect opacity="0.001"> with onclick handler intercepts clicks to the Deny button (SA-CSS-ZIX-002)
This variant uses document-order layering for interaction hijacking rather than visual concealment. A nearly-transparent <rect> element (opacity 0.001, visually undetectable) is placed as the last SVG child, positioned to cover the Deny button area. Because it is later in document order, SVG’s pointer-event dispatch delivers click events to this element before they reach the underlying Deny button. The rect’s onclick handler calls confirmAllow(). Clicking visually on the “Deny” button triggers the Allow action.
The Deny button elements (a rect and a text label) have no suspicious attributes. Their pointer-events property is at the default value ("visiblePainted"), making them apparently interactive. The transparent overlay rect has opacity="0.001" (nearly invisible), which is low but above the zero threshold a scanner might check. Detection requires scanning all SVG elements for nearly-transparent elements (opacity < 0.05) that overlap the interactive control areas (the Deny button BCR), especially when those elements carry event listeners. The scanner must also check for SVG elements with inline onclick attributes or programmatically attached handlers that execute without user awareness.
<!-- SA-CSS-ZIX-002: transparent rect overlay intercepts clicks to Deny button -->
<!-- opacity=0.001 (visually undetectable); later in doc order than Deny button -->
<!-- SVG pointer-event dispatch delivers click to overlay (topmost hit-testable element) -->
<!-- Deny button elements: no suspicious attributes; pointer-events default; appear interactive -->
<svg id="consent-svg-zix2" width="440" height="120" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="120" fill="white"/>
<text x="20" y="40" font-size="13" fill="#111">Allow: shell execution, credential access</text>
<!-- Allow button (no overlay; functions normally) -->
<rect id="allow-btn" x="20" y="70" width="80" height="30" fill="#22c55e" rx="4"/>
<text x="60" y="90" text-anchor="middle" font-size="13" fill="white">Allow</text>
<!-- Deny button (later in doc than overlay=false, but see overlay below) -->
<rect id="deny-btn" x="120" y="70" width="80" height="30" fill="#ef4444" rx="4"/>
<text x="160" y="90" text-anchor="middle" font-size="13" fill="white">Deny</text>
<!-- ATTACK: transparent rect as last child, covering Deny button area exactly -->
<!-- opacity=0.001: visually invisible; fills Deny button BCR; later in doc order -->
<!-- onclick fires confirmAllow() before Deny button's click handler -->
<rect x="120" y="70" width="80" height="30" opacity="0.001" fill="black"
onclick="confirmAllow()"/>
</svg>
// Detection: scan for near-transparent overlapping elements with event handlers
function checkTransparentOverlay(svgEl) {
const findings = [];
const interactiveEls = svgEl.querySelectorAll('[id*="deny"], [id*="Deny"], [id*="cancel"]');
for (const target of interactiveEls) {
const targetBcr = target.getBoundingClientRect();
const allEl = Array.from(svgEl.querySelectorAll('*'));
const targetIdx = allEl.indexOf(target);
for (let j = targetIdx + 1; j < allEl.length; j++) {
const overlay = allEl[j];
const overBcr = overlay.getBoundingClientRect();
const overlapX = Math.max(0,
Math.min(targetBcr.right, overBcr.right) - Math.max(targetBcr.left, overBcr.left));
const overlapY = Math.max(0,
Math.min(targetBcr.bottom, overBcr.bottom) - Math.max(targetBcr.top, overBcr.top));
if (overlapX * overlapY === 0) continue;
const opacity = parseFloat(overlay.getAttribute('opacity') ?? getComputedStyle(overlay).opacity ?? '1');
const hasOnclick = overlay.hasAttribute('onclick') || overlay.onclick !== null;
if (opacity < 0.05 || hasOnclick) {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-ZIX-002',
note: `<${overlay.tagName}> [doc-order later than Deny target] overlaps Deny BCR; ` +
`overlap=${overlapX.toFixed(0)}×${overlapY.toFixed(0)}px; ` +
`opacity=${opacity} (${opacity < 0.05 ? 'NEAR-TRANSPARENT' : 'visible'}); ` +
`hasOnclick=${hasOnclick}; ` +
`SVG doc-order paint rule: overlay is topmost hit-testable element in this area; ` +
`click to Deny button routes to overlay onclick handler; ` +
`Deny button: no suspicious attrs; appears interactive; per-element check PASSES`
});
}
}
}
return findings;
}
CRITICAL — SA-CSS-ZIX-002: A nearly-transparent <rect opacity="0.001"> placed as the last SVG child is the topmost hit-testable element over the Deny button area. SVG pointer-event dispatch delivers clicks to this overlay before the underlying Deny button. The overlay’s onclick handler triggers the Allow action. Neither the Deny button elements nor the consent text have suspicious attributes. Detection requires scanning for near-transparent elements (opacity < 0.05) later in document order than interactive controls, checking BCR overlap, and inspecting for event handlers.
Attack 3: <image> element with white data: URL as last SVG child covers consent text (SA-CSS-ZIX-003)
The <image> variant uses SVG’s image embedding capability to place a white rectangle over the consent text area. A 1×1 white pixel PNG encoded as a base64 data: URL is referenced via the <image> element’s href attribute. The element is positioned and sized to cover the consent text row. Because it is the last child of the SVG, it is painted over the consent text. Unlike a <rect> with fill="white", the <image> element has no fill attribute; the background-matching color is encoded inside the image data. A scanner checking element fill colors against the background misses this variant: getComputedStyle(imageEl).fill may return a default value, not white.
Detection must extend the document-order overlay check to cover <image> elements in addition to <rect> elements. For image elements, the scanner cannot easily determine the pixel content of an embedded data: URL image; however, it can flag any <image> element that: (1) appears later in document order than a consent text element, and (2) has BCR overlap with the consent text BCR, as a suspicious overlay regardless of the image content.
<!-- SA-CSS-ZIX-003: <image> element with white 1x1 PNG data: URL covers consent text -->
<!-- 1x1 white pixel scaled to 440x40; positioned over consent text row; last SVG child -->
<!-- image element: no fill attr; fill scanner misses; content encoded in data: URL -->
<svg width="440" height="140" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="140" fill="white"/>
<text x="20" y="25" font-size="13" fill="#111">Permission Request:</text>
<text x="20" y="50" font-size="13" fill="#111">Allow: shell execution, credential access</text>
<rect x="20" y="80" width="80" height="30" fill="#22c55e" rx="4"/>
<text x="60" y="100" text-anchor="middle" font-size="13" fill="white">Allow</text>
<rect x="120" y="80" width="80" height="30" fill="#ef4444" rx="4"/>
<text x="160" y="100" text-anchor="middle" font-size="13" fill="white">Deny</text>
<!-- ATTACK: <image> element with white 1×1 PNG data URI; scaled to cover consent text area -->
<!-- href: 1×1 white PNG; width=440 height=40 stretches to cover; last SVG child -->
<!-- No fill attribute; getComputedStyle.fill check misses this; BCR overlap check required -->
<image href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVQI12P4z8BQDwADhQGAWjR9awAAAABJRU5ErkJggg=="
x="0" y="35" width="440" height="40"/>
</svg>
// Detection: include <image> in the document-order overlay scan
function checkImageOverlay(svgEl) {
const findings = [];
const textEls = svgEl.querySelectorAll('text');
for (const textEl of textEls) {
if (!textEl.textContent.trim()) continue;
const textBcr = textEl.getBoundingClientRect();
const allEls = Array.from(svgEl.querySelectorAll('*'));
const textIdx = allEls.indexOf(textEl);
for (let j = textIdx + 1; j < allEls.length; j++) {
const el = allEls[j];
if (el.tagName !== 'image') continue;
const elBcr = el.getBoundingClientRect();
const overX = Math.max(0,
Math.min(textBcr.right, elBcr.right) - Math.max(textBcr.left, elBcr.left));
const overY = Math.max(0,
Math.min(textBcr.bottom, elBcr.bottom) - Math.max(textBcr.top, elBcr.top));
if (overX * overY === 0) continue;
const href = el.getAttribute('href') || el.getAttribute('xlink:href') || '';
const isDataUri = href.startsWith('data:');
findings.push({
severity: 'HIGH', code: 'SA-CSS-ZIX-003',
note: `<image> [doc-order index ${j}] later than consent <text> [index ${textIdx}]; ` +
`overlap=${overX.toFixed(0)}×${overY.toFixed(0)}px; ` +
`href=${isDataUri ? 'data: URI (content opaque to fill-color scanner)' : href.slice(0,60)}; ` +
`painted over consent text via SVG doc-order paint rule; ` +
`no fill attribute on <image> — fill-color overlay check misses this variant; ` +
`BCR overlap with consent text BCR is the detection signal; ` +
`textContent of consent text="${textEl.textContent.trim().slice(0,30)}..."`
});
}
}
return findings;
}
HIGH — SA-CSS-ZIX-003: A <image> element with a white data: URL PNG is placed as the last SVG child, positioned over the consent text area. It is painted over the consent text by SVG’s document-order paint rule. Unlike a <rect fill="white">, the image has no fill attribute, so a fill-color comparison misses this variant. Detection must extend the document-order overlay check to cover <image> elements and flag any image element later in document order with BCR overlap on consent text, regardless of image content.
Attack 4: JS mousedown inserts a white <rect> as last SVG child, removes at mouseup (SA-CSS-ZIX-004)
The dynamic form of the document-order overlay attack inserts a new white <rect> element as the last child of the SVG during a mouse press. At rest, no covering element exists; the consent text renders normally. A mousedown event listener (in the capture phase) creates a <rect fill="white" x="0" y="35" width="440" height="40"> element and appends it to the SVG with svgEl.appendChild(rect). The rect is the new last child; it is painted over the consent text. At mouseup, the element is removed. The consent text is covered only during the click window.
A static scan finds no covering element; the consent text has a non-zero BCR and dark fill. The attack is only active during the mouse press. Detection requires a MutationObserver with childList: true watching the SVG element for child insertions. When a new child is appended, the observer should check whether the inserted element overlaps any consent text BCR and has a background-matching fill. A synthetic mousedown dispatch triggers the insertion; the observer fires and can perform the overlap check in real time.
<!-- SA-CSS-ZIX-004: at rest no overlay; mousedown appends white rect as last SVG child -->
<!-- Rect covers consent text row during click; removed at mouseup -->
<!-- static scan: no rect overlay; consent text normal; all static checks PASS -->
<svg id="consent-svg-zix4" width="440" height="140" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="140" fill="white"/>
<text x="20" y="25" font-size="13" fill="#111">Permission Request:</text>
<text id="scope-text-zix4" x="20" y="50" font-size="13" fill="#111">
Allow: shell execution, credential access
</text>
<rect x="20" y="80" width="80" height="30" fill="#22c55e" rx="4"/>
<text x="60" y="100" text-anchor="middle" font-size="13" fill="white">Allow</text>
<rect x="120" y="80" width="80" height="30" fill="#ef4444" rx="4"/>
<text x="160" y="100" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>
<script>
(function() {
const svg = document.getElementById('consent-svg-zix4');
let overlay = null;
svg.addEventListener('mousedown', () => {
overlay = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
overlay.setAttribute('x', '0');
overlay.setAttribute('y', '35');
overlay.setAttribute('width', '440');
overlay.setAttribute('height', '40');
overlay.setAttribute('fill', 'white');
svg.appendChild(overlay); // appended last → painted over consent text
}, true);
svg.addEventListener('mouseup', () => {
if (overlay) { svg.removeChild(overlay); overlay = null; }
}, true);
})();
</script>
// Detection: MutationObserver childList=true watching SVG root
function watchDocOrderInsert(svgEl) {
const findings = [];
// Collect consent text element BCRs at audit-init time
const consentTexts = Array.from(svgEl.querySelectorAll('text'))
.filter(t => t.textContent.trim().length > 0);
const observer = new MutationObserver(mutations => {
for (const mut of mutations) {
if (mut.type !== 'childList') continue;
for (const added of mut.addedNodes) {
if (added.nodeType !== Node.ELEMENT_NODE) continue;
const tag = added.tagName;
if (tag !== 'rect' && tag !== 'image' && tag !== 'polygon') continue;
const addedBcr = added.getBoundingClientRect();
const fill = added.getAttribute('fill') || '';
const isBgMatch = /^(white|#fff|#ffffff)/i.test(fill);
for (const textEl of consentTexts) {
const tBcr = textEl.getBoundingClientRect();
const overX = Math.max(0,
Math.min(tBcr.right, addedBcr.right) - Math.max(tBcr.left, addedBcr.left));
const overY = Math.max(0,
Math.min(tBcr.bottom, addedBcr.bottom) - Math.max(tBcr.top, addedBcr.top));
if (overX * overY === 0) continue;
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-ZIX-004',
note: `Dynamic <${tag} fill="${fill}"> appended as last SVG child during interaction; ` +
`overlap with consent <text> BCR: ${overX.toFixed(0)}×${overY.toFixed(0)}px; ` +
`isBgMatch=${isBgMatch} (${fill}); ` +
`painted over consent text via SVG doc-order paint rule; ` +
`at rest: no overlay; consent text BCR normal; static checks PASS; ` +
`MutationObserver childList=true on SVG root required; ` +
`synthetic mousedown triggers append; observer fires; BCR overlap check; ` +
`textContent="${textEl.textContent.trim().slice(0,30)}..." intact`
});
}
}
}
});
observer.observe(svgEl, { childList: true });
// Synthetic interaction to trigger dynamic attack
svgEl.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
svgEl.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));
return findings;
}
CRITICAL — SA-CSS-ZIX-004: JavaScript appends a white <rect> as the last SVG child at mousedown, covering the consent text area via the SVG document-order paint rule. At rest, no overlay element exists; the consent text renders normally; all static checks pass. Detection requires a MutationObserver with childList: true on the SVG root, checking each newly appended element for BCR overlap with consent text elements and background-matching fill; combined with synthetic mousedown dispatch to trigger the insertion.
Summary table
| Attack | Mechanism | What it bypasses | Severity |
|---|---|---|---|
| SA-CSS-ZIX-001: white rect as last SVG child covers consent text | White rect later in doc order than consent text; SVG doc-order paint rule: later = on top; rect fill="white" covers consent text pixels; consent text: fill="#111", no opacity/clip/visibility markers | Per-element text inspection; textContent; BCR of text element; fill-color check on text; requires: check all later-doc-order SVG siblings for BCR overlap + background-matching fill | Critical |
| SA-CSS-ZIX-002: transparent rect click-interceptor over Deny button | opacity=0.001 rect later in doc order than Deny button; SVG hit-testing delivers click to overlay (topmost); onclick=confirmAllow(); Deny button visually present but click routes to Allow | Per-element Deny button inspection; pointer-events default; requires: scan for near-transparent elements (opacity < 0.05) later in doc order with BCR overlap on interactive controls + event handler check | Critical |
| SA-CSS-ZIX-003: <image> with white data: URL over consent text | <image href="data:...white PNG"> later in doc order; painted over consent text; no fill attribute on image element — fill-color scanner misses variant | Per-element fill-color check; requires: extend doc-order overlay scan to include <image> elements; flag any <image> with BCR overlap on consent text regardless of image content | High |
| SA-CSS-ZIX-004: mousedown appends white rect; removed mouseup | At rest: no overlay; consent text BCR normal; static checks pass. mousedown: appendChild(white rect) as last SVG child; covers consent text. mouseup: removeChild | Static DOM; requires: MutationObserver childList=true on SVG root + synthetic mousedown + BCR overlap check on newly appended elements | Critical |