Security Guide

MCP server SVG stroke-width consent security — stroke-width=0 on fill=none text produces no ink while stroke color check passes, stroke-width=0.001 is sub-pixel invisible after anti-aliasing, per-tspan scope, and mousedown zero-width injection

When SVG consent text is rendered using the stroke channel only — fill="none" with a dark stroke color — the stroke-width presentation attribute becomes the final arbiter of whether any ink reaches the screen. Setting stroke-width="0" reduces the stroke path to zero physical width: the rendering engine traces each glyph outline but paints no pixels because the stroke has no extent. This attack is categorically distinct from stroke="none": the stroke color attribute remains set to a dark value and passes any color-based check, but the zero width means that color is never applied to any pixel. getComputedStyle(el).stroke returns a dark color; getBoundingClientRect returns non-zero dimensions (layout depends on the text geometry, not stroke painting); textContent is intact. A scanner that checks fill (finding "none"), then checks stroke color (finding dark), concludes the text is visible — and entirely misses the stroke-width=0 that suppresses all ink. A sub-pixel variant at stroke-width="0.001" evades even basic non-zero width checks; only a threshold comparison at 0.25px catches it. Four attack patterns: zero-width full invisible, sub-pixel near-zero, per-tspan scope, and mousedown injection.

Attack 1: stroke-width=0 on fill=none text — zero ink despite dark stroke color (SA-CSS-SWI-001)

SVG text rendering proceeds through two independent channels: the fill channel (painting the interior of each glyph shape) and the stroke channel (painting the outline of each glyph shape). When fill="none" is set, the fill channel is fully disabled and the stroke channel becomes the sole ink source. The stroke channel requires three properties to produce ink: a non-none stroke color, a non-zero stroke width, and a stroke opacity above zero. Setting any one of these to its suppressing value eliminates all ink. stroke-width="0" sets the width to zero, which collapses the stroke to a mathematical line of zero extent — no pixels are covered and no ink is deposited.

The critical deception in this attack is that stroke-width=0 does not touch the stroke color. getComputedStyle(el).stroke continues to return the dark color set on the element — commonly rgb(17, 17, 17) or similar. A stroke-color-based detection pass checks this value, finds a dark color, and determines that the stroke channel is active and the text is visible. The check passes. This is not an edge case: the stroke color and stroke width are independent CSS properties with independent computed values. Only by reading both properties and understanding that both must be non-trivial for ink to be produced can a scanner correctly assess stroke-only text visibility. The detection algorithm: for any text element where fill is "none" or fill-opacity is negligible, read getComputedStyle(el).strokeWidth (or parse the stroke-width attribute), convert to a float, and flag as CRITICAL if the value is exactly 0.

<!-- SA-CSS-SWI-001: fill=none, stroke=#111, stroke-width=0 — zero visible ink -->
<!-- Stroke color dark (check PASSES); zero width means no pixels are painted -->
<!-- BCR non-zero; textContent intact; only stroke-width 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-width=0: zero-extent stroke; no pixels painted regardless of stroke color -->
  <!-- getComputedStyle(el).stroke = "rgb(17,17,17)" — stroke color check PASSES! -->
  <!-- getComputedStyle(el).strokeWidth = "0px" -->
  <text x="20" y="40" font-size="13"
        fill="none" stroke="#111" stroke-width="0">
    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-width; flag exactly 0 as CRITICAL
function checkZeroStrokeWidth(svgEl) {
  const findings = [];

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

    // Determine effective fill: none or negligible 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: stroke-width doesn't hide fill ink

    // Stroke channel is sole ink source: check stroke-width
    // getComputedStyle.strokeWidth returns a px string (e.g. "0px", "1px", "0.001px")
    const swComputed = cs.strokeWidth || '';
    const swAttr     = el.getAttribute('stroke-width') || '';
    const swStr      = swComputed || swAttr;
    const swPx       = parseFloat(swStr); // strips "px" suffix

    if (swPx === 0) {
      // Note: stroke color may still be dark — this is the deception
      const strokeColor = cs.stroke || el.getAttribute('stroke') || 'unset';
      const bcr = el.getBoundingClientRect();
      findings.push({
        severity: 'CRITICAL', code: 'SA-CSS-SWI-001',
        note: `<${el.tagName}> fill=none (stroke-only text); ` +
              `stroke-width=0: zero-extent stroke; no pixels painted on glyph outlines; ` +
              `stroke="${strokeColor}" (dark — stroke color check PASSES!); ` +
              `BCR: width=${bcr.width.toFixed(0)} height=${bcr.height.toFixed(0)} (non-zero); ` +
              `textContent="${el.textContent.trim().slice(0,30)}..." intact; ` +
              `stroke-width must be checked when fill=none — color alone is insufficient`
      });
    }
  }
  return findings;
}

