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
<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
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
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
<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.