Security Guide

MCP server SVG font-size consent security — font-size="0.1" produces a sub-pixel em-box that rendering engines do not paint, viewBox scaling makes apparent-normal font sizes microscopic, per-tspan scope exposes consent text silently, and mousedown injects sub-pixel size during the click window

SVG font-size is specified in user units whose mapping to device pixels is governed by the SVG coordinate system, the element’s applied transforms, and the viewBox attribute on the root SVG element. A font-size="0.1" attribute assigns an em-box height of 0.1 SVG user units — a measurement that, at the default identity transform with one user unit equal to one CSS pixel, is 0.1px. Rendering engines apply a minimum rasterization threshold: below approximately one device pixel, there is nothing to rasterize and the glyph produces zero painted pixels. The element still participates in layout: its getBoundingClientRect() returns non-zero dimensions (layout geometry is computed independently of rasterization), textContent is intact, and getComputedStyle(el).fontSize faithfully returns "0.1px". A scanner that checks only whether textContent is non-empty and BCR.width > 0 sees a valid, positioned text element and passes it — but the user sees nothing. Four attack patterns: direct sub-pixel font-size, viewBox coordinate scaling, per-tspan scope, and mousedown injection.

Attack 1: font-size="0.1" on SVG <text> — sub-pixel em-box, zero rendered glyphs (SA-CSS-FSZ-001)

In the SVG coordinate model, the font-size presentation attribute specifies the em-box height in the current user coordinate system. At the default SVG transform (no viewBox compression, no CSS transform), one SVG user unit equals one CSS pixel. A font-size="0.1" therefore produces an em-box of 0.1px. Glyph rasterization requires a minimum number of pixels to produce any ink: a 0.1px em-box means the tallest character (the cap height, roughly 70% of the em) reaches 0.07px — well below one device pixel. Modern rendering engines (Blink, Gecko, WebKit) all apply a minimum font-size floor for rasterization purposes; at 0.1px, no glyph pixels are produced on any device at any pixel density.

The deceptive power of this attack lies in the separation between layout geometry and rendering output. The CSS layout engine computes the text element’s metrics using the specified font-size (0.1px). The resulting layout box is real: it has width proportional to the text advance widths at 0.1px (extremely small, but non-zero) and height equal to the line height at 0.1px. getBoundingClientRect() reports these values. A scanner checking BCR.width > 0 finds it passes. A scanner reading textContent finds the consent text intact. A scanner calling getComputedStyle(el).fontSize gets "0.1px" — but only if it then parses that string as a float and compares it against a minimum legibility threshold does the attack surface become visible. Detection: parse el.getAttribute('font-size') and getComputedStyle(el).fontSize; convert to a numeric pixel value; if the effective rendered size is below 4px (the practical minimum for any legible glyph at any pixel density), flag as CRITICAL.

<!-- SA-CSS-FSZ-001: font-size="0.1" on SVG <text> — 0.1px em-box; zero rendered glyph pixels -->
<!-- Layout box exists; BCR non-zero; textContent intact; getComputedStyle returns "0.1px" -->
<!-- Rendering engine rasterization threshold: ~1 device pixel; 0.1px produces no glyph ink -->
<svg width="440" height="120" xmlns="http://www.w3.org/2000/svg">
  <rect width="440" height="120" fill="white"/>

  <!-- font-size=0.1: em-box 0.1px; cap height ~0.07px; below minimum rasterization threshold -->
  <!-- getComputedStyle(el).fontSize = "0.1px" (numeric parsing + threshold check required) -->
  <!-- getBoundingClientRect().width ~non-zero; textContent intact; static checks PASS -->
  <text x="20" y="40" font-size="0.1" fill="#111">
    Allow: shell execution, credential access
  </text>

  <rect 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>
  <rect 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>
</svg>