CRITICAL — SA-CSS-SWI-001: fill="none" consent text relies solely on the stroke channel for rendering. stroke-width="0" sets the stroke to zero width — no pixels are painted regardless of the stroke color. getComputedStyle(el).stroke returns a dark color (stroke color check PASSES); BCR is non-zero; textContent is intact. Detection requires checking stroke-width (both attribute and getComputedStyle.strokeWidth) when fill is "none" — a value of 0 means zero ink, regardless of the stroke color.

Attack 2: stroke-width=0.001 near-zero sub-pixel on fill=none text (SA-CSS-SWI-002)

The sub-pixel stroke-width attack uses a value that is technically greater than zero but falls below the anti-aliasing threshold of the rendering engine, making the stroke effectively invisible on any physical display. At 96 dpi, the mapping from CSS pixels to physical device pixels is a 1:1 ratio on standard displays (device pixel ratio = 1) or a 2:1 ratio on HiDPI displays. For a stroke-width of 0.001px on a standard display, the stroke extent is 0.001 physical pixels — approximately 250 times smaller than a single pixel. The anti-aliasing algorithm that converts sub-pixel geometry into actual pixel coverage (alpha values) assigns an alpha contribution proportional to coverage. A 0.001px stroke covering a single pixel row contributes an alpha of approximately 0.001 / 1.0 = 0.1% per pixel — completely imperceptible against a white background and indistinguishable from the background at any zoom level a user would interact at.

The attack exploits the boundary between mathematical non-zero and perceptual zero. A naive detection check of parseFloat(strokeWidth) > 0 returns true for 0.001, concluding the stroke has a positive width and the text is visible. The check passes. The actual rendering threshold at which a stroke becomes visually perceptible depends on font size, device pixel ratio, and the specific rendering engine, but 0.25px is a well-supported lower bound: at 96 dpi on a standard display, 0.25px is approximately 0.24 physical pixels, which produces an average alpha of about 24% across the covered pixel — marginally perceptible at best, and only for sharp-eyed users in high-contrast conditions. For sub-pixel protection in consent text, a practical threshold of 0.25px covers the anti-aliasing shadow zone. Detection: parseFloat(getComputedStyle(el).strokeWidth) < 0.25 on fill=none text.

<!-- SA-CSS-SWI-002: fill=none, stroke=#111, stroke-width=0.001 — sub-pixel invisible -->
<!-- stroke-width > 0 (basic non-zero check PASSES); 0.001px < anti-aliasing threshold -->
<!-- At 96dpi: 0.001px = ~0.001 physical pixels; alpha contribution ~0.1%: imperceptible -->
<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-width=0.001: sub-pixel; anti-aliasing renders at ~0.1% alpha per pixel -->
  <!-- getComputedStyle(el).strokeWidth = "0.001px" -->
  <!-- parseFloat("0.001px") = 0.001 — basic > 0 check passes! -->
  <text x="20" y="40" font-size="13"
        fill="none" stroke="#111" stroke-width="0.001">
    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: threshold-based sub-pixel stroke-width check for fill=none text
