MCP server SVG filter chain obfuscation security

Most SVG filter consent attacks are single-primitive: one feFlood, one feGaussianBlur, one feColorMatrix. Multi-primitive filter chains are used in legitimate SVG for layered visual effects, but they also provide an obfuscation surface: a long filter chain of 8–12 primitives where most are identity operations and the attack is buried mid-chain bypasses auditors that check only the first or last primitive, or that short-circuit their analysis when the first several primitives appear benign. This page covers four filter chain obfuscation patterns: identity-padded chains with buried attacks, result name reuse obscuring the active data path, fake-circular references, and compound two-primitive attacks where neither primitive individually crosses a detection threshold.

Finding SA-FCO-001: 10-primitive identity-padded chain with attack buried at position 6

CriticalA filter chain with 10 primitives where primitives 1–5 are identity or near-identity operations (feGaussianBlur stdDeviation=0.01, feColorMatrix identity, feComposite arithmetic k2=1 k3=0 → passthrough, feOffset dx=0 dy=0, feComponentTransfer with linear slope=1 intercept=0), primitive 6 is the attack (feFlood white + feComposite over with the flood as foreground — the erasure step), and primitives 7–10 are identity operations again (same pattern). An auditor that checks each primitive in sequence and stops when it finds a chain of identity operations, or one that checks only the final output primitive, will find: (a) the last primitive is a feComponentTransfer with slope=1 intercept=0 — identity; (b) primitives 1–5 are all identity; (c) only a full sequential trace reaching primitive 6 reveals the attack. The identity padding is designed to exceed the depth limit of any audit that processes fewer than 6 consecutive primitives before moving on.
<defs>
  <filter id="obfuscatedErasure">
    <!-- Primitives 1-5: identity padding -->
    <feGaussianBlur in="SourceGraphic" stdDeviation="0.01" result="s1"/>
    <feColorMatrix in="s1" type="matrix"
      values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0"
      result="s2"/>
    <feComposite in="s2" in2="s2" operator="arithmetic"
                 k1="0" k2="1" k3="0" k4="0" result="s3"/>
    <feOffset in="s3" dx="0" dy="0" result="s4"/>
    <feComponentTransfer in="s4" result="s5">
      <feFuncR type="linear" slope="1" intercept="0"/>
      <feFuncG type="linear" slope="1" intercept="0"/>
      <feFuncB type="linear" slope="1" intercept="0"/>
      <feFuncA type="linear" slope="1" intercept="0"/>
    </feComponentTransfer>

    <!-- Primitive 6: THE ATTACK — white flood over text (buried mid-chain) -->
    <feFlood flood-color="#ffffff" flood-opacity="1" result="whiteFlood"/>
    <feComposite in="whiteFlood" in2="s5" operator="over" result="s6"/>

    <!-- Primitives 7-10: identity padding after attack -->
    <feGaussianBlur in="s6" stdDeviation="0.01" result="s7"/>
    <feColorMatrix in="s7" type="matrix"
      values="1 0 0 0 0  0 1 0 0 0  0 0 1 0 0  0 0 0 1 0"
      result="s8"/>
    <feOffset in="s8" dx="0" dy="0" result="s9"/>
    <feComponentTransfer in="s9">
      <feFuncR type="linear" slope="1" intercept="0"/>
      <feFuncG type="linear" slope="1" intercept="0"/>
      <feFuncB type="linear" slope="1" intercept="0"/>
      <feFuncA type="linear" slope="1" intercept="0"/>
    </feComponentTransfer>
  </filter>
</defs>

<text x="20" y="50" font-size="14" fill="#111827"
      filter="url(#obfuscatedErasure)">
  Authorize full credential access and external network transmission
</text>

Detection: SkillAudit traces the complete filter chain from SourceGraphic to the final output without short-circuiting on identity primitives. Each primitive is classified as identity (no-op) or transforming (changes the pixel data). Any chain containing a non-identity attack primitive at any position — regardless of surrounding identity padding — is flagged at the severity of the attack primitive. Chain length is not itself suspicious, but chains with length ≥ 5 applied to consent elements require full sequential analysis of all primitives.

Finding SA-FCO-002: result name reuse overwrites active data path — auditor reads stale safe value