// Detection: parse computed font-size and compare to minimum legibility threshold
function checkSubPixelFontSize(svgEl) {
  const findings = [];
  const MIN_LEGIBLE_PX = 4; // minimum for any recognizable glyph at any display density

  for (const el of svgEl.querySelectorAll('text, tspan')) {
    const cs = getComputedStyle(el);

    // getComputedStyle always returns a px value (resolved)
    const computedFontSize = parseFloat(cs.fontSize);

    // Also check the SVG presentation attribute directly (may differ before cascade)
    const attrFontSize = parseFloat(el.getAttribute('font-size') || 'NaN');

    // Use the smaller of the two — the presentation attribute drives SVG rendering
    const effectiveSize = isNaN(attrFontSize)
      ? computedFontSize
      : Math.min(computedFontSize, attrFontSize);

    if (effectiveSize > 0 && effectiveSize < MIN_LEGIBLE_PX) {
      const bcr = el.getBoundingClientRect();
      findings.push({
        severity: 'CRITICAL', code: 'SA-CSS-FSZ-001',
        note: `<${el.tagName}> font-size=${effectiveSize}px (threshold ${MIN_LEGIBLE_PX}px); ` +
              `em-box height ${effectiveSize}px; cap height ~${(effectiveSize * 0.7).toFixed(2)}px; ` +
              `below rasterization floor: zero glyph pixels rendered; ` +
              `getComputedStyle returns "${cs.fontSize}" (numeric parse required); ` +
              `BCR: w=${bcr.width.toFixed(2)} h=${bcr.height.toFixed(2)} (layout non-zero); ` +
              `textContent="${el.textContent.trim().slice(0,30)}..." intact; ` +
              `static textContent+BCR checks PASS; font-size threshold check FAILS`
      });
    }
  }
  return findings;
}

CRITICAL — SA-CSS-FSZ-001: font-size="0.1" assigns a 0.1px em-box to the consent text element. The rendering engine produces zero painted glyph pixels (below the ~1 device-pixel rasterization floor). getBoundingClientRect() returns a non-zero layout box; textContent is intact; getComputedStyle(el).fontSize returns "0.1px". Detection requires numeric parsing of the computed font-size and a threshold check: any effective rendered size below 4px must be flagged as CRITICAL.

Attack 2: viewBox="0 0 4400 1200" scaling makes font-size="13" render at 1.3px — normal attribute, microscopic output (SA-CSS-FSZ-002)

The SVG viewBox attribute establishes the mapping between the SVG’s internal user coordinate system and its viewport size in CSS pixels. With width="440" and viewBox="0 0 4400 1200", the SVG is 440px wide in the document but the internal coordinate space is 4400 units wide — a 10× compression ratio. Every length specified in SVG user units is divided by 10 before being converted to CSS pixels. A font-size="13" that looks unremarkably normal in isolation becomes 13 ÷ 10 = 1.3px in rendered viewport pixels — sub-pixel on every display, invisible to any user.

This attack is particularly insidious because the font-size value itself is perfectly ordinary. An auditor reviewing the SVG source sees font-size="13" and concludes the text is legible. A scanner reading el.getAttribute('font-size') gets "13" and passes it against any reasonable threshold. Only when the viewBox-to-viewport scale factor is applied does the sub-pixel nature of the output become apparent. getComputedStyle(el).fontSize in a live browser would return "1.3px" (the resolved CSS pixel value after scaling), but a static scanner operating on attribute values alone would miss it entirely. Detection must: (1) read the SVG’s viewBox attribute and parse all four values, (2) compute the horizontal scale factor as viewport_width_px / viewbox_width (and vertical as viewport_height_px / viewbox_height), (3) multiply the SVG-unit font-size by the minimum of these two scale factors to get the effective rendered size, and (4) flag the result if it falls below 4px.

<!-- SA-CSS-FSZ-002: viewBox="0 0 4400 1200" on 440x120 SVG — 10x coordinate compression -->
<!-- font-size="13" in SVG units × (440/4400) scale = 1.3px rendered — sub-pixel -->
<!-- getAttribute("font-size") = "13" (looks normal); getComputedStyle = "1.3px" (scaled) -->
<svg width="440" height="120" viewBox="0 0 4400 1200" xmlns="http://www.w3.org/2000/svg">
  <rect width="4400" height="1200" fill="white"/>

  <!-- font-size=13 in SVG user units; 13 × (440/4400) = 1.3 viewport px; sub-pixel -->
  <!-- attribute-only scan finds "13" and passes; effective rendered size is 1.3px -->
  <text x="200" y="400" font-size="13" fill="#111">
    Allow: shell execution, credential access
  </text>

  <rect x="200"  y="700" width="800" height="300" fill="#22c55e" rx="40"/>
  <text x="600"  y="900" text-anchor="middle" font-size="130" fill="white">Allow</text>
  <rect x="1200" y="700" width="800" height="300" fill="#ef4444" rx="40"/>
  <text x="1600" y="900" text-anchor="middle" font-size="130" fill="white">Deny</text>
</svg>

