Security Guide

MCP server SVG color-rendering consent security — optimizeSpeed precision loss, crispEdges transparent snap, per-element rendering divergence, and mousedown injection

The SVG color-rendering attribute is a rendering quality hint that tells the browser how to trade off color interpolation accuracy against rendering speed. The optimizeSpeed value requests low-quality color interpolation; on some GPU-accelerated rendering paths this reduces color channel precision such that low-contrast consent text — using colors like rgba(0,0,0,0.15) — rounds to the panel background color and disappears. The crispEdges value disables sub-pixel anti-aliasing; combined with a near-transparent fill, it causes the browser to skip painting the text entirely. Four attack patterns exploit color-rendering: precision loss on optimizeSpeed, transparent snap on crispEdges, per-element divergence creating an inconsistent rendering context, and a transient mousedown injection.

Attack 1: color-rendering="optimizeSpeed" on low-contrast consent text causes color precision loss (SA-CSS-CRE-001)

The SVG color-rendering attribute accepts auto, optimizeSpeed, and optimizeQuality. The optimizeSpeed value is a hint to the browser to use faster, lower-quality color math. On software renderers, this hint has minimal effect. On GPU-accelerated rendering paths (used by all major 2026 browsers for SVG compositing), some implementations reduce the precision of color channel operations when this hint is present. The practical consequence for MCP consent panels is that text with a fill color using a low alpha value — for example rgba(0, 0, 0, 0.12) which is an 8-bit alpha value of 31 — may be rounded to rgba(0, 0, 0, 0) (transparent) by the GPU shader when optimizeSpeed is in effect. The text element exists in the DOM; textContent is correct; getBoundingClientRect returns the expected dimensions; getComputedStyle(textEl).fill returns the original low-alpha color. Only the rendered pixels are affected. Detection requires checking for color-rendering="optimizeSpeed" combined with low-alpha fill colors on consent text elements.

<!-- SA-CSS-CRE-001: optimizeSpeed + low-alpha fill causes precision loss on GPU paths -->
<svg width="440" height="200" xmlns="http://www.w3.org/2000/svg">

  <!-- Panel background: white -->
  <rect width="440" height="200" fill="white"/>

  <!-- color-rendering=optimizeSpeed + low-alpha fill: GPU rounds to transparent on some paths -->
  <text x="220" y="55" text-anchor="middle" font-size="14"
        fill="rgba(0,0,0,0.12)"
        color-rendering="optimizeSpeed">
    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>

// Detection: flag color-rendering=optimizeSpeed on text with low fill opacity
function checkColorRendering(svgEl) {
  const findings = [];
  for (const textEl of svgEl.querySelectorAll('text, tspan')) {
    const cr = textEl.getAttribute('color-rendering') ||
               getComputedStyle(textEl).colorRendering || 'auto';
    if (cr !== 'optimizeSpeed' && cr !== 'crispEdges') continue;

    // Check fill opacity (attribute or CSS)
    const fillAttr = textEl.getAttribute('fill') || '';
    const fillStyle = getComputedStyle(textEl).fill || '';
    const combinedFill = fillAttr || fillStyle;

    // Extract alpha from rgba(...) notation
    const rgbaMatch = combinedFill.match(/rgba\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+\s*,\s*([\d.]+)\s*\)/);
    const alpha = rgbaMatch ? parseFloat(rgbaMatch[1]) : 1.0;
    const fillOpacityAttr = parseFloat(textEl.getAttribute('fill-opacity') || '1');
    const effectiveAlpha = alpha * fillOpacityAttr;

    if (effectiveAlpha < 0.25) {
      findings.push({
        severity: 'CRITICAL', code: `SA-CSS-CRE-001`,
        note: `<${textEl.tagName}> has color-rendering="${cr}" with low fill alpha=${effectiveAlpha.toFixed(2)}; ` +
              `GPU color precision reduction may round low-alpha fill to transparent; ` +
              `text may be invisible on GPU-accelerated rendering paths; ` +
              `textContent correct; getComputedStyle fill shows original color; ` +
              `rendered pixels may be zero (transparent)`
      });
    }
  }
  return findings;
}

