MCP server SVG text presentation attribute consent security
SVG <text> elements support a parallel attribute system for visual properties that is distinct from the CSS property system. fill, opacity, transform, and textLength as SVG presentation attributes have different APIs, different resolution rules, and different detection methods compared to their CSS equivalents. Audit tools built for HTML consent text — which query getComputedStyle for color, opacity, and transform — check the CSS property system and miss attacks delivered via SVG presentation attributes. Four SA-SVG-TXT patterns cover the primary SVG-specific consent attack vectors.
SVG presentation attributes and CSS properties have a defined interaction in the SVG specification: CSS properties take precedence over presentation attributes for inherited properties (like fill and opacity), but the SVG attribute is the fallback when no CSS property is set. An audit tool that only inspects CSS computed properties will see the default CSS values, not the SVG presentation attribute values, creating a detection gap for SVG-specific attacks.
SA-SVG-TXT-001: SVG text fill="none" — no glyph fill pixels
SVG <text fill="none"> sets the SVG fill presentation attribute to none. Unlike CSS color: transparent (which renders glyph-shaped transparent pixels over a background), SVG fill="none" tells the SVG renderer not to apply any fill paint to the glyph shapes at all. The glyph shapes are computed (the text has layout dimensions), but no pixels are emitted for the fill. If no CSS color property is set and no SVG stroke is applied, the text is entirely invisible: zero fill pixels, zero stroke pixels.
<svg width="400" height="60">
<text
x="10" y="30"
fill="none" /* SVG presentation attribute: no fill on glyphs */
font-size="16"
font-family="sans-serif"
>By clicking Allow you authorize file access.</text>
</svg>
/* CSS audit result:
getComputedStyle(textEl).color → 'rgb(0, 0, 0)' (the CSS color default)
→ CSS auditor reports: correct black text color
→ No CSS anomaly detected
SVG attribute reality:
textEl.getAttribute('fill') → 'none'
→ No fill applied to glyphs regardless of CSS color
→ Text is invisible (zero fill pixels emitted)
Why CSS color ≠ visual result for SVG fill:
For HTML elements: CSS color IS the fill color.
For SVG elements: CSS color sets the currentColor value;
the actual fill comes from the SVG fill attribute/property.
SVG fill='none' overrides currentColor → no fill pixels.
Detection:
el.getAttribute('fill') === 'none' on SVG text element → CRITICAL
OR el.getAttribute('fill') === 'transparent' → CRITICAL
Note: CSS getComputedStyle check for color will NOT detect this. */
SVG fill ≠ CSS color: The mental model of “check getComputedStyle.color to find invisible text” does not apply to SVG text elements. SVG has its own paint model: fill controls the interior of glyph shapes; stroke controls the outline. Neither is the CSS color property. An SVG text element with fill="none" and getComputedStyle.color = 'rgb(0,0,0)' reports normal via CSS audit but renders zero visible pixels.
fill="none" on <text>: no fill pixels emitted; CSS color audit reports normal. Detection: el.getAttribute('fill') === 'none' on SVG text element → CRITICAL (not detectable via getComputedStyle.color).SA-SVG-TXT-002: SVG text opacity="0.01" — SVG attribute vs CSS property audit gap
SVG elements support an opacity presentation attribute that is separate from the CSS opacity property. getComputedStyle(el).opacity returns the CSS opacity property value; it does not reflect the SVG opacity attribute unless the attribute has been resolved into the CSS computed style. In practice, browsers resolve SVG presentation attributes into the CSS cascade for inherited properties, but the exact behavior for opacity depends on the browser's implementation of SVG attribute cascading. Auditors that rely solely on getComputedStyle(el).opacity may miss opacity="0.01" set as an SVG attribute, especially in older browser versions or SVG elements created outside the normal HTML document flow.
<svg>
<text opacity="0.01" fill="black" font-size="14">
Authorize file access.
</text>
</svg>
/* Detection approach 1 — CSS computed style (may work in modern browsers):
getComputedStyle(textEl).opacity → '0.01' (if CSS cascade resolves SVG attr)
→ this works in Chrome/Firefox/Safari as of 2024
BUT: older browsers, SVG documents, or SVG injected as data: URI may not reflect
Detection approach 2 — SVG attribute direct check (authoritative):
textEl.getAttribute('opacity') → '0.01'
parseFloat('0.01') < 0.1 → CRITICAL
Detection approach 3 — SVGAnimatedNumber (handles animations):
textEl.style.opacity OR textEl.getAttribute('opacity')
For animated SVG opacity: textEl.getAnimations() → check opacity keyframes
Best practice detection: check BOTH getAttribute('opacity') AND
getComputedStyle(el).opacity to catch all vectors. */
opacity="0.01" attribute: near-invisible; getComputedStyle may not reflect SVG attribute in all contexts. Detection: getAttribute('opacity') < 0.1 on SVG text element → CRITICAL (use attribute check, not only getComputedStyle).SA-SVG-TXT-003: SVG text transform="scale(0.001 0.001)" — sub-pixel collapse
SVG elements support a transform presentation attribute that takes an SVG transform string (e.g., scale(0.001 0.001), translate(2000, 0)). This attribute is separate from the CSS transform property in its API: el.getAttribute('transform') returns the SVG transform attribute string; getComputedStyle(el).transform returns the CSS transform matrix. Auditors checking CSS transform for suspicious scale values need to also check the SVG transform attribute. scale(0.001 0.001) collapses the text to 0.001× its rendered size — a 100px-wide text block collapses to 0.1px — completely invisible at any display density.
<svg viewBox="0 0 400 60">
<text
transform="scale(0.001 0.001)" /* SVG attribute: shrinks to 0.1% of size */
x="10" y="30"
fill="black"
font-size="16"
>By clicking Allow you authorize file access.</text>
</svg>
/* CSS transform check:
getComputedStyle(textEl).transform →
'matrix(0.001, 0, 0, 0.001, 0, 0)' (if CSS cascade resolves SVG attr)
OR: 'none' (if CSS transform is separate from SVG transform attribute in this context)
SVG attribute check (authoritative):
textEl.getAttribute('transform') → 'scale(0.001 0.001)'
Using SVGTransform API for precise detection:
const transforms = textEl.transform.baseVal; // SVGTransformList
for (const t of transforms) {
if (t.type === SVGTransform.SVG_TRANSFORM_SCALE) {
if (Math.abs(t.matrix.a) < 0.01 || Math.abs(t.matrix.d) < 0.01) {
→ CRITICAL: scale below 1% on SVG text element
}
}
}
Also check for translation outside viewport:
SVGTransform.SVG_TRANSFORM_TRANSLATE with large dx/dy → CRITICAL */
transform="scale(0.001 0.001)": text collapsed to sub-pixel; SVG transform attribute separate from CSS transform property. Detection: el.transform.baseVal scale matrix < 0.01 → CRITICAL.SA-SVG-TXT-004: SVG text textLength="1" — character-crush to 1px
SVG's textLength attribute forces the total rendered length of a text element to a specific pixel value by adjusting inter-glyph spacing or glyph scaling (controlled by the lengthAdjust attribute). textLength="1" forces all characters in the text into a 1-pixel total width: each character is scaled and spaced so that the entire text string occupies 1px horizontally. On a consent text string of 50 characters, each character occupies 0.02px — completely invisible. This attack has no CSS equivalent: there is no CSS property that forces the total rendered text length to a specific pixel value. It exists only in SVG.
<svg width="400" height="40">
<text
x="10" y="25"
textLength="1" /* force total width to 1px */
lengthAdjust="spacingAndGlyphs" /* both glyph and spacing compressed */
fill="black"
font-size="16"
>By clicking Allow you authorize permanent file system access to all documents.</text>
</svg>
/* Visual result:
50 characters compressed into 1 horizontal pixel → invisible cluster
getBoundingClientRect().width → ~1 (reflects textLength)
textContent → full consent string (DOM-present)
CSS equivalent: none (textLength is SVG-specific)
getComputedStyle check: no relevant CSS property to check
Detection:
textEl.getAttribute('textLength') → '1'
parseFloat('1') < 10 on a text string with length > 5 characters → CRITICAL
More precise: compute expected minimum readable width:
const charCount = textEl.textContent.trim().length;
const textLen = parseFloat(textEl.getAttribute('textLength') || '0');
if (textLen > 0 && textLen / charCount < 2) → CRITICAL
(less than 2px per character average → illegible) */
textLength is SVG-unique with no CSS analogue: No CSS property compresses total text width to a specific pixel value. CSS width on a block element clips or wraps text; it does not compress glyph rendering. textLength compresses glyph rendering itself. Auditors that only check CSS properties and HTML attributes will never encounter this attack vector, which requires SVG-specific attribute inspection.
textLength="1": all characters compressed into 1px width; no CSS equivalent; CSS-only auditors cannot detect. Detection: getAttribute('textLength') < 2px per character → CRITICAL.Summary of SA-SVG-TXT findings
| ID | Severity | SVG attribute | Attack mechanism | Detection (CSS getComputedStyle) |
|---|---|---|---|---|
| SA-SVG-TXT-001 | CRITICAL | fill="none" | No fill on glyph shapes; zero fill pixels emitted | Missed — CSS color appears normal |
| SA-SVG-TXT-002 | CRITICAL | opacity="0.01" | Near-zero opacity via SVG attribute, not CSS property | Partial — may resolve in modern browsers |
| SA-SVG-TXT-003 | CRITICAL | transform="scale(0.001)" | Sub-pixel scale via SVG transform, not CSS transform | Partial — CSS transform may reflect SVG attr |
| SA-SVG-TXT-004 | CRITICAL | textLength="1" | All characters crushed to 1px; SVG-unique, no CSS equivalent | Missed — no CSS property to check |