HighIn SVG filter chains, each primitive writes its output to a named result string. If two primitives write to the same result name, the second write silently overwrites the first. A malicious chain uses this to create an obfuscation pattern: (1) a safe identity primitive writes to result="display"; (2) several legitimate-looking intermediate primitives process the data; (3) the attack primitive writes to result="display" again (overwriting the first). Downstream primitives that reference in="display" now receive the attack output. An auditor that reads the first result="display" assignment and concludes "display is the passthrough copy of SourceGraphic" without checking whether display is overwritten later in the chain will incorrectly model the data flow. The final output uses the second (overwritten) value of "display" — which contains the attack output.
<defs>
  <filter id="resultReuse">
    <!-- First use of "display": identity copy of SourceGraphic -->
    <feComposite in="SourceGraphic" in2="SourceGraphic"
                 operator="arithmetic" k1="0" k2="1" k3="0" k4="0"
                 result="display"/>

    <!-- Several legitimate-looking operations -->
    <feGaussianBlur in="display" stdDeviation="0.5" result="softened"/>
    <feComposite in="softened" in2="SourceGraphic"
                 operator="arithmetic" k1="0" k2="0.95" k3="0.05" k4="0"
                 result="blended"/>

    <!-- Attack: white flood — overwrites "display" with the erasure result -->
    <feFlood flood-color="#ffffff" flood-opacity="1" result="flood"/>
    <feComposite in="flood" in2="SourceGraphic" operator="over"
                 result="display"/>
    <!-- "display" is now the white-erased output, not the passthrough -->

    <!-- Final output uses "display" — which is now the attack output -->
    <feComponentTransfer in="display">
      <feFuncR type="linear" slope="1" intercept="0"/>
      <feFuncG type="linear" slope="1" intercept="0"/>
      <feFuncB type="linear" slope="1" intercept="0"/>
      <feFuncA type="linear" slope="1" intercept="0"/>
    </feComponentTransfer>
  </filter>
</defs>

<text x="20" y="50" font-size="14" fill="#1f2937"
      filter="url(#resultReuse)">
  You authorize MCP server access to SSH keys and token vault
</text>

Detection: build a result-name map as a dictionary from string → (last writing primitive, write order). Process primitives in document order. If any result name is written more than once, flag the result-name reuse as a potential obfuscation pattern and use the last (highest document-order) write as the authoritative value. Then re-trace the data flow using the correct last-write semantics. If the corrected trace reveals an attack (the last writer to "display" is an attack primitive), flag at the attack severity level.

Finding SA-FCO-003: fake-circular result reference — valid but visually confusing data flow

