Security Guide
MCP server SVG <use> element consent security — shadow DOM isolation defeating CSS consent audits, CSS custom property color tunneling, cross-document href, and absolute-positioned overlay
The SVG <use> element instantiates the content of a referenced <symbol> (or any SVG element) by creating a shadow DOM clone at the <use> element’s position. This shadow DOM has a specific CSS isolation property: most CSS rules applied to the <use> host element do not cascade into the shadow DOM content. Only CSS custom properties (variables) and a small set of inheritable SVG presentation attributes cross the shadow DOM boundary. Four consent-bypass attacks exploit this: (1) a consent-covering <rect> inside a symbol is invisible to CSS audits that query the shadow DOM; (2) CSS custom properties tunnel the background color into the shadow DOM to produce a precisely background-matching cover; (3) a cross-document href loads attacker-controlled SVG content; (4) the <use> element is positioned absolutely over the consent panel.
Attack 1: <use> shadow DOM isolates consent-covering content from CSS audit (SA-CSS-SUE-001)
A <symbol> element contains a full-size white <rect> (width=“100%” height=“100%” fill=“white”). A <use href="#cover-symbol"> element is inserted as a sibling of the consent panel in the DOM, positioned absolutely to overlap it. The <rect> inside the symbol is instantiated inside the shadow DOM of the <use> element. A CSS consent audit that iterates document.querySelectorAll('rect[fill="white"]') finds zero results because the shadow DOM content is not accessible via document.querySelectorAll. Only shadow-piercing selectors or iterating the shadow root finds the cover rect.
The shadow DOM boundary specifically blocks document.querySelectorAll and document.getElementById from finding elements inside it. The <use> element itself is accessible to those APIs, but its shadow content is not. Security audits that rely on document-scope CSS selectors to find suspicious SVG primitives (rects, feFlood, etc.) produce false negatives for any content placed inside a <symbol> and instantiated via <use>.
/* SA-CSS-SUE-001: SVG use shadow DOM isolates cover rect from document.querySelectorAll
document.querySelectorAll('rect[fill="white"]') finds zero results
Cover rect is inside the use element's shadow DOM, invisible to document-scope queries */
<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" style="position:absolute">
<defs>
<!-- The cover is defined in a symbol — not directly in the document's element tree -->
<symbol id="white-cover" viewBox="0 0 1 1" preserveAspectRatio="none">
<!-- This rect is inside the symbol definition
It will NOT be found by document.querySelectorAll('rect') -->
<rect x="0" y="0" width="1" height="1" fill="white"/>
</symbol>
</defs>
</svg>
<!-- The use element is positioned absolutely to overlap the consent panel -->
<svg style="position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; z-index:9999">
<use href="#white-cover" x="0" y="0" width="100%" height="100%"/>
<!-- pointer-events:none on parent SVG: click events still reach consent button below -->
<!-- The cover rect renders as white over the entire consent panel -->
<!-- document.querySelectorAll('rect[fill="white"]') → [] (empty — shadow DOM) -->
</svg>
// --- Detection: must inspect shadow DOM content ---
function detectUseShadowCover(documentRoot = document) {
const findings = [];
for (const useEl of documentRoot.querySelectorAll('use')) {
// Try to access the shadow root (SVG use creates a special "shadow-like" clone)
// In modern browsers: useEl.shadowRoot is null for SVG use (it uses a "shadow tree" not an open shadowRoot)
// Instead: inspect the referenced symbol directly
const href = useEl.getAttribute('href') || useEl.getAttribute('xlink:href') || '';
if (!href.startsWith('#')) continue;
const referencedEl = documentRoot.querySelector(href);
if (!referencedEl) continue;
// Inspect the referenced element's content for cover rects:
for (const rect of referencedEl.querySelectorAll('rect')) {
const fill = rect.getAttribute('fill') || '';
const opacity = parseFloat(rect.getAttribute('opacity') || rect.getAttribute('fill-opacity') || '1');
if (fill === 'white' || fill === '#fff' || fill === '#ffffff') {
findings.push({
severity: 'HIGH',
note: `SVG <use href="${href}"> shadow DOM contains white <rect> (fill="${fill}", opacity=${opacity}) — invisible to document.querySelectorAll; use element should be checked for overlap with consent panel`
});
}
// Check if fill is a computed background-matching color using currentColor inheritance:
if (fill === 'currentColor' || fill.startsWith('var(')) {
findings.push({
severity: 'HIGH',
note: `SVG <use href="${href}"> shadow DOM <rect> uses fill="${fill}" — may inherit background-matching color through CSS custom property; CSS property tunnel attack; check actual computed color`
});
}
}
// Check if the use element overlaps any consent panel element:
const useRect = useEl.getBoundingClientRect();
const consentEls = documentRoot.querySelectorAll('[data-consent], #consent, .consent-panel, [aria-label*="permission"], [aria-label*="allow"]');
for (const consent of consentEls) {
const cr = consent.getBoundingClientRect();
const overlaps = !(useRect.right < cr.left || useRect.left > cr.right || useRect.bottom < cr.top || useRect.top > cr.bottom);
if (overlaps) {
findings.push({ severity: 'CRITICAL', note: `SVG <use> element overlaps consent panel BCR — cover content is positioned over consent` });
}
}
}
return findings;
}
CRITICAL — SA-CSS-SUE-001: A white cover <rect> inside a <symbol> instantiated via <use> is rendered over the consent panel but is invisible to document.querySelectorAll. CSS consent audits that scan for suspicious elements using document-scope queries produce false negatives. Detection must: (1) iterate all <use> elements; (2) resolve the referenced <symbol> or element; (3) inspect the referenced element’s subtree for suspicious <rect>, <image>, or filter primitive elements; (4) check the <use> element’s getBoundingClientRect() against all consent panel BCRs for overlap.
Attack 2: CSS custom property tunneling — background color piped through shadow DOM boundary (SA-CSS-SUE-002)
CSS custom properties (variables) are the primary mechanism that can cross the SVG <use> shadow DOM boundary. Other CSS properties like color, font-size, and opacity may also inherit through, but arbitrary properties do not. The attacker defines a <rect fill="var(--cover-color, transparent)"> inside the symbol. On the <use> element (or any ancestor), they inject a CSS rule --cover-color: white. The custom property inherits into the shadow DOM, resolves the var() in the fill attribute, and the rect renders as white. Static analysis of the <rect fill> attribute sees var(--cover-color, transparent) — which defaults to transparent and appears safe. Only the dynamic resolution of the custom property produces the attack value.
/* SA-CSS-SUE-002: CSS custom property tunneling
fill="var(--cover-color, transparent)" in symbol rect
--cover-color: white injected on use element or ancestor
Static rect fill attribute appears safe (transparent fallback); dynamic resolution produces white */
<svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" style="position:absolute">
<defs>
<symbol id="custom-prop-cover" viewBox="0 0 1 1" preserveAspectRatio="none">
<!-- fill attribute uses CSS custom property with transparent fallback
Static analysis: fill="var(--cover-color, transparent)" appears safe
Dynamic resolution: --cover-color=white → fill=white → consent covered -->
<rect x="0" y="0" width="1" height="1" fill="var(--cover-color, transparent)"/>
</symbol>
</defs>
</svg>
<!-- The custom property is injected on an ancestor element -->
<div style="--cover-color: white">
<svg style="position:absolute; top:0; left:0; width:100%; height:100%; z-index:9999">
<use href="#custom-prop-cover" x="0" y="0" width="100%" height="100%"/>
</svg>
<div id="consent-panel">
<!-- Consent text covered by use element above -->
</div>
</div>
// Detection: check computed fill value, not just attribute value
function detectUseCustomPropertyTunnel(documentRoot = document) {
const findings = [];
for (const useEl of documentRoot.querySelectorAll('use')) {
const href = useEl.getAttribute('href') || useEl.getAttribute('xlink:href') || '';
if (!href.startsWith('#')) continue;
const referencedEl = documentRoot.querySelector(href);
if (!referencedEl) continue;
for (const rect of referencedEl.querySelectorAll('rect')) {
const fillAttr = rect.getAttribute('fill') || '';
if (fillAttr.includes('var(')) {
// Attribute uses CSS variable — get computed value (requires the element to be in DOM)
// The shadow DOM clone is accessible via the use element's shadow tree in modern browsers
// Approximate: check the custom property value on ancestors of the use element
let el = useEl;
while (el) {
const style = getComputedStyle(el);
// Check all custom properties that look like color values:
// (CSS custom properties are not enumerable via getComputedStyle in all browsers,
// but we can check specific known property names)
const varMatch = fillAttr.match(/var\((--[\w-]+)/);
if (varMatch) {
const propName = varMatch[1];
const propVal = style.getPropertyValue(propName).trim();
if (propVal) {
// Check if resolved value is a near-white or background-matching color
findings.push({
severity: 'HIGH',
note: `SVG <use href="${href}"> rect fill="var(${propName})" resolves to "${propVal}" via ancestor CSS custom property — check if this color matches the consent panel background`
});
break;
}
}
el = el.parentElement;
}
if (findings.length === 0) {
findings.push({
severity: 'MEDIUM',
note: `SVG <use href="${href}"> shadow DOM rect fill uses CSS custom property "${fillAttr}" — custom properties can cross the shadow DOM boundary; dynamic resolution required`
});
}
}
}
}
return findings;
}
HIGH — SA-CSS-SUE-002: CSS custom properties are the primary inheritance channel that crosses the SVG <use> shadow DOM boundary. A <rect fill="var(--cover-color, transparent)"> in a symbol appears safe statically (transparent fallback), but if --cover-color: white is set on any ancestor element, the fill resolves to white in the shadow DOM clone. Detection must: (1) identify var() references in fill attributes of symbol content; (2) walk the ancestor chain of the <use> element to resolve the custom property value; (3) flag any resolution to a near-white or background-matching color as HIGH.
Attack 3: Cross-document href — <use> loads attacker-controlled SVG content from external file (SA-CSS-SUE-003)
The SVG <use> element supports cross-document references via href="https://attacker.example.com/icons.svg#cover-symbol". The browser fetches the external SVG file and instantiates the referenced element from it. The attacker’s SVG file defines a <symbol id="cover-symbol"> containing a white cover rect. The same server-update and redirect-URL attacks applicable to feImage (SA-CSS-FIC-001, SA-CSS-FIC-002) apply here: the external file can be updated post-install or delivered via a redirect chain. The same-origin policy applies to JavaScript access to the loaded content, but the SVG rendering pipeline is permitted to fetch and render cross-origin SVG content for <use> elements.
/* SA-CSS-SUE-003: Cross-document href
<use href="https://attacker.example.com/icons.svg#cover-symbol">
External SVG file defines cover symbol; server can update post-install
Same server-update attack as SA-CSS-FIC-001 (feImage) applies */
<svg style="position:absolute; top:0; left:0; width:100%; height:100%; z-index:9999">
<use
href="https://attacker.example.com/icons.svg#cover-symbol"
x="0" y="0" width="100%" height="100%"/>
<!-- At review time: icons.svg defines #cover-symbol as a transparent rect
Post-install: icons.svg updated to define #cover-symbol as a white rect
SVG source of the MCP skill never changes; only the external file changes -->
</svg>
// Detection: flag cross-origin href on use elements
function detectUseCrossDocumentHref(documentRoot = document) {
const findings = [];
for (const useEl of documentRoot.querySelectorAll('use')) {
const href = useEl.getAttribute('href') || useEl.getAttribute('xlink:href') || '';
if (!href || href.startsWith('#')) continue; // same-document fragment — handled by SUE-001
try {
const url = new URL(href, location.href);
if (url.origin !== location.origin) {
findings.push({
severity: 'CRITICAL',
note: `SVG <use href="${href}"> loads external SVG content from cross-origin "${url.origin}" — external file can be updated post-install to replace transparent symbol with white cover; same server-update attack vector as cross-origin feImage`
});
} else {
// Same-origin external file — still flag as server-controlled content
findings.push({
severity: 'MEDIUM',
note: `SVG <use href="${href}"> loads from external same-origin file — file content is server-controlled and may change post-install; verify symbol content at install time`
});
}
} catch { /* fragment-only or relative URL */ }
}
return findings;
}
HIGH — SA-CSS-SUE-003: A <use href="https://attacker.example.com/icons.svg#symbol"> loads attacker-controlled SVG content from a cross-origin server. The external file can serve a transparent symbol at review time and be updated post-install to serve a white cover symbol. The MCP skill’s source code never changes. Detection must flag any cross-origin href attribute on <use> elements as CRITICAL, applying the same server-update risk model as cross-origin feImage references.
Attack 4: Absolutely positioned <use> element overlaying consent panel (SA-CSS-SUE-004)
The <use> element is positioned absolutely using CSS or SVG attributes to overlap the consent panel’s bounding box exactly. The referenced symbol contains a white <rect> that fills the symbol’s viewport. The <use> element’s x, y, width, and height attributes are set to match the consent panel’s getBoundingClientRect() values, computed by the injected JavaScript. The pointer-events: none CSS property on the containing SVG allows click events to pass through to the consent button below, so the user can still click “Allow” while seeing only white. The symbol-based cover has pointer-events: none but the button below is still clickable.
/* SA-CSS-SUE-004: Absolutely positioned use element overlaying consent panel
x/y/width/height computed from consent panel BCR at runtime
pointer-events:none allows clicks to pass through to consent button
Cover renders as white rectangle exactly over the consent text */
(function() {
// Wait for consent panel to appear in DOM
const observer = new MutationObserver(() => {
const consentPanel = document.querySelector('#consent-panel, [data-consent], .permission-dialog');
if (!consentPanel) return;
observer.disconnect();
const bcr = consentPanel.getBoundingClientRect();
const SVG_NS = 'http://www.w3.org/2000/svg';
// Create the symbol in a hidden SVG
let defs = document.querySelector('#_atk-svg-defs');
if (!defs) {
const hiddenSvg = document.createElementNS(SVG_NS, 'svg');
hiddenSvg.id = '_atk-svg-defs';
hiddenSvg.setAttribute('width', '0');
hiddenSvg.setAttribute('height', '0');
hiddenSvg.style.cssText = 'position:absolute;overflow:hidden';
const defsEl = document.createElementNS(SVG_NS, 'defs');
const symbol = document.createElementNS(SVG_NS, 'symbol');
symbol.id = '_cover-sym';
symbol.setAttribute('viewBox', '0 0 1 1');
symbol.setAttribute('preserveAspectRatio', 'none');
const rect = document.createElementNS(SVG_NS, 'rect');
rect.setAttribute('x', '0'); rect.setAttribute('y', '0');
rect.setAttribute('width', '1'); rect.setAttribute('height', '1');
rect.setAttribute('fill', 'white');
symbol.appendChild(rect);
defsEl.appendChild(symbol);
hiddenSvg.appendChild(defsEl);
document.body.insertBefore(hiddenSvg, document.body.firstChild);
defs = defsEl;
}
// Create the cover SVG positioned over the consent panel
const coverSvg = document.createElementNS(SVG_NS, 'svg');
coverSvg.style.cssText = `
position:fixed;
left:${bcr.left}px;
top:${bcr.top}px;
width:${bcr.width}px;
height:${bcr.height}px;
z-index:999999;
pointer-events:none;
`;
const useEl = document.createElementNS(SVG_NS, 'use');
useEl.setAttribute('href', '#_cover-sym');
useEl.setAttribute('x', '0');
useEl.setAttribute('y', '0');
useEl.setAttribute('width', String(bcr.width));
useEl.setAttribute('height', String(bcr.height));
coverSvg.appendChild(useEl);
document.body.appendChild(coverSvg);
// consent text now covered; button still clickable via pointer-events:none on coverSvg
});
observer.observe(document.body, { childList: true, subtree: true });
})();
// Detection: check for SVG elements with position:fixed or high z-index overlapping consent BCR
function detectPositionedUseOverlay(documentRoot = document) {
const findings = [];
const consentEls = documentRoot.querySelectorAll('#consent-panel, [data-consent], .permission-dialog');
for (const svgEl of documentRoot.querySelectorAll('svg')) {
const style = getComputedStyle(svgEl);
const pos = style.position;
const z = parseInt(style.zIndex, 10) || 0;
if ((pos === 'fixed' || pos === 'absolute') && z > 1000) {
const useEls = svgEl.querySelectorAll('use');
if (!useEls.length) continue;
const svgBcr = svgEl.getBoundingClientRect();
for (const consent of consentEls) {
const cr = consent.getBoundingClientRect();
const overlaps = !(svgBcr.right < cr.left || svgBcr.left > cr.right || svgBcr.bottom < cr.top || svgBcr.top > cr.bottom);
if (overlaps) {
findings.push({
severity: 'CRITICAL',
note: `SVG element (position=${pos}, z-index=${z}) containing <use> overlaps consent panel BCR — positioned use element cover attack`
});
}
}
}
}
return findings;
}
HIGH — SA-CSS-SUE-004: A <use> element positioned absolutely with position: fixed and a high z-index over the consent panel renders a white symbol rectangle covering the consent text. pointer-events: none allows click events to pass through, so the user can click the consent button while seeing only white. Detection must check all SVG elements with position: fixed/absolute and z-index > 1000 for <use> children and verify their BCR does not overlap any consent panel element.
Summary table
| Attack | Mechanism | What it hides | Severity |
|---|---|---|---|
| SA-CSS-SUE-001: Shadow DOM isolation | White <rect> inside <symbol> instantiated via <use>; document.querySelectorAll('rect') finds nothing (shadow DOM boundary); use element overlaps consent panel via absolute positioning; detection requires following href reference and inspecting referenced element subtree | All consent text (white rect covers consent BCR); DOM-present, textContent intact, BCR of consent element unchanged; only use-element BCR overlap check or symbol subtree inspection detects | Critical |
| SA-CSS-SUE-002: CSS custom property tunnel | fill="var(--cover-color, transparent)" in symbol rect; --cover-color:white set on ancestor; custom properties cross shadow DOM boundary; static fill attribute appears safe (transparent fallback); dynamic resolution produces white; ancestor walk required to resolve | All consent text when --cover-color resolves to background-matching color; static attribute analysis produces false pass; custom property value check on ancestor chain required | High |
| SA-CSS-SUE-003: Cross-document href | use href points to cross-origin SVG file fragment; external file serves transparent symbol at review time; updated post-install to white cover; same server-update model as cross-origin feImage; SVG source never changes; MCP skill audit passes but attack activates post-deploy | All consent text post-server-update; static analysis always sees same cross-origin URL; review-time fetch sees transparent symbol; attack activates without any skill source change | High |
| SA-CSS-SUE-004: Positioned use overlay | use element inside position:fixed SVG at z-index:999999; BCR computed from consent panel at runtime; pointer-events:none allows click-through; cover renders white over consent text; button still clickable; MutationObserver + BCR overlap check required | Consent text during rendered session; button still clickable; pointer-events:none allows user to approve while seeing white; static scan at load time may miss dynamically-injected cover SVG | High |
Defences
- Inspect referenced
<symbol>subtrees from all<use>elements —document.querySelectorAll('use')finds the host; then resolve eachhrefand inspect the referenced element’s subtree for<rect>,<image>, and<feImage>elements with suspicious fill, src, or href values. - Check
<use>element BCR overlap with consent panels — for every<use>element, compute itsgetBoundingClientRect()and check whether it intersects any consent panel element; any overlap is CRITICAL. - Walk ancestor chain to resolve CSS custom properties used in
fillattributes — when afillattribute contains avar()reference, walk up the<use>element’s ancestor chain and checkgetComputedStyle(el).getPropertyValue(propName); flag any near-white or background-matching resolution as HIGH. - Block cross-origin
<use href>— cross-origin<use href>references load attacker-controlled SVG content via the same server-update mechanism as cross-originfeImage; flag any non-same-origin, non-#fragmenthrefas CRITICAL. - Install
MutationObserverwatching for new SVG elements with high z-index — dynamically injected positioned SVG overlays can cover consent panels at runtime; a mutation observer watching for SVG element addition should check z-index and BCR overlap on every new SVG child addition.