Security Guide

MCP server SVG stroke-dasharray consent security — stroke-dasharray=0 1e9 produces zero-length dashes making stroke-only text completely invisible, dasharray=1 100 renders one-pixel fragments per glyph destroying readability, per-tspan scope with normal header, and mousedown zero-dash injection

When SVG consent text is rendered using the stroke channel only — fill="none" with a dark stroke color — the stroke-dasharray presentation attribute becomes the sole determinant of whether any ink is produced. stroke-dasharray specifies alternating dash and gap lengths along the stroked path of each glyph outline. With stroke-dasharray="0 1e9", the dash length is zero and the gap length is astronomically large: the rendering engine advances through the entire stroke path finding zero pixels of dash before a 10&sup9;px gap — no ink is ever painted. Despite this, the element’s bounding box is fully valid (layout depends on text geometry, not stroke rendering), textContent is intact, and the stroke color attribute returns the dark value. A scanner checking fill finds "none" (appropriate for stroke-only text) and switches to checking stroke, finding a dark color and concluding the text is visible — but it fails to check the dasharray that suppresses all stroke painting. Four attack patterns: zero-dash full invisible, extreme-gap near-invisible fragments, per-tspan scope with normal header, and mousedown injection.

Attack 1: stroke-dasharray=0 1e9 on stroke-only text — zero visible strokes (SA-CSS-SDA-001)

SVG stroke rendering for text paints the glyph outline paths as dashed (or solid, if no dasharray is set) strokes of specified width and color. The stroke is the outer boundary path of each glyph’s shape. When fill="none", only this stroke channel produces ink — there is no fill to fall back on. stroke-dasharray="0 1e9" sets the first value (dash length) to 0 and the second (gap length) to 1×10&sup9; SVG user units. The rendering algorithm walks along the stroke path, starting in “dash mode”. The first dash segment has length 0 — it is painted instantaneously (zero pixels). Then the algorithm enters “gap mode” for 10&sup9; units. Since the entire glyph outline for a 13px font character occupies perhaps 40-80 SVG units of total path length, the path is entirely consumed within the first gap segment. No ink is painted. The next dash would start at path position 10&sup9; — never reached for any glyph in any reasonably sized text.

The attack exploits the specific vulnerability of stroke-only text rendering. For text rendered with both fill and stroke, the fill channel would still produce ink even if the stroke were dashed away. But fill="none" text relies entirely on the stroke. The attacker sets up this dependency by using fill="none" in the text element definition — a configuration that might appear intentional for a particular visual style (outline text) — and then applies the zero-dash dasharray to suppress the stroke. A scanner checking for consent text rendering must verify not only that a stroke color is set and that fill might be none, but also that the stroke-dasharray attribute does not suppress the stroke. The detection check: on any text element with fill=none (or fill-opacity=0), parse the stroke-dasharray value; if the first dash value is 0 or the gap dominates the entire glyph path length, flag as CRITICAL.

<!-- SA-CSS-SDA-001: fill=none, stroke=#111, stroke-dasharray=0 1e9 — zero visible ink -->
<!-- Stroke channel is sole ink source; zero-length dashes = no ink painted -->
<!-- BCR valid; textContent intact; stroke color dark; only dasharray reveals attack -->
<svg width="440" height="120" xmlns="http://www.w3.org/2000/svg">
  <rect width="440" height="120" fill="white"/>

  <!-- fill=none: stroke is sole rendering channel -->
  <!-- stroke-dasharray=0 1e9: dash=0 (zero ink), gap=1e9 (consumes entire path) -->
  <!-- getComputedStyle(el).stroke = "rgb(17,17,17)" (dark — stroke check PASSES!) -->
  <!-- getComputedStyle(el).strokeDasharray = "0 1000000000" -->
  <text x="20" y="40" font-size="13"
        fill="none" stroke="#111" stroke-width="0.5"
        stroke-dasharray="0 1e9">
    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: for fill=none text, check stroke-dasharray first dash value