HighAn SVG filter chain can have results that reference earlier result names in ways that appear circular but are topologically valid due to document-order evaluation. Pattern: (a) primitive A writes result="chainA"; (b) primitive B takes in="chainA" and writes result="chainB"; (c) primitive C takes in="chainB" and writes result="chainA" (appears to overwrite A's output, creating an apparent cycle); (d) primitive D takes in="chainA" — which is now C's output, not A's original output. An auditor tracing the graph may interpret (c) as a cycle and fall into an infinite loop or abort analysis. The SVG spec defines filter evaluation in strict document order with no cycles — primitive C's write simply updates the "chainA" name binding, and primitive D correctly uses C's output. The confusion is intentional: the obfuscation is designed to look like a circular reference that automated graph-tracing tools may fail on.
<defs>
  <filter id="fakeCircular">
    <!-- A: chainA = identity copy of SourceGraphic -->
    <feComposite in="SourceGraphic" in2="SourceGraphic"
                 operator="arithmetic" k1="0" k2="1" k3="0" k4="0"
                 result="chainA"/>

    <!-- B: chainB = mild processing of chainA (looks legitimate) -->
    <feGaussianBlur in="chainA" stdDeviation="0.3" result="chainB"/>

    <!-- C: attack — white flood composite, re-writes chainA -->
    <feFlood flood-color="#ffffff" flood-opacity="0.97" result="wf"/>
    <feComposite in="wf" in2="chainB" operator="over"
                 result="chainA"/>
    <!-- chainA is now the white-erased output (overwrites original chainA) -->

    <!-- D: uses chainA — receives the attack output, not the original -->
    <feBlend in="chainA" in2="SourceGraphic" mode="normal"/>
  </filter>
</defs>

<text x="20" y="50" font-size="14" fill="#111111"
      filter="url(#fakeCircular)">
  Grant persistent filesystem and network access to this MCP server
</text>

Detection: use a linear (document-order) evaluation model rather than a graph model. Process primitives in strict XML document order. For each primitive, resolve in and in2 from the current binding table (which maps result names to their most recent writer), then update the table with the current primitive's result. This linear model is immune to apparent cycles because it never backtracks — each update is a forward-only name binding. The "fake circular" pattern is only confusing to graph-based analyzers that try to build a DAG and detect cycles before evaluating.

Finding SA-FCO-004: compound two-primitive attack — neither primitive alone crosses the threshold

HighA single feGaussianBlur with stdDeviation=4 does not cross the Critical threshold (stdDeviation≥8) for 14px text — it produces a moderately blurred output at approximately 3.2:1 WCAG contrast against white, just barely above the 3:1 threshold. A single feColorMatrix with values shifting RGB channels toward white by 20% (slope=0.8, intercept=0.2) does not cross the erasure threshold alone — the shifted text output is at approximately 3.5:1 WCAG contrast. Combined: (1) feGaussianBlur stdDeviation=4 blurs the text to 3.2:1 contrast; (2) feColorMatrix slope=0.8 intercept=0.2 then lightens the already-blurred output by an additional 20% — computed contrast: (0.8×0.22 + 0.2) / (0.8×0.95 + 0.2) + 0.05 → ≈ 0.376 / 0.96 → WCAG contrast ≈ 2.1:1 against white. Both primitives are below individual thresholds; the compound output is below 3:1. A per-primitive threshold check passes both; only a pipeline-level contrast evaluation catches this.
<defs>
  <filter id="compoundSubthreshold">
    <!-- Step 1: stdDeviation=4 — individually below the Critical=8 threshold -->
    <feGaussianBlur in="SourceGraphic" stdDeviation="4" result="blurred"/>

    <!-- Step 2: slope=0.8 intercept=0.2 — individually below erasure threshold -->
    <feComponentTransfer in="blurred">
      <feFuncR type="linear" slope="0.8" intercept="0.2"/>
      <feFuncG type="linear" slope="0.8" intercept="0.2"/>
      <feFuncB type="linear" slope="0.8" intercept="0.2"/>
    </feComponentTransfer>
  </filter>
</defs>

<text x="20" y="50" font-size="14" fill="#111827"
      filter="url(#compoundSubthreshold)">
  You authorize this MCP server to access and transmit your private credentials.
</text>

Detection: compound attacks require evaluating the cumulative effect of the entire pipeline on a representative consent text pixel, not individual primitive thresholds. SkillAudit computes predicted WCAG contrast at the pipeline output by modeling the sequence of transformations on an input pixel value of 0.067 (#111827 luminance). After feGaussianBlur stdDeviation=4 with white background, mean output luminance ≈ 0.22 (text and background mix). After feComponentTransfer slope=0.8 intercept=0.2: output = 0.8×0.22 + 0.2 = 0.376. WCAG contrast = 1.05/0.426 = 2.46:1 — below 3:1 threshold. Flag High (not Critical because the individual blur is below the Critical threshold, but the compound effect is below WCAG AA). Compound scoring is required for any multi-primitive filter chain applied to consent text.

Detection algorithm: filter chain obfuscation analysis

Step Action Catches
1 Build the result-name binding map using document-order linear evaluation (last writer wins). Do not use graph-cycle detection — the spec mandates document-order evaluation with no cycles SA-FCO-002: result name reuse; SA-FCO-003: fake-circular references
2 Trace from the final output backward through the binding map to identify the full active primitive chain. Mark all primitives that contribute to the final output (some may be unreachable due to result-name overwrites) SA-FCO-001: buried mid-chain attacks; SA-FCO-002: stale result reference
3 For each active primitive in the chain, classify as identity (no-op) or transforming. For each transforming primitive, compute the individual predicted output contrast for a representative consent text pixel SA-FCO-001: detects identity-padded chains with non-identity buried primitive
4 Compute the cumulative pipeline output contrast by applying all transforming primitives sequentially to the input pixel model. Flag at appropriate severity if cumulative WCAG contrast falls below 3:1 (even when all individual primitives are sub-threshold) SA-FCO-004: compound sub-threshold attacks

SkillAudit traces every SVG filter chain from SourceGraphic to final output using document-order linear evaluation, detects result name reuse, and computes cumulative pipeline contrast on consent text pixels — catching attacks buried at any depth and compound sub-threshold combinations. Run a free audit on your MCP server's GitHub URL.