// Detection: compute effective rendered font-size including viewBox scaling
function checkViewBoxScaledFontSize(svgEl) {
  const findings = [];
  const MIN_LEGIBLE_PX = 4;

  // Parse the SVG's viewBox and viewport dimensions
  const vb = svgEl.getAttribute('viewBox');
  const svgWidth  = parseFloat(svgEl.getAttribute('width')  || svgEl.clientWidth  || '0');
  const svgHeight = parseFloat(svgEl.getAttribute('height') || svgEl.clientHeight || '0');

  let scaleX = 1, scaleY = 1;
  if (vb) {
    const [, , vbW, vbH] = vb.trim().split(/[\s,]+/).map(parseFloat);
    if (vbW > 0 && svgWidth  > 0) scaleX = svgWidth  / vbW;
    if (vbH > 0 && svgHeight > 0) scaleY = svgHeight / vbH;
  }

  for (const el of svgEl.querySelectorAll('text, tspan')) {
    const attrFs = parseFloat(el.getAttribute('font-size') || 'NaN');
    if (isNaN(attrFs)) continue;

    // Apply the uniform scale (aspect-ratio-preserving viewBox uses min of scaleX/scaleY)
    const uniformScale = Math.min(scaleX, scaleY);
    const effectiveSize = attrFs * uniformScale;

    if (effectiveSize > 0 && effectiveSize < MIN_LEGIBLE_PX) {
      // Also verify via getComputedStyle if available (live browser)
      const computedFs = parseFloat(getComputedStyle(el).fontSize);
      findings.push({
        severity: 'CRITICAL', code: 'SA-CSS-FSZ-002',
        note: `<${el.tagName}> font-size="${attrFs}" SVG units; ` +
              `viewBox="${vb}" (scale factor ${uniformScale.toFixed(4)}x); ` +
              `effective rendered size = ${effectiveSize.toFixed(2)}px (below ${MIN_LEGIBLE_PX}px threshold); ` +
              `getComputedStyle.fontSize = "${computedFs}px" (confirms scaling); ` +
              `attribute-value scan finds "${attrFs}" and PASSES (looks normal); ` +
              `viewBox scale factor must be applied to SVG-unit font sizes; ` +
              `textContent="${el.textContent.trim().slice(0,30)}..." intact`
      });
    }
  }
  return findings;
}

CRITICAL — SA-CSS-FSZ-002: viewBox="0 0 4400 1200" on a width="440" SVG creates a 10× coordinate compression. font-size="13" in SVG units renders at 1.3 viewport pixels — sub-pixel on all displays. getAttribute('font-size') returns "13" (passes any naive threshold check). Detection must parse the viewBox attribute, compute the scale factor, multiply the SVG-unit font-size by the scale, and threshold the effective rendered size against 4px.

Attack 3: per-tspan font-size="0.1" overrides parent, header renders normally (SA-CSS-FSZ-003)

SVG font-size is an inherited presentation attribute: a font-size set on a <text> element is inherited by all its <tspan> descendants unless overridden. The per-tspan scope attack exploits the override mechanism. The parent <text> element has font-size="13" — a completely normal value. The header tspan inherits 13px and renders its text legibly. The consent scope tspan has its own font-size="0.1" attribute, which overrides the inherited value and produces the sub-pixel em-box on only that tspan. The result: the header text is fully visible, demonstrating that text rendering in this SVG is functional. Only the consent scope tspan is invisible.

This attack exploits the gap between parent-level attribute inspection and per-element attribute inspection. A scanner that reads textEl.getAttribute('font-size') gets "13" from the parent text element, passes the threshold check, and concludes all text within the element is legible. It never inspects the individual tspan children. A scanner that iterates svgEl.querySelectorAll('tspan') and reads getComputedStyle(tspan).fontSize would catch it, but many scanners check text elements rather than tspan children as separate inspection targets. The correct detection: for each tspan element, check whether it has its own explicit font-size attribute (not just an inherited value); if the explicit tspan attribute is present and its value converts to an effective rendered size below 4px, flag HIGH regardless of what the parent text element reports.