function checkSubPixelStrokeWidth(svgEl) {
  const findings = [];
  // At 96dpi, 0.25px is approximately 0.24 physical pixels — imperceptible after anti-aliasing
  const STROKE_WIDTH_THRESHOLD_PX = 0.25;

  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;

    // getComputedStyle.strokeWidth returns resolved value with px unit (e.g. "0.001px")
    const swComputed = cs.strokeWidth || '';
    const swAttr     = el.getAttribute('stroke-width') || '';
    const swStr      = swComputed || swAttr;
    if (!swStr) continue;

    // Float parsing is required — string comparison would treat "0.001px" as non-zero
    const swPx = parseFloat(swStr);
    if (isNaN(swPx)) continue;
    if (swPx === 0) continue; // caught by SWI-001

    if (swPx > 0 && swPx < STROKE_WIDTH_THRESHOLD_PX) {
      findings.push({
        severity: 'CRITICAL', code: 'SA-CSS-SWI-002',
        note: `<${el.tagName}> fill=none (stroke-only text); ` +
              `stroke-width="${swStr}" (parsed: ${swPx}px); ` +
              `sub-pixel: ${swPx}px < threshold ${STROKE_WIDTH_THRESHOLD_PX}px; ` +
              `anti-aliasing alpha contribution ~${(swPx * 100).toFixed(2)}% per pixel: imperceptible; ` +
              `basic strokeWidth > 0 check passes (${swPx} > 0 = true); ` +
              `float parsing + threshold comparison required for sub-pixel detection; ` +
              `textContent="${el.textContent.trim().slice(0,30)}..." intact`
      });
    }
  }
  return findings;
}

CRITICAL — SA-CSS-SWI-002: stroke-width="0.001" on fill=none text sets a sub-pixel stroke that is invisible after anti-aliasing. The value is technically greater than zero: a basic strokeWidth > 0 check passes. At 96 dpi, 0.001px produces approximately 0.1% alpha coverage per pixel — completely imperceptible. Detection requires float-parsing getComputedStyle.strokeWidth and applying a threshold of 0.25px for fill=none text; values below this threshold must be flagged as invisibly thin.

Attack 3: per-tspan scope stroke-width=0, header tspan normal (SA-CSS-SWI-003)

The per-tspan scope attack applies stroke-width="0" exclusively to the consent scope tspan while the header tspan inherits the parent text element’s normal stroke width and renders visibly. The parent text element is configured as stroke-only with fill="none" stroke="#111" stroke-width="1". The header tspan inherits all three properties: fill=none (stroke-only), stroke=#111 (dark), and stroke-width=1 (visible). The consent scope tspan explicitly overrides only stroke-width="0". CSS and SVG presentation attribute inheritance means that a property set on a child element overrides the parent value for that element only — so the scope tspan has an effective stroke-width of 0 while the parent check reveals stroke-width=1.

A scanner that reads stroke-width from the parent text element finds the value 1 and concludes that all stroke-only text within the element has adequate stroke width. The parent-level check passes. Only by inspecting each tspan individually — reading its own computed strokeWidth, which accounts for the tspan’s own presentation attributes overriding the parent — does the zero-width override on the scope tspan become visible. The correct detection procedure: for any text element with fill=none, enumerate all tspan children; for each tspan, read getComputedStyle(tspan).strokeWidth (computed value incorporating the tspan’s own attributes) and apply the same zero-or-sub-pixel check. A tspan with stroke-width=0 while sibling tspans have normal stroke width indicates a scope-targeted hiding attack.

<!-- SA-CSS-SWI-003: per-tspan scope — header tspan normal; scope tspan stroke-width=0 -->
<!-- Parent text: fill=none, stroke=#111, stroke-width=1 — parent check PASSES -->
<!-- Scope tspan: stroke-width=0 override; zero ink on scope. 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-width=1 — stroke-only rendering -->
  <text font-size="13" fill="none" stroke="#111" stroke-width="1">
    <!-- header tspan: inherits parent stroke-width=1; normal stroke outlines visible -->
    <tspan x="20" y="30">Permission Request:</tspan>
    <!-- scope tspan: stroke-width=0 overrides parent value; zero ink -->
    <!-- parent text has stroke-width=1 (parent check passes!) -->
    <tspan x="20" y="52" stroke-width="0">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 stroke-width check; parent value does not represent tspan effective value
function checkPerTspanStrokeWidth(svgEl) {
  const findings = [];
  const STROKE_WIDTH_THRESHOLD_PX = 0.25;

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

    // Read parent text stroke-width for comparison
    const parentSwPx = parseFloat(textCs.strokeWidth || '1');

    for (const tspan of textEl.querySelectorAll('tspan')) {
      // Must read computed style on tspan itself — not parent
      const tspanCs = getComputedStyle(tspan);
      const tswStr  = tspanCs.strokeWidth || tspan.getAttribute('stroke-width') || '';
      if (!tswStr) continue;

      const tswPx = parseFloat(tswStr);
      if (isNaN(tswPx)) continue;

      if (tswPx < STROKE_WIDTH_THRESHOLD_PX) {
        findings.push({
          severity: 'HIGH', code: 'SA-CSS-SWI-003',
          note: `tspan stroke-width="${tswStr}" (${tswPx}px) on fill=none text; ` +
                `parent <text> stroke-width=${parentSwPx}px (parent check passes!); ` +
                `tspan overrides parent to zero/sub-pixel: zero ink on scope tspan; ` +
                `header tspan inherits parent stroke-width=${parentSwPx}px (visible); ` +
                `per-tspan getComputedStyle.strokeWidth check required; ` +
                `tspan textContent="${tspan.textContent.trim().slice(0,30)}..."`
        });
      }
    }
  }
  return findings;
}