function checkZeroDashStrokeOnly(svgEl) {
  const findings = [];

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

    // Determine effective fill: none or zero opacity
    const fillVal    = cs.fill || el.getAttribute('fill') || '';
    const fillOpacity = parseFloat(cs.fillOpacity || el.getAttribute('fill-opacity') || '1');
    const fillIsNone = fillVal === 'none' || fillOpacity < 0.05;
    if (!fillIsNone) continue; // fill-based text: dasharray doesn't hide text

    // Stroke channel is sole ink source: check stroke-dasharray
    const dashAttr = el.getAttribute('stroke-dasharray') || cs.strokeDasharray || '';
    if (!dashAttr || dashAttr === 'none') continue;

    const dashes = dashAttr.trim().split(/[\s,]+/).map(parseFloat).filter(n => !isNaN(n));
    if (dashes.length === 0) continue;

    const firstDash = dashes[0]; // first value is a dash length
    if (firstDash === 0) {
      const bcr = el.getBoundingClientRect();
      findings.push({
        severity: 'CRITICAL', code: 'SA-CSS-SDA-001',
        note: `<${el.tagName}> fill=none (stroke-only text); ` +
              `stroke-dasharray="${dashAttr}"; ` +
              `first dash length=0: zero-length dashes = no ink painted on any glyph outline; ` +
              `stroke="${el.getAttribute('stroke') || cs.stroke}" (dark — stroke check PASSES!); ` +
              `BCR: width=${bcr.width.toFixed(0)} height=${bcr.height.toFixed(0)} (non-zero); ` +
              `textContent="${el.textContent.trim().slice(0,30)}..." intact; ` +
              `stroke-dasharray must be checked when fill=none (stroke-only text)`
      });
    }
  }
  return findings;
}

CRITICAL — SA-CSS-SDA-001: fill="none" consent text relies solely on the stroke channel for rendering. stroke-dasharray="0 1e9" sets a zero-length dash: no ink is painted on any glyph outline. getComputedStyle(el).stroke returns a dark color (passes stroke check); BCR is non-zero; textContent is intact. Detection requires checking stroke-dasharray when fill is "none" — if the first dash length is 0, no ink is produced.

Attack 2: stroke-dasharray=1 100 — one-pixel ink fragments, consent terms unreadable (SA-CSS-SDA-002)

The fragment attack uses a dasharray with a very small dash length and a large gap, producing sparse isolated ink pixels rather than complete glyph outlines. stroke-dasharray="1 100" paints 1px of ink, then skips 100px, then paints 1px again, repeating along every glyph stroke path. For a 13px font character with a glyph outline of approximately 40-80px total path length, the entire outline is rendered as one or two 1px ink fragments separated by 100px gaps. Each fragment is a single pixel — insufficient to show any letterform. The entire consent text string becomes a scattered pattern of isolated dots; no character shape, no word, no permission term is legible.

This attack differs from the zero-dash form in that ink is technically present. A scanner using a canvas-render approach to check for ink presence in the consent region would find ink (the isolated pixel fragments). A scanner checking whether the rendered text has any contrast against the background would find contrast (the dark pixel fragments are dark on white). What neither check reveals is whether the ink fragments are arranged into legible characters. The legibility test for dasharray-fragmented stroke text requires checking the ratio of dash length to glyph-path density. For 13px text, any dasharray where the dash-to-period ratio (dash / (dash + gap)) is less than approximately 5% means fewer than 5% of each glyph outline is painted — insufficient for character recognition. Detection threshold: dashes[0] / (dashes[0] + dashes[1]) < 0.05 with fill=none text flags fragment attacks.

<!-- SA-CSS-SDA-002: fill=none, stroke-dasharray=1 100 — sparse 1px fragments -->
<!-- Ink present (1px dashes) but no glyph shapes visible; permission terms unreadable -->
<svg width="440" height="120" xmlns="http://www.w3.org/2000/svg">
  <rect width="440" height="120" fill="white"/>

  <!-- stroke-dasharray=1 100: 1px ink, 100px gap; ~1% of glyph outline painted -->
  <!-- isolated dots at random positions on glyph paths; no letterforms recognizable -->
  <text x="20" y="40" font-size="13"
        fill="none" stroke="#111" stroke-width="0.5"
        stroke-dasharray="1 100">
    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: for fill=none text, check dash-to-period ratio; flag if < 5% ink coverage