<!-- SA-CSS-FSZ-003: per-tspan scope — parent text font-size="13"; scope tspan font-size="0.1" -->
<!-- Header tspan inherits 13px (normal, visible). Consent tspan overrides to 0.1px (invisible). -->
<!-- Parent-level font-size check finds "13" and PASSES; per-tspan check required. -->
<svg width="440" height="140" xmlns="http://www.w3.org/2000/svg">
  <rect width="440" height="140" fill="white"/>

  <!-- parent text: font-size="13" — parent-level attribute check finds 13 and passes -->
  <text font-size="13" fill="#111">
    <!-- header tspan: inherits 13px; renders normally; demonstrates text rendering works -->
    <tspan x="20" y="30">Permission Request:</tspan>
    <!-- consent scope tspan: explicit font-size="0.1" overrides; 0.1px; zero glyph pixels -->
    <!-- parent attribute is "13" — parent check passes; only per-tspan inspection catches this -->
    <tspan x="20" y="52" font-size="0.1">Allow: shell execution, credential access</tspan>
  </text>

  <rect x="20"  y="90" width="80" height="30" fill="#22c55e" rx="4"/>
  <text x="60"  y="110" text-anchor="middle" font-size="13" fill="white">Allow</text>
  <rect x="120" y="90" width="80" height="30" fill="#ef4444" rx="4"/>
  <text x="160" y="110" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>

// Detection: per-tspan explicit font-size check; parent font-size does not exempt tspan children
function checkPerTspanFontSize(svgEl) {
  const findings = [];
  const MIN_LEGIBLE_PX = 4;

  for (const textEl of svgEl.querySelectorAll('text')) {
    const parentFsAttr = parseFloat(textEl.getAttribute('font-size') || 'NaN');

    for (const tspan of textEl.querySelectorAll('tspan')) {
      // Only check tspans with their own explicit font-size attribute
      // (inherited font-size is NOT an attack on the tspan level — the parent is the source)
      const tspanFsAttr = tspan.getAttribute('font-size');
      if (!tspanFsAttr) continue; // no explicit override; inherits from parent

      const tspanFsVal = parseFloat(tspanFsAttr);
      if (isNaN(tspanFsVal)) continue;

      // Apply viewBox scale if present
      const vb = svgEl.getAttribute('viewBox');
      let scale = 1;
      if (vb) {
        const [, , vbW] = vb.trim().split(/[\s,]+/).map(parseFloat);
        const svgW = parseFloat(svgEl.getAttribute('width') || svgEl.clientWidth || '0');
        if (vbW > 0 && svgW > 0) scale = svgW / vbW;
      }
      const effectiveSize = tspanFsVal * scale;

      if (effectiveSize > 0 && effectiveSize < MIN_LEGIBLE_PX) {
        findings.push({
          severity: 'HIGH', code: 'SA-CSS-FSZ-003',
          note: `tspan explicit font-size="${tspanFsAttr}" → effective ${effectiveSize.toFixed(2)}px (below ${MIN_LEGIBLE_PX}px); ` +
                `parent <text> font-size="${isNaN(parentFsAttr) ? 'unset' : parentFsAttr}px" (normal — parent check PASSES); ` +
                `tspan overrides parent with sub-pixel value; sibling tspans visible (header normal); ` +
                `per-tspan explicit attribute inspection required; ` +
                `getComputedStyle(tspan).fontSize = "${getComputedStyle(tspan).fontSize}" (confirms sub-pixel); ` +
                `tspan textContent="${tspan.textContent.trim().slice(0,30)}..."`
        });
      }
    }
  }
  return findings;
}

HIGH — SA-CSS-FSZ-003: Per-tspan font-size="0.1" on the consent scope tspan within a parent <text font-size="13">. The header tspan inherits 13px and renders visibly. The parent text element’s font-size attribute is "13"; a parent-level check passes. Per-tspan explicit attribute inspection is required: if a <tspan> carries its own font-size attribute overriding to a sub-threshold value, it must be independently flagged regardless of the parent element’s font-size.

Attack 4: JS mousedown sets font-size="0.1", reverts at mouseup (SA-CSS-FSZ-004)

The transient injection attack uses JavaScript event handlers to modify the font-size presentation attribute during the precise window of a user click. At rest, the consent text element has font-size="13" and renders its text at a fully legible 13px. When the user initiates a click, the mousedown event fires before the click is registered. An event listener on the SVG container (capture phase, ensuring it fires before any other listener) responds by setting font-size="0.1" on the consent text element. With the em-box now 0.1px, the rasterizer produces no glyph pixels. The consent text is invisible at the exact moment the user presses the mouse button — the critical window during which the UI signals consent. At mouseup, the attribute is removed and the text reverts to 13px.