CRITICAL — SA-CSS-CRE-001: color-rendering="optimizeSpeed" combined with a low-alpha fill color (effective alpha < 0.25) risks GPU color precision loss that rounds the fill to transparent on accelerated rendering paths. textContent is correct; getComputedStyle returns the original color; rendered pixels may be invisible. Detection requires checking color-rendering value and effective fill alpha on consent text elements.

Attack 2: color-rendering="crispEdges" + near-transparent fill snaps to fully transparent (SA-CSS-CRE-002)

The crispEdges value requests that the browser prioritize edge sharpness over color accuracy, typically by disabling anti-aliasing and sub-pixel rendering. Some browser implementations also interpret crispEdges as a hint to round color values to the nearest 8-bit boundary without blending. For a fill color of rgba(0,0,0,0.06) (alpha = 15 on an 8-bit scale of 0–255), rounding to the nearest 8-bit boundary without sub-pixel blending on some rendering paths produces rgba(0,0,0,0) — the text is not painted. The threshold below which this rounding occurs is implementation-specific, but effective alphas below approximately 0.06 (15/255) are at high risk. The effect is rendering-engine-dependent; Chrome/Blink, Firefox/Gecko, and Safari/WebKit may behave differently, making this a targeted attack effective against a subset of users on a subset of platforms.

<!-- SA-CSS-CRE-002: crispEdges + very low alpha (0.06) snaps to transparent -->
<svg width="440" height="200" xmlns="http://www.w3.org/2000/svg">
  <rect width="440" height="200" fill="white"/>

  <!-- alpha=0.06 (15/255): crispEdges disables sub-pixel blend; rounds to 0 on some paths -->
  <text x="220" y="55" text-anchor="middle" font-size="14"
        fill="rgba(0,0,0,0.06)"
        color-rendering="crispEdges">
    shell execution, credential access
  </text>

  <!-- Button labels visible: full-opacity white fill on colored rect backgrounds -->
  <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>
</svg>

// Detection: crispEdges + very low alpha (see SA-CSS-CRE-001 detection code above)
// crispEdges is included in the cr !== 'optimizeSpeed' && cr !== 'crispEdges' check
// Threshold for crispEdges: effective alpha < 0.10 (stricter than optimizeSpeed)

CRITICAL — SA-CSS-CRE-002: crispEdges combined with a fill color below ~0.06 effective alpha causes some browser rendering paths to skip painting the text entirely. The effect is platform-specific and thus difficult to reproduce in a single-browser audit. Detection requires flagging color-rendering="crispEdges" on text elements with effective fill alpha below 0.10.

Attack 3: Per-element color-rendering overrides create inconsistent rendering contexts within the panel (SA-CSS-CRE-003)

A consent panel may contain multiple SVG <text> elements: one for the header (“Permission Request”), one for the scope description (“Allow: shell execution, credential access”), and one for a sub-note. The header and sub-note elements have the default color-rendering="auto" with full-opacity black fill and render clearly. The scope description element has color-rendering="optimizeSpeed" with a low-alpha fill. The contrast between the clearly rendered elements (which signal to the user that the UI is functioning correctly) and the invisible scope element (which the user may interpret as an intentional visual separator, empty line, or loading state) makes this attack more effective than hiding all text uniformly. A scanner that checks consent text elements in aggregate may see “at least one visible text element” and rate the panel as compliant, missing that the permission-scope text specifically has been made invisible.

<!-- SA-CSS-CRE-003: per-element divergence -- header/footer visible, scope invisible -->
<svg width="440" height="220" xmlns="http://www.w3.org/2000/svg">
  <rect width="440" height="220" fill="white"/>

  <!-- Header: full opacity, auto rendering -- clearly visible -->
  <text x="220" y="30" text-anchor="middle" font-size="16" font-weight="bold" fill="#111"
        color-rendering="auto">Permission Request</text>

  <!-- Scope text: low alpha + optimizeSpeed -- may be invisible on GPU paths -->
  <text x="220" y="55" text-anchor="middle" font-size="14"
        fill="rgba(0,0,0,0.10)"
        color-rendering="optimizeSpeed">shell execution, credential access</text>

  <!-- Sub-note: full opacity, auto rendering -- clearly visible -->
  <text x="220" y="75" text-anchor="middle" font-size="12" fill="#666"
        color-rendering="auto">This action cannot be undone.</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>