function checkFragmentedDash(svgEl) {
  const findings = [];
  const MIN_INK_RATIO = 0.05; // less than 5% of glyph outline painted = fragments only

  for (const el of svgEl.querySelectorAll('text, tspan')) {
    const cs = getComputedStyle(el);
    const fillVal     = cs.fill || el.getAttribute('fill') || '';
    const fillOpacity = parseFloat(cs.fillOpacity || el.getAttribute('fill-opacity') || '1');
    const fillIsNone  = fillVal === 'none' || fillOpacity < 0.05;
    if (!fillIsNone) continue;

    const dashAttr = el.getAttribute('stroke-dasharray') || cs.strokeDasharray || '';
    if (!dashAttr || dashAttr === 'none') continue;

    const dashes = dashAttr.trim().split(/[\s,]+/).map(parseFloat).filter(n => !isNaN(n));
    if (dashes.length < 2) continue; // solid stroke (no gap) or single value (unusual)

    const dashLen = dashes[0];
    const gapLen  = dashes[1];
    if (dashLen <= 0) continue; // caught by SDA-001

    const inkRatio = dashLen / (dashLen + gapLen);
    if (inkRatio < MIN_INK_RATIO) {
      findings.push({
        severity: 'CRITICAL', code: 'SA-CSS-SDA-002',
        note: `<${el.tagName}> fill=none (stroke-only); ` +
              `stroke-dasharray="${dashAttr}"; ` +
              `dash=${dashLen}px gap=${gapLen}px; ink coverage ratio=${(inkRatio*100).toFixed(1)}% (threshold ${MIN_INK_RATIO*100}%); ` +
              `only ~${(inkRatio*100).toFixed(1)}% of each glyph outline painted as isolated ${dashLen}px fragments; ` +
              `no letterform shapes visible; permission terms unreadable; ` +
              `ink-presence check passes (fragments are dark on white); ` +
              `ink-ratio threshold required for dasharray fragment detection`
      });
    }
  }
  return findings;
}

CRITICAL — SA-CSS-SDA-002: stroke-dasharray="1 100" on fill=none text paints only 1% of each glyph outline as isolated 1px fragments. Ink is technically present; BCR is non-zero; stroke color is dark. No letterforms are visible. Detection requires computing the ink coverage ratio (dash / (dash + gap)) for fill=none text and flagging ratios below 5% as insufficient for character recognition.

Attack 3: per-tspan scope stroke-dasharray=0 1e9, header tspan normal (SA-CSS-SDA-003)

The per-tspan scope attack applies the zero-dash pattern to the consent scope tspan while the header tspan has no dasharray and renders normally. The parent text element uses fill="none" and stroke="#111" (stroke-only rendering). The header tspan inherits the stroke rendering and produces normal solid outlines. The consent scope tspan has stroke-dasharray="0 1e9", suppressing all stroke ink for that tspan. The user sees the header text rendered as stroke outlines; the consent scope area is blank.

The per-tspan scope form exploits two detection gaps simultaneously: the gap in per-element scope analysis (checking parent text but not tspan children for dasharray) and the fill=none rendering gap (checking fill color and missing the stroke-only dependency). A scanner checking only the parent text element for stroke-dasharray finds no dasharray attribute (the parent has none) and passes. A scanner checking each tspan for dasharray but not filtering by fill=none would still detect this. The correct detection: on any text element with fill=none, recursively check all tspan descendants for non-trivial stroke-dasharray patterns. A scope tspan with a zero-dash or fragment dasharray while sibling tspans have normal (solid) strokes is a per-scope dasharray hiding attack.