HIGH — SA-CSS-SWI-003: Per-tspan stroke-width="0" on the consent scope tspan within a fill=none stroke-only text element. The header tspan inherits the parent stroke-width="1" (visible); the scope tspan overrides to zero (zero ink). The parent text element has stroke-width="1" — a parent-level check passes. Per-tspan getComputedStyle(tspan).strokeWidth inspection is required, as the computed value accounts for the tspan’s own attribute overriding the inherited parent value.

Attack 4: JS mousedown sets stroke-width=0 on fill=none text, reverts at mouseup (SA-CSS-SWI-004)

The transient injection attack exploits the click event lifecycle. JavaScript registers a mousedown listener (in the capture phase, so it fires before any other handlers) on the SVG container. When the user initiates a click anywhere in the consent dialog, the handler immediately sets stroke-width="0" on the fill=none consent text element. The entire stroke ink is suppressed within the same rendering frame as the click start: the text disappears. At mouseup, the attribute is removed (or set back to the original value), restoring the visible stroke rendering. From the user’s perspective, the text vanishes when they press the mouse button and reappears when they release it — during the click itself, the text is invisible.

The attack is especially effective because the click decision (which button to press) has already been made before mousedown fires. The user has visually confirmed the consent text before initiating the click, but during the click’s execution window — the moment their commitment is registered by the system — the consent terms are invisible. If the text had been altered between when the user read it and when the click registered, they would have no visual feedback. The detection approach uses a MutationObserver watching both the stroke-width presentation attribute and the style attribute (which can carry the CSS stroke-width property). The observer must be attached to the consent elements before any interaction; it should also watch parent group elements with subtree: true to catch injections on ancestor elements. Crucially, the observer must verify that the affected element has fill=none: a stroke-width change on fill-rendered text only affects the decorative stroke outline, not the fill ink that makes text visible.

<!-- SA-CSS-SWI-004: fill=none stroke text; mousedown sets stroke-width=0; reverts mouseup -->
<!-- At rest: stroke-width=1, glyph outlines visible. Click: stroke-width=0, text invisible. -->
<svg id="consent-svg-swi" width="440" height="120" xmlns="http://www.w3.org/2000/svg">
  <rect width="440" height="120" fill="white"/>

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

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

  const observer = new MutationObserver(mutations => {
    for (const mut of mutations) {
      const attr = mut.attributeName;
      if (attr !== 'stroke-width' && 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 swStr = null;
      if (attr === 'stroke-width') {
        swStr = el.getAttribute('stroke-width');
      } else {
        // style attribute: parse CSS stroke-width property
        swStr = el.style.strokeWidth || null;
      }
      if (swStr === null) continue;

      const swPx = parseFloat(swStr);
      if (isNaN(swPx)) continue;

      if (swPx < STROKE_WIDTH_THRESHOLD_PX) {
        findings.push({
          severity: 'CRITICAL', code: 'SA-CSS-SWI-004',
          note: `Dynamic stroke-width="${swStr}" (${swPx}px) on fill=none <${el.tagName}> ` +
                `during interaction; zero/sub-pixel: no ink on glyph outlines during click window; ` +
                `at rest: normal stroke-width produces visible glyph outlines; static scan passes; ` +
                `attribute changed via: ${attr}; ` +
                `fill=none confirmed: stroke suppression hides the only rendering channel; ` +
                `MutationObserver on stroke-width + style attrs required; ` +
                `textContent="${el.textContent?.trim().slice(0,30) ?? ''}..."`
        });
      }
    }
  });
  observer.observe(svgEl, {
    subtree: true, attributes: true,
    attributeFilter: ['stroke-width', 'style']
  });
  // Trigger interaction simulation to expose transient injection
  svgEl.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
  svgEl.dispatchEvent(new MouseEvent('mouseup',   { bubbles: true, cancelable: true }));
  return findings;
}