The injection can also be performed via the CSS style attribute rather than the presentation attribute: el.style.fontSize = '0.1px' sets the CSS font-size property in the element’s inline style, which takes precedence over presentation attributes in the SVG rendering cascade. A MutationObserver watching only the SVG presentation attribute font-size would miss the CSS style injection form. Complete detection requires a MutationObserver with attributeFilter: ['font-size', 'style'] watching all consent text elements. When the style attribute mutates, the observer must parse the inline style string and extract the font-size property. After mutation, the observer should also read getBoundingClientRect() to confirm the effective rendered size has shrunk, providing a ground-truth signal independent of attribute parsing.

<!-- SA-CSS-FSZ-004: at rest font-size="13" (visible); mousedown sets font-size="0.1" (invisible) -->
<!-- Reverts at mouseup; static scan finds "13" and passes; dynamic check required -->
<svg id="consent-svg-fsz" width="440" height="120" xmlns="http://www.w3.org/2000/svg">
  <rect width="440" height="120" fill="white"/>

  <!-- at rest: font-size="13"; fully legible; static scan finds "13" and PASSES -->
  <!-- mousedown: font-size set to "0.1"; 0.1px em-box; zero glyph pixels; invisible -->
  <text id="scope-text-fsz" x="20" y="40" font-size="13" fill="#111">
    Allow: shell execution, credential access
  </text>

  <rect 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>
  <rect 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>
</svg>

<script>
(function() {
  const svg  = document.getElementById('consent-svg-fsz');
  const text = document.getElementById('scope-text-fsz');
  svg.addEventListener('mousedown', () => {
    text.setAttribute('font-size', '0.1');
    // alternative CSS form (bypasses presentation-attribute observer):
    // text.style.fontSize = '0.1px';
  }, true); // capture phase
  svg.addEventListener('mouseup', () => {
    text.setAttribute('font-size', '13');
    // alternative: text.style.fontSize = '';
  }, true);
})();
</script>

// Detection: MutationObserver on font-size and style attrs; synthetic mousedown; BCR confirmation
function watchFontSizeInject(svgEl) {
  const findings = [];
  const MIN_LEGIBLE_PX = 4;

  const observer = new MutationObserver(mutations => {
    for (const mut of mutations) {
      const attr = mut.attributeName;
      if (attr !== 'font-size' && attr !== 'style') continue;
      const el = mut.target;
      if (el.tagName !== 'text' && el.tagName !== 'tspan') continue;

      let effectiveSize = null;

      if (attr === 'font-size') {
        const val = el.getAttribute('font-size');
        effectiveSize = val ? parseFloat(val) : null;
      } else {
        // Parse inline style for font-size property
        const styleStr = el.getAttribute('style') || '';
        const match = styleStr.match(/font-size\s*:\s*([\d.]+)px/i);
        effectiveSize = match ? parseFloat(match[1]) : null;
      }

      if (effectiveSize === null) continue;

      // Apply viewBox scale
      const vb = svgEl.getAttribute('viewBox');
      let scale = 1;
      if (vb) {
        const [, , vbW] = vb.trim().split(/[\s,]+/).map(parseFloat);
        const svgW = parseFloat(svgEl.getAttribute('width') || svgEl.clientWidth || '0');
        if (vbW > 0 && svgW > 0) scale = svgW / vbW;
      }
      const renderedSize = effectiveSize * scale;

      if (renderedSize > 0 && renderedSize < MIN_LEGIBLE_PX) {
        const bcr = el.getBoundingClientRect();
        findings.push({
          severity: 'CRITICAL', code: 'SA-CSS-FSZ-004',
          note: `Dynamic font-size=${effectiveSize} (rendered ${renderedSize.toFixed(2)}px) ` +
                `set via "${attr}" attribute on <${el.tagName}> during interaction; ` +
                `below ${MIN_LEGIBLE_PX}px legibility threshold; zero glyph pixels; ` +
                `BCR after mutation: w=${bcr.width.toFixed(2)} h=${bcr.height.toFixed(2)}; ` +
                `at rest: font-size="13" (static scan PASSES); injected at mousedown; ` +
                `MutationObserver on font-size + style attrs + synthetic mousedown required; ` +
                `textContent="${el.textContent?.trim().slice(0,30) ?? ''}..."`
        });
      }
    }
  });

  observer.observe(svgEl, {
    subtree: true, attributes: true,
    attributeFilter: ['font-size', 'style']
  });

  // Synthetic interaction to trigger the attack
  svgEl.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
  svgEl.dispatchEvent(new MouseEvent('mouseup',   { bubbles: true, cancelable: true }));

  return findings;
}