<!-- SA-CSS-SDA-003: per-tspan scope — header tspan normal stroke; scope tspan dasharray=0 1e9 -->
<!-- Parent text: fill=none, stroke=#111. Header tspan: normal (no dasharray). -->
<!-- Scope tspan: stroke-dasharray=0 1e9 (zero ink). Per-tspan check required. -->
<svg width="440" height="140" xmlns="http://www.w3.org/2000/svg">
  <rect width="440" height="140" fill="white"/>

  <!-- parent: fill=none, stroke=#111 — stroke-only rendering -->
  <text font-size="13" fill="none" stroke="#111" stroke-width="0.5">
    <!-- header tspan: inherits stroke rendering; no dasharray; normal stroke outlines -->
    <tspan x="20" y="30">Permission Request:</tspan>
    <!-- scope tspan: stroke-dasharray=0 1e9; zero ink; per-tspan check required -->
    <!-- parent text has no dasharray attr; parent-level check passes -->
    <tspan x="20" y="52" stroke-dasharray="0 1e9">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 dasharray check for fill=none text; parent dasharray absence doesn't exempt tspans
function checkPerTspanDasharray(svgEl) {
  const findings = [];

  for (const textEl of svgEl.querySelectorAll('text')) {
    const cs = getComputedStyle(textEl);
    const fillVal  = cs.fill || textEl.getAttribute('fill') || '';
    const fillIsNone = fillVal === 'none' || parseFloat(cs.fillOpacity) < 0.05;
    if (!fillIsNone) continue;

    for (const tspan of textEl.querySelectorAll('tspan')) {
      const tspanDashAttr = tspan.getAttribute('stroke-dasharray') || '';
      if (!tspanDashAttr) continue; // no dasharray on this tspan

      const dashes = tspanDashAttr.trim().split(/[\s,]+/).map(parseFloat).filter(n => !isNaN(n));
      if (dashes.length === 0) continue;

      const firstDash = dashes[0];
      const gapLen    = dashes[1] ?? 0;
      const inkRatio  = gapLen > 0 ? firstDash / (firstDash + gapLen) : 1;

      if (firstDash === 0 || inkRatio < 0.05) {
        findings.push({
          severity: 'HIGH', code: 'SA-CSS-SDA-003',
          note: `tspan stroke-dasharray="${tspanDashAttr}" on fill=none text; ` +
                `dash=${firstDash} gap=${gapLen ?? 'n/a'}; ink ratio=${(inkRatio*100).toFixed(1)}%; ` +
                `parent <text> has no stroke-dasharray attr (parent check passes); ` +
                `per-tspan scope: header tspan normal stroke visible; scope tspan zero/fragment ink; ` +
                `per-tspan dasharray check on fill=none parent text required; ` +
                `tspan textContent="${tspan.textContent.trim().slice(0,30)}..."`
        });
      }
    }
  }
  return findings;
}

HIGH — SA-CSS-SDA-003: Per-tspan stroke-dasharray="0 1e9" on the consent scope tspan within a fill=none stroke-only text element. The header tspan inherits normal stroke rendering (visible); the scope tspan has zero-dash suppression. The parent text element has no stroke-dasharray attribute; a parent-level check passes. Per-tspan stroke-dasharray inspection is required for fill=none parent text elements.

Attack 4: JS mousedown injects stroke-dasharray=0 1e9, reverts at mouseup (SA-CSS-SDA-004)

The transient injection form of the stroke-dasharray attack sets stroke-dasharray="0 1e9" on the stroke-only consent text element at mousedown and removes it at mouseup. At rest, the element has no stroke-dasharray attribute and renders with solid stroke outlines (for stroke-only text). When the user initiates a click, the mousedown event fires first (capture phase), injecting the zero-dash dasharray. All stroke ink is suppressed: the text is invisible. At mouseup, the attribute is removed and the text reverts to solid stroke rendering. The user sees the text before pressing and after releasing; during the press, the text disappears.