CRITICAL — SA-CSS-SWI-004: JavaScript sets stroke-width="0" at mousedown on a fill=none stroke-only consent text element, suppressing all ink during the click window. At rest, stroke-width="1" produces visible glyph outlines and all static checks pass. MutationObserver must watch both the stroke-width presentation attribute and the style attribute (CSS stroke-width property) and must verify fill=none on the target to confirm that stroke suppression eliminates the only ink channel.

Summary table

AttackMechanismWhat it bypassesSeverity
SA-CSS-SWI-001: stroke-width=0 on fill=none text — zero ink despite dark stroke color fill=none: stroke is sole ink channel. stroke-width=0: zero-extent stroke; no pixels painted. BCR valid; textContent intact; stroke color dark — color check passes textContent; BCR; stroke-color check (stroke color still dark!); fill=none prerequisite; stroke-width === 0 check required Critical
SA-CSS-SWI-002: stroke-width=0.001 sub-pixel — invisible after anti-aliasing fill=none; stroke-width=0.001px; at 96dpi: ~0.001 physical pixels; anti-aliasing alpha ~0.1% per pixel: imperceptible; basic strokeWidth > 0 check passes textContent; BCR; basic non-zero strokeWidth check passes (0.001 > 0 = true); float-parsed threshold (0.25px) required for sub-pixel detection Critical
SA-CSS-SWI-003: per-tspan scope stroke-width=0; header tspan normal fill=none parent; header tspan inherits stroke-width=1 (visible); scope tspan stroke-width=0 override (zero ink); parent has stroke-width=1 — parent check passes textContent; parent-level stroke-width check passes; per-tspan getComputedStyle.strokeWidth inspection required for fill=none parent text High
SA-CSS-SWI-004: mousedown sets stroke-width=0; reverts mouseup fill=none; stroke-width=1 at rest (solid strokes visible); mousedown sets stroke-width=0 (all ink suppressed); reverted at mouseup; static scan passes Static DOM; requires MutationObserver on stroke-width + style attrs + synthetic mousedown + fill=none check on target Critical

SkillAudit findings for this attack surface

CRITICAL SA-CSS-SWI-001: stroke-width="0" on fill=none stroke-only consent text — stroke channel is the sole ink source for fill=none text; stroke-width=0 collapses the stroke to a zero-extent mathematical line; no pixels are covered and no ink is deposited on any glyph outline path; getComputedStyle(el).stroke returns a dark color (passes stroke color check — this is the deception); BCR non-zero; textContent intact; detection requires checking stroke-width on fill=none text elements and flagging value === 0 as zero-ink attack regardless of stroke color.
CRITICAL SA-CSS-SWI-002: stroke-width="0.001" on fill=none text — sub-pixel stroke width is invisible after anti-aliasing; at 96dpi the stroke covers approximately 0.001 physical pixels, contributing ~0.1% alpha per pixel row: completely imperceptible; getComputedStyle.strokeWidth returns "0.001px" — the value is technically non-zero; a basic strokeWidth > 0 check returns true and passes; float parsing plus a 0.25px threshold comparison is required; strokeWidth < 0.25px for fill=none text must be flagged as sub-pixel invisible.
HIGH SA-CSS-SWI-003: per-tspan scope stroke-width="0" on consent scope tspan within a fill=none stroke-only parent text element — header tspan inherits parent stroke-width=1 (visible); scope tspan overrides with stroke-width=0 (zero ink); parent text element has stroke-width=1 — parent-level stroke-width check passes; per-tspan getComputedStyle(tspan).strokeWidth inspection is required for all tspan children of fill=none parent text elements, accounting for each tspan's own attribute overriding the inherited parent value.
CRITICAL SA-CSS-SWI-004: JS mousedown sets stroke-width="0" on fill=none stroke-only consent text; all glyph outlines suppressed during click window; element text invisible; reverted at mouseup; at rest stroke-width=1, solid strokes visible — all static checks pass; MutationObserver must watch stroke-width attribute and style attribute (CSS stroke-width property) with synthetic mousedown simulation; must verify fill=none on target to confirm stroke suppression hides the only rendering channel, not merely a decorative stroke on fill-rendered text.

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

← Blog  |  Security Checklist