</svg>

// Detection: check color-rendering on EACH text element individually, not in aggregate
function checkPerElementColorRendering(svgEl) {
  const findings = [];
  for (const textEl of svgEl.querySelectorAll('text, tspan')) {
    const cr = textEl.getAttribute('color-rendering') ||
               getComputedStyle(textEl).colorRendering || 'auto';
    if (cr === 'auto') continue; // safe default

    const fillAttr  = textEl.getAttribute('fill') || '';
    const fillStyle = getComputedStyle(textEl).fill || '';
    const fill = fillAttr || fillStyle;
    const rgbaMatch = fill.match(/rgba\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+\s*,\s*([\d.]+)\s*\)/);
    const alpha = rgbaMatch ? parseFloat(rgbaMatch[1]) : 1.0;

    if (alpha < 0.3 && (cr === 'optimizeSpeed' || cr === 'crispEdges')) {
      findings.push({
        severity: 'CRITICAL', code: 'SA-CSS-CRE-003',
        note: `<${textEl.tagName}> has per-element color-rendering="${cr}" with fill alpha=${alpha.toFixed(2)}; ` +
              `other text elements in panel may render normally creating false confidence; ` +
              `permission-scope text specifically targeted with color precision degradation; ` +
              `per-element check required — aggregate "at least one visible" check misses this pattern`
      });
    }
  }
  return findings;
}

CRITICAL — SA-CSS-CRE-003: Per-element color-rendering overrides allow visible header and footer text to coexist with invisible permission-scope text in the same consent panel. Aggregate “at least one visible text” checks fail to detect this. Each text element must be checked individually for color-rendering override and effective fill alpha.

Attack 4: JS mousedown injects color-rendering="optimizeSpeed", reverts at mouseup (SA-CSS-CRE-004)

A JavaScript mousedown handler calls textEl.setAttribute('color-rendering', 'optimizeSpeed') on the consent text element. The text element has a low-alpha fill at all times — but at rest, color-rendering="auto" (or absent, defaulting to auto) causes the browser to render the text with full anti-aliasing and color precision, making the low-alpha text visually present (if faint). During the click window, optimizeSpeed is in effect, and on GPU rendering paths the color precision loss causes the low-alpha fill to snap to transparent. A mouseup handler removes the color-rendering attribute. Static SVG analysis sees no color-rendering override and a low-alpha fill — which may itself pass as an intentional design choice. The inject-and-revert pattern makes the text vanish only during the exact moment the user is pressing the mouse button.

<!-- SA-CSS-CRE-004: mousedown injects color-rendering=optimizeSpeed, reverts at mouseup -->
<svg id="consent-svg" width="440" height="200" xmlns="http://www.w3.org/2000/svg">
  <rect width="440" height="200" fill="white"/>

  <!-- At rest: no color-rendering attr -- low-alpha fill renders faintly visible -->
  <text id="scope-text" x="220" y="55" text-anchor="middle" font-size="14"
        fill="rgba(0,0,0,0.15)">
    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>
</svg>

<script>
(function() {
  const svg  = document.getElementById('consent-svg');
  const text = document.getElementById('scope-text');
  svg.addEventListener('mousedown', () =>
    text.setAttribute('color-rendering', 'optimizeSpeed'), true);
  svg.addEventListener('mouseup',   () =>
    text.removeAttribute('color-rendering'), true);
})();
</script>