The SVG stroke-dasharray property exists both as a presentation attribute (el.setAttribute('stroke-dasharray', '0 1e9')) and as a CSS property accessible via el.style.strokeDasharray. A MutationObserver watching the stroke-dasharray attribute will catch the presentation attribute form. A MutationObserver watching the style attribute will catch the CSS property form (but must parse the style string to identify the stroke-dasharray property). The observer must be set up on the stroke-only consent text elements specifically; it can also be set up on parent group elements with subtree: true to catch injections on ancestor elements that would cascade to text descendants. The detection also requires verifying that the affected text element uses fill=none (stroke-only rendering), since stroke-dasharray injection on fill-based text would only affect the decorative stroke outline and would not hide the fill-based ink.

<!-- SA-CSS-SDA-004: fill=none stroke text; mousedown injects dasharray=0 1e9; reverts mouseup -->
<!-- At rest: no dasharray; solid stroke outlines visible. Click: zero-dash injected, invisible. -->
<svg id="consent-svg-sda" width="440" height="120" xmlns="http://www.w3.org/2000/svg">
  <rect width="440" height="120" fill="white"/>

  <!-- fill=none: stroke-only; at rest no dasharray; solid glyph outlines visible -->
  <text id="scope-text-sda" x="20" y="40" font-size="13"
        fill="none" stroke="#111" stroke-width="0.5">
    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-sda');
  const text = document.getElementById('scope-text-sda');
  svg.addEventListener('mousedown', () => {
    text.setAttribute('stroke-dasharray', '0 1e9');
    // alternative CSS form: text.style.strokeDasharray = '0 1e9';
  }, true);
  svg.addEventListener('mouseup', () => {
    text.removeAttribute('stroke-dasharray');
    // alternative: text.style.strokeDasharray = '';
  }, true);
})();
</script>

// Detection: for fill=none text; MutationObserver on stroke-dasharray and style attrs
function watchDasharrayInject(svgEl) {
  const findings = [];

  const observer = new MutationObserver(mutations => {
    for (const mut of mutations) {
      const attr = mut.attributeName;
      if (attr !== 'stroke-dasharray' && attr !== 'style') continue;
      const el = mut.target;

      // Only relevant for stroke-only (fill=none) text elements
      const cs = getComputedStyle(el);
      const fillVal = cs.fill || el.getAttribute('fill') || '';
      const fillIsNone = fillVal === 'none' || parseFloat(cs.fillOpacity) < 0.05;
      if (!fillIsNone) continue;

      let dashVal = null;
      if (attr === 'stroke-dasharray') {
        dashVal = el.getAttribute('stroke-dasharray');
      } else {
        dashVal = el.style.strokeDasharray || null;
      }
      if (!dashVal || dashVal === 'none') continue;

      const dashes = dashVal.trim().split(/[\s,]+/).map(parseFloat).filter(n => !isNaN(n));
      if (dashes.length === 0) continue;

      const firstDash = dashes[0];
      const gapLen    = dashes[1] ?? 0;
      const inkRatio  = gapLen > 0 ? firstDash / (firstDash + gapLen) : 1;

      if (firstDash === 0 || inkRatio < 0.05) {
        findings.push({
          severity: 'CRITICAL', code: 'SA-CSS-SDA-004',
          note: `Dynamic stroke-dasharray="${dashVal}" on fill=none <${el.tagName}> ` +
                `during interaction; dash=${firstDash} ink ratio=${(inkRatio*100).toFixed(1)}%; ` +
                `stroke outlines suppressed during click window; removed at mouseup; ` +
                `at rest: no dasharray; solid strokes visible; static scan passes; ` +
                `MutationObserver on stroke-dasharray + style attrs; ` +
                `fill=none required for this to suppress all ink; ` +
                `textContent="${el.textContent?.trim().slice(0,30) ?? ''}..."`
        });
      }
    }
  });
  observer.observe(svgEl, {
    subtree: true, attributes: true,
    attributeFilter: ['stroke-dasharray', 'style']
  });
  svgEl.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
  svgEl.dispatchEvent(new MouseEvent('mouseup',   { bubbles: true, cancelable: true }));
  return findings;
}

CRITICAL — SA-CSS-SDA-004: JavaScript injects stroke-dasharray="0 1e9" at mousedown on a fill=none stroke-only text element, suppressing all stroke ink during the click window. At rest, no stroke-dasharray attribute exists and the text renders normally. MutationObserver must watch both the stroke-dasharray attribute and the style attribute (CSS strokeDasharray property) and must verify fill=none on the target element to confirm that stroke suppression hides the only ink channel.

Summary table

AttackMechanismWhat it bypassesSeverity
SA-CSS-SDA-001: stroke-dasharray=0 1e9 on fill=none text — zero ink painted fill=none: stroke is sole ink channel. dash=0, gap=1e9: entire glyph path in gap; zero strokes rendered. BCR valid; stroke color dark; dasharray is the only indicator textContent; BCR; stroke-color check (stroke still dark!); fill=none prerequisite; stroke-dasharray first-dash=0 check required Critical
SA-CSS-SDA-002: stroke-dasharray=1 100 — 1px ink fragments, no letterforms fill=none; dash=1px gap=100px; ~1% ink coverage; isolated 1px fragments at random glyph-path positions; ink present but no characters recognizable textContent; BCR; ink-presence check passes (fragments are dark); ink-ratio threshold: dash/(dash+gap) < 0.05 flags fragment attack Critical
SA-CSS-SDA-003: per-tspan scope dasharray=0 1e9; header tspan normal fill=none parent; header tspan solid stroke (visible); scope tspan stroke-dasharray=0 1e9 (zero ink); parent no dasharray attr; parent check passes textContent; parent-level dasharray check passes; per-tspan dasharray inspection required for fill=none parent text High
SA-CSS-SDA-004: mousedown injects dasharray=0 1e9; reverts mouseup fill=none; no dasharray at rest (solid strokes); mousedown injects zero-dash (all strokes suppressed); removed at mouseup; static scan passes Static DOM; requires MutationObserver on stroke-dasharray + style attrs + synthetic mousedown + fill=none check on target Critical

SkillAudit findings for this attack surface

CRITICAL SA-CSS-SDA-001: stroke-dasharray="0 1e9" on fill=none stroke-only consent text — stroke channel is the sole ink source for fill=none text; zero-length dash (first dasharray value=0) means no paint occurs for any point on any glyph outline path; entire path consumed in the 1e9-unit gap segment; getComputedStyle(el).stroke returns dark color (passes stroke check); BCR non-zero; textContent intact; detection requires checking stroke-dasharray on fill=none text elements and flagging first-dash-value=0 as zero-ink attack.
CRITICAL SA-CSS-SDA-002: stroke-dasharray="1 100" on fill=none text — ink coverage ratio 1/(1+100)=0.99% means only ~1% of each glyph outline is painted as isolated 1-pixel dots; no letterform shapes are visible; permission terms unreadable; ink-presence checks pass (dark dots on white background have measurable contrast); ink-coverage-ratio threshold required: dash/(dash+gap) < 5% flags fragment-only patterns that cannot convey consent text legibility.
HIGH SA-CSS-SDA-003: per-tspan scope stroke-dasharray="0 1e9" on consent scope tspan within a fill=none stroke-only parent text element — header tspan inherits normal stroke rendering (visible); scope tspan overrides with zero-dash dasharray (zero ink); parent text element has no stroke-dasharray attribute — parent-level dasharray check passes; per-tspan stroke-dasharray inspection is required for all tspan children of fill=none parent text elements.
CRITICAL SA-CSS-SDA-004: JS mousedown injects stroke-dasharray="0 1e9" on fill=none stroke-only consent text; all stroke outlines suppressed during click window; element invisible; removed at mouseup; at rest no stroke-dasharray attribute, solid strokes visible — all static checks pass; MutationObserver must watch stroke-dasharray attribute and style attribute (CSS strokeDasharray property) with synthetic mousedown simulation; must verify fill=none on target to confirm stroke suppression hides the only rendering channel.

Related: SVG stroke-opacity consent attacks  |  SVG fill-opacity consent attacks  |  SVG opacity consent attacks

← Blog  |  Security Checklist