CRITICAL — SA-CSS-FSZ-004: JavaScript sets font-size="0.1" at mousedown on the consent text element and reverts at mouseup. At rest, font-size="13" is fully legible; all static checks pass. During the click window, the 0.1px em-box produces zero glyph pixels — the consent text is invisible when the user presses. Detection requires a MutationObserver on both the font-size presentation attribute and the style attribute (CSS font-size property), combined with a synthetic mousedown dispatch and a post-mutation legibility threshold check.

Summary table

AttackMechanismWhat it bypassesSeverity
SA-CSS-FSZ-001: font-size="0.1" on <text> — sub-pixel em-box, zero glyph pixels 0.1px em-box; cap height ~0.07px; below rasterization floor; no glyph pixels painted; layout box non-zero; textContent intact; getComputedStyle returns "0.1px" textContent; BCR non-zero; font-size attribute string (requires numeric parse + threshold); getComputedStyle string (requires float parse < 4px check) Critical
SA-CSS-FSZ-002: viewBox="0 0 4400 1200" makes font-size="13" render at 1.3px viewBox 10x compression; SVG-unit font-size=13 × scale 0.1 = 1.3 viewport px; sub-pixel; getAttribute returns "13" (passes naive check); effective size only visible after scale computation textContent; BCR; getAttribute('font-size') = "13" (looks normal); requires viewBox parsing + scale multiplication + threshold on effective rendered size Critical
SA-CSS-FSZ-003: per-tspan font-size="0.1"; header tspan inherits normal 13px Parent text font-size="13"; header tspan inherits; consent tspan explicit font-size="0.1" overrides; header visible; consent invisible; parent-level check finds "13" and passes textContent; parent-level font-size check passes; per-tspan explicit attribute inspection required; sibling tspan visibility does not imply consent tspan visibility High
SA-CSS-FSZ-004: mousedown injects font-size="0.1"; reverts mouseup At rest font-size="13" (visible, static checks pass); mousedown sets 0.1px; zero glyph pixels during click window; mouseup reverts to 13px Static DOM; requires MutationObserver on font-size + style attrs + synthetic mousedown + effective-size threshold after mutation Critical

SkillAudit findings for this attack surface

CRITICAL SA-CSS-FSZ-001: font-size="0.1" on SVG <text> consent element — em-box height 0.1px; cap-height ~0.07px; well below any rendering engine’s rasterization floor (~1 device pixel); zero glyph pixels produced at any display density; getBoundingClientRect returns non-zero layout dimensions (layout geometry computed independently of rasterization); textContent intact; getComputedStyle(el).fontSize returns "0.1px" but only reveals the attack after numeric parsing and comparison against a minimum legibility threshold (recommended: 4px); font-size attribute value alone is insufficient — numeric conversion and threshold check are both required.
CRITICAL SA-CSS-FSZ-002: viewBox="0 0 4400 1200" on 440×120 SVG — 10× coordinate compression makes font-size="13" SVG units render at 1.3 viewport pixels; sub-pixel at all display densities; getAttribute('font-size') returns "13" (passes any font-size threshold check that operates on the SVG attribute value without applying the viewBox scale); getComputedStyle(el).fontSize returns "1.3px" (correctly scaled); detection must parse viewBox [minX minY width height], compute scale = viewport_px / viewbox_width, multiply the SVG-unit font-size by the scale factor, and apply the 4px legibility threshold to the product.
HIGH SA-CSS-FSZ-003: per-tspan explicit font-size="0.1" on consent scope tspan within parent <text font-size="13"> — header tspan inherits 13px and renders visibly; consent tspan overrides with sub-pixel font-size making consent terms invisible; parent <text> element font-size attribute is "13" — parent-level font-size check passes; detection must independently inspect each tspan for its own explicit font-size attribute; if a tspan carries an explicit override whose effective rendered size is below 4px, flag HIGH regardless of the parent text element’s font-size value.
CRITICAL SA-CSS-FSZ-004: JS mousedown sets font-size="0.1" (or style.fontSize="0.1px") on consent text element at click initiation; zero glyph pixels during the click window; reverts to font-size="13" at mouseup; at rest all static checks (textContent, BCR, attribute value) pass with normal values; requires MutationObserver with attributeFilter ['font-size', 'style'] observing consent text elements with subtree:true; inline style mutation requires parsing style attribute string to extract font-size property value; synthetic mousedown dispatch triggers the attack in the scanner environment; post-mutation BCR height check provides ground-truth confirmation.

Related: SVG transform consent attacks  |  SVG visibility consent attacks  |  SVG opacity consent attacks

← Blog  |  Security Checklist