← Blog

MCP server SVG feComponentTransfer gamma consent security

Four SA-CSS-GAM consent attack patterns using feComponentTransfer type="gamma" — the gamma transfer function C' = amplitude × C^exponent + offset applied per channel to produce white-out, dark-out, near-transparent, and click-time alpha erasure attacks.

By SkillAudit  |  23 Oct 2026

The feComponentTransfer SVG filter primitive applies a per-channel transfer function to every pixel. The type="gamma" variant uses the power-law formula C' = amplitude × C^exponent + offset (SVG §15.10). This formula has three free parameters — amplitude, exponent, and offset — each of which can be independently exploited to map consent element pixel values to constants that render text invisible. The gamma type is distinct from type="linear" (which uses C' = slope × C + intercept) and from the table/discrete types; its power-law nonlinearity creates attack configurations that evade threshold checks tuned for the linear case.

Formula breakdown: With amplitude=0, the formula collapses to C' = offset regardless of the input. This makes the transfer function a constant mapping: every input value produces exactly offset. Set offset=1 for white-out (all channels clamped to 1); set offset=0 for dark-out or full alpha erasure depending on which feFuncX is targeted.

SA-CSS-GAM-001: amplitude=0 + offset=1 on RGB channels (white-out)

Applying type="gamma" amplitude="0" exponent="1" offset="1" to feFuncR, feFuncG, and feFuncB sets all three RGB channels to exactly 1 for every input pixel. The output color is rgb(255,255,255) — pure white — independent of the consent text color. On a white or light-themed MCP client, the consent element becomes fully white and invisible. The alpha channel is not touched; the element remains fully opaque and occupies its normal layout space.

This attack is structurally valid SVG: all attribute values are in-range for their types. Structural scanners that check for valid attribute ranges without evaluating the formula output will not flag it. The rendered output is detectable only by computing the formula or simulating pixel output.

<filter id="gam1">
  <feComponentTransfer>
    <feFuncR type="gamma" amplitude="0" exponent="1" offset="1"/>
    <feFuncG type="gamma" amplitude="0" exponent="1" offset="1"/>
    <feFuncB type="gamma" amplitude="0" exponent="1" offset="1"/>
  </feComponentTransfer>
</filter>
<!-- C'_R = 0 × C_R^1 + 1 = 1; same for G and B → all pixels white-out -->

SA-CSS-GAM-002: amplitude=0 + offset=0 on alpha channel (full transparency)

Targeting the alpha channel with feFuncA type="gamma" amplitude="0" exponent="1" offset="0" maps every alpha value to 0. The element becomes fully transparent. Unlike CSS opacity:0, SVG filter-induced transparency does not affect getComputedStyle.opacity, which continues to return 1. getBoundingClientRect() returns non-zero dimensions. textContent is unchanged. The element is invisible to the user but fully present in the DOM and accessible tree.

<filter id="gam2">
  <feComponentTransfer>
    <feFuncA type="gamma" amplitude="0" exponent="1" offset="0"/>
  </feComponentTransfer>
</filter>
<!-- C'_A = 0 × C_A^1 + 0 = 0; element fully transparent regardless of original opacity -->

Alpha-only erasure detection: Because only feFuncA is affected, RGB channel analysis of the filter finds no anomaly — the color values are correctly preserved. An auditor checking only for illegal color transforms will miss this attack. The alpha channel must be independently evaluated.

SA-CSS-GAM-003: exponent=0.001 on RGB channels (near-white via power-law limit)

For any channel value C > 0, the expression C^exponent approaches 1 as exponent approaches 0. At exponent=0.001 with amplitude=1 and offset=0, dark text pixels (e.g., C ≈ 0.05 for near-black) produce 0.05^0.001 ≈ 0.9965 — effectively white. Mid-gray pixels (C = 0.5) produce 0.5^0.001 ≈ 0.9993. White background pixels (C = 1) produce 1^0.001 = 1. The result is that all pixels converge to values indistinguishably close to 1 — the consent text disappears against a white background. This attack evades amplitude=0 detection heuristics because amplitude remains at its default value (1), and the attack effect is entirely driven by the near-zero exponent.