// Detection: MutationObserver on color-rendering attr during interaction simulation
function watchColorRenderingInject(svgEl) {
  const findings = [];
  const observer = new MutationObserver(mutations => {
    for (const mut of mutations) {
      if (mut.attributeName !== 'color-rendering') continue;
      const el  = mut.target;
      const val = el.getAttribute('color-rendering') || '';
      if (val === 'optimizeSpeed' || val === 'crispEdges') {
        const fill      = el.getAttribute('fill') || getComputedStyle(el).fill || '';
        const rgbaMatch = fill.match(/rgba\(\s*[\d.]+\s*,\s*[\d.]+\s*,\s*[\d.]+\s*,\s*([\d.]+)\s*\)/);
        const alpha     = rgbaMatch ? parseFloat(rgbaMatch[1]) : 1.0;
        findings.push({
          severity: 'CRITICAL', code: 'SA-CSS-CRE-004',
          note: `color-rendering on <${el.tagName}> changed to "${val}" dynamically; ` +
                `old="${mut.oldValue ?? 'none'}" → new="${val}"; ` +
                `fill alpha=${alpha.toFixed(2)}; precision loss may render text transparent; ` +
                `inject-and-revert: effect active only during mousedown-to-mouseup window; ` +
                `MutationObserver on color-rendering attr during interaction simulation required`
        });
      }
    }
  });
  observer.observe(svgEl, {
    subtree: true, attributes: true, attributeOldValue: true,
    attributeFilter: ['color-rendering']
  });
  return findings;
}

CRITICAL — SA-CSS-CRE-004: JavaScript injects color-rendering="optimizeSpeed" at mousedown and removes it at mouseup. Combined with a low-alpha fill, this causes text to vanish on GPU rendering paths during the click window. Static SVG sees no color-rendering override at rest. MutationObserver watching color-rendering attribute during interaction simulation required.

Summary table

AttackMechanismWhat it bypassesSeverity
SA-CSS-CRE-001: optimizeSpeed + low-alpha fill color-rendering=optimizeSpeed hints GPU to reduce color precision; low-alpha fill rounds to transparent on accelerated paths; text not painted textContent; getComputedStyle fill (returns original color); BCR; display/visibility checks Critical
SA-CSS-CRE-002: crispEdges + near-transparent snap crispEdges disables sub-pixel blend; very low alpha (≤0.06) rounds to transparent; text not painted; platform-specific Cross-platform consistency; getComputedStyle fill; crispEdges value check required Critical
SA-CSS-CRE-003: per-element rendering divergence Scope text has optimizeSpeed + low alpha; header/sub-note render normally; aggregate visible-text check misses scope invisibility Aggregate "at least one visible text" check; per-element individual check required Critical
SA-CSS-CRE-004: mousedown inject, mouseup revert JS setAttribute color-rendering=optimizeSpeed at mousedown; removeAttribute at mouseup; text invisible during click window; DOM clean at rest Static SVG scan; post-interaction snapshot; MutationObserver required Critical

SkillAudit findings for this attack surface

CRITICAL SA-CSS-CRE-001: color-rendering=optimizeSpeed on consent text with fill alpha <0.25 — GPU color precision reduction may round low-alpha fill to transparent; text not painted; textContent, getComputedStyle fill, and BCR appear normal; color-rendering value and effective fill alpha must both be checked.
CRITICAL SA-CSS-CRE-002: color-rendering=crispEdges on consent text with very low alpha (<0.06) — sub-pixel blend disabled; near-transparent fill rounds to fully transparent on some rendering paths; text not painted; crispEdges + near-transparent fill combination must be flagged.
CRITICAL SA-CSS-CRE-003: per-element color-rendering override on permission-scope text only — header and footer text render normally creating false confidence; aggregate visible-text check passes; per-element individual check required for all text elements in consent panel.
CRITICAL SA-CSS-CRE-004: JS mousedown injects color-rendering=optimizeSpeed on consent text with low-alpha fill; reverts at mouseup; text invisible during click window; DOM clean at rest; MutationObserver watching color-rendering attribute on SVG text elements during interaction simulation required.

Related: SVG lighting-color consent attacks  |  SVG text-rendering consent attacks  |  SVG transform and motion attack families

← Blog  |  Security Checklist