<filter id="gam3">
  <feComponentTransfer>
    <feFuncR type="gamma" amplitude="1" exponent="0.001" offset="0"/>
    <feFuncG type="gamma" amplitude="1" exponent="0.001" offset="0"/>
    <feFuncB type="gamma" amplitude="1" exponent="0.001" offset="0"/>
  </feComponentTransfer>
</filter>
<!-- C^0.001 → 1 for all C > 0; dark text ≈ 0.05 → 0.997; background = 1 → 1; all pixels near-white -->

Note the edge case: exactly C = 0 (pure black, no anti-aliasing) maps to 0^0.001 = 0, remaining black. Anti-aliased text edges, which always have intermediate values, are the primary erasure targets. Pixel-perfect black glyph centers survive but are isolated dots, insufficient for readability.

SA-CSS-GAM-004: JS mousedown changes feFuncA amplitude 1 → 0 (click-time alpha erasure)

The filter begins with feFuncA type="gamma" amplitude="1" exponent="1" offset="0" — the identity transfer for alpha (every alpha value maps to itself). The consent element is fully visible. On mousedown, a script changes amplitude to "0", mapping all alpha values to 0. The element disappears at the moment of click interaction.

consentElement.addEventListener("mousedown", () => {
  const fa = document.getElementById("consentFilter")
              .querySelector("feFuncA");
  fa.setAttribute("amplitude", "0");
  // exponent and offset remain 1 and 0; formula: 0 × C_A^1 + 0 = 0
  // element becomes fully transparent at click time
});

MutationObserver requirement: Static analysis sees amplitude="1" (identity) at load time. Only a MutationObserver on the amplitude attribute of feFuncA (and feFuncR/feFuncG/feFuncB) within consent ancestor filters will detect the live modification. Without dynamic observation, SA-CSS-GAM-004 is invisible to static scanners.

Attack summary

IDChannelamplitudeexponentoffsetEffectSeverity
SA-CSS-GAM-001R, G, B011C'=1 for all → white-outCritical
SA-CSS-GAM-002A010C'_A=0 for all → transparentCritical
SA-CSS-GAM-003R, G, B10.0010C^0.001≈1 for all C>0 → near-whiteHigh
SA-CSS-GAM-004A (mousedown)1→010Identity→transparent at click timeCritical

Defences

SkillAudit findings for this attack surface

CRITICAL SA-CSS-GAM-001: feFuncR/feFuncG/feFuncB type="gamma" amplitude="0" offset="1" — gamma formula collapses to C'=1; white-out on all RGB channels; element invisible on white/light backgrounds; alpha unaffected; getComputedStyle.color unchanged; structurally valid SVG.
CRITICAL SA-CSS-GAM-002: feFuncA type="gamma" amplitude="0" offset="0" — alpha channel collapses to 0; element fully transparent; layout/DOM/textContent unchanged; getComputedStyle.opacity returns 1 (filter transparency invisible to CSS inspection); alpha-specific detection required.
HIGH SA-CSS-GAM-003: feFuncR/feFuncG/feFuncB type="gamma" exponent="0.001" — power-law limit drives C^0.001→1 for all C>0; dark text pixels (C≈0.05) map to ≈0.997; near-white-out on light backgrounds; amplitude=1 (nominal value) evades amplitude=0 heuristic; exponent threshold check required.
CRITICAL SA-CSS-GAM-004: JS mousedown changes feFuncA amplitude from "1" to "0" — initial identity transfer (amplitude=1, exponent=1, offset=0); click-time alpha erasure; static analysis sees identity; MutationObserver on feFuncA amplitude attribute required.

Related: feComponentTransfer consent attacks overview  |  feComponentTransfer type=table attacks  |  feComponentTransfer type=discrete attacks  |  feColorMatrix consent attacks

← Blog  |  Security Checklist