SVG feComponentTransfer Transfer Function Attacks on MCP Consent: gamma, table, discrete, and linear Type Synthesis
The four feComponentTransfer transfer-function subtypes — type=gamma, type=table, type=discrete, and type=linear — form a unified consent-attack family that applies different mathematical encodings of the same underlying operation: produce a constant or near-constant output regardless of input, then route that constant through the element's alpha or RGB channels to erase or white-out consent text.
In our research auditing MCP server SVG for consent attacks, we have catalogued sixteen attack patterns across the four feComponentTransfer subtypes. Individually, each subtype article covers four patterns with code examples and defences. But in practice, a sophisticated attacker does not commit to a single subtype — they mix subtypes across channels, exploiting the fact that per-subtype analyzers have blind spots at the intersection.
This synthesis covers all four transfer-function families in a single unified framework. We present the mathematical basis of each, the canonical constant-output form, the readability-attack variants, and then a unified detection algorithm that does not need separate rules per subtype. We also compare the feComponentTransfer attack surface with the related feColorMatrix type=matrix attack surface, which achieves similar ends through a completely different SVG syntax that requires independent detection paths.
The feComponentTransfer primitive: architecture
The feComponentTransfer SVG filter primitive applies a per-channel transfer function to each pixel. Four child elements define the transfer for each channel: feFuncR, feFuncG, feFuncB, and feFuncA. Each feFuncX element has a type attribute selecting the mathematical formula, plus type-specific parameters.
The key architectural property for consent attacks is channel independence: each feFuncX is evaluated independently. An attacker can apply a type=gamma transfer to feFuncA (alpha channel) while leaving feFuncR/feFuncG/feFuncB as type=identity — or can mix gamma, table, discrete, and linear across the four channels in a single feComponentTransfer. Analyzers that check for a specific subtype on a specific channel will miss cross-type mixed attacks.
Threat model: An MCP server embeds SVG with a filter attribute (or style="filter:url(#f)") on a consent element — a button, dialog, or permission grant panel. The filter's feComponentTransfer primitive applies a transfer function that, at render time or click-time, makes the element invisible (alpha erasure) or indistinguishable from the background (white-out or dark-out). The user proceeds through a UI they believe to be empty; the consent element records a click.
Type 1: gamma — power-law transfer (SA-CSS-GAM)
The type=gamma transfer applies the formula: C' = amplitude × C^exponent + offset (SVG §15.10). Three parameters control the output: amplitude (default 1), exponent (default 1), offset (default 0). The default configuration is the identity function (1 × C^1 + 0 = C).
The constant-output attacks exploit two special cases of the power-law formula:
SA-CSS-GAM-001: exponent=0 (constant amplitude+offset)
Any number raised to the power 0 equals 1 (and 0^0 is treated as 1 in most SVG renderers). Therefore C^0 = 1 for all inputs, and C' = amplitude × 1 + offset = amplitude + offset. The output is constant regardless of C. Setting exponent="0" amplitude="0" offset="0" on feFuncA produces constant alpha = 0 (transparent). Setting exponent="0" amplitude="0" offset="1" on feFuncR/feFuncG/feFuncB produces constant RGB = 1 (white-out).
<filter id="gam1">
<feComponentTransfer>
<feFuncA type="gamma" amplitude="0" exponent="0" offset="0"/>
</feComponentTransfer>
</filter>
<!-- C' = 0 × C^0 + 0 = 0; all alpha → 0; element transparent -->
SA-CSS-GAM-002: amplitude=0 (offset-only constant)
With amplitude=0, the formula reduces to C' = 0 × C^exponent + offset = offset. The output is exactly offset, independent of input and exponent. This is structurally different from the exponent=0 case: here exponent is irrelevant, but amplitude must be zero. Applied to feFuncA with offset="0", all alpha values map to 0.
<filter id="gam2">
<feComponentTransfer>
<feFuncA type="gamma" amplitude="0" exponent="1" offset="0"/>
</feComponentTransfer>
</filter>
<!-- C' = 0 × C^1 + 0 = 0; alpha → 0; exponent=1 (default) makes this look normal -->
SA-CSS-GAM-003: near-zero exponent (extreme compression)
With exponent=0.01, the power-law is highly nonlinear. For any input C > 0.02: C^0.01 > 0.96 (nearly 1). The output is approximately amplitude × 1 + offset ≈ amplitude + offset for nearly all non-trivial inputs. With amplitude=0 and offset=1: C' ≈ 1 for all C > 0.02 — a near-constant white-out that processes pixels with non-zero alpha as white, while the actual formula appears reasonable at a glance. The only pixel that escapes is the fully-transparent pixel (C=0), which maps to exactly offset=1 regardless.
<filter id="gam3">
<feComponentTransfer>
<feFuncR type="gamma" amplitude="1" exponent="0.01" offset="0"/>
<feFuncG type="gamma" amplitude="1" exponent="0.01" offset="0"/>
<feFuncB type="gamma" amplitude="1" exponent="0.01" offset="0"/>
</feComponentTransfer>
</filter>
<!-- C^0.01 → near-1 for any C > 0.02; channels approach 1.0; near white-out via compression -->
SA-CSS-GAM-004: JS mousedown amplitude/offset swap
Initial state: amplitude="1" offset="0" (identity for exponent=1). On mousedown: amplitude set to 0, offset set to 0. The filter transitions from identity to constant-zero alpha. The consent element disappears at click time. Tools auditing only initial attribute values will not flag this attack vector.
Type 2: table — piecewise-linear transfer (SA-CSS-TAB)
The type=table transfer performs piecewise-linear interpolation between N breakpoints in tableValues. For N values, the input domain [0, 1] is divided into N−1 equal segments of width 1/(N−1). An input in segment k is linearly interpolated between tableValues[k] and tableValues[k+1].
The constant-output attack is the all-equal-breakpoint table. For a two-value table, there is a single segment [0,1] interpolating between two equal values V — every input maps to V. For alpha erasure: tableValues="0 0". For white-out: tableValues="1 1". The syntactic representation is unambiguously valid (N=2 values, both in [0,1]), but the semantic is constant-output.
SA-CSS-TAB-001/002: constant-output two-value tables
<!-- Alpha erasure --> <feFuncA type="table" tableValues="0 0"/> <!-- White-out --> <feFuncR type="table" tableValues="1 1"/> <feFuncG type="table" tableValues="1 1"/> <feFuncB type="table" tableValues="1 1"/>
Detection: parse tableValues as a list of floats. If all values are within 0.02 of the same value, the table is constant-output. Check the constant against alpha-erasure threshold (near-0) or white-out threshold (near-1).
SA-CSS-TAB-003: high-frequency alternation (readability attack)
A 10-value alternating table "0 1 0 1 0 1 0 1 0 1" creates 9 segments with rapidly alternating direction. Adjacent pixels with similar input R values land in adjacent segments and receive opposite transfer outputs. The spatial frequency of the R-channel variation exceeds the Nyquist frequency of the glyph anti-aliasing — readable text is converted to high-frequency noise without erasing alpha.
<feFuncR type="table" tableValues="0 1 0 1 0 1 0 1 0 1"/>
Detection: count sign changes in finite differences (tableValues[k+1] − tableValues[k]). If ≥3 sign changes in N−1 segments, flag as spatial-frequency attack on readability.
SA-CSS-TAB-004: identity table as JS override target
tableValues="0 1" is the exact linear identity for the alpha channel (segment [0,1] interpolates 0→1 = input). This is a safe initial state. On mousedown, it is replaced with "0 0". Auditing only static values will pass this because the initial table is genuinely identity.
Type 3: discrete — nearest-neighbor binning (SA-CSS-DIS)
The type=discrete transfer partitions the [0,1] input range into N equal bins (one per value in tableValues) and assigns every input in bin k the output tableValues[k]. There is no interpolation — the output is a step function with N steps of width 1/N.
The constant-output attack is any single-value tableValues (N=1), which covers the entire [0,1] range with a single output. tableValues="0" on feFuncA maps every alpha to 0. Multi-value tables with all equal values are also constant-output but syntactically distinguish from N=1.
SA-CSS-DIS-001: N=1 single-bin alpha erasure
<filter id="dis1">
<feComponentTransfer>
<feFuncA type="discrete" tableValues="0"/>
</feComponentTransfer>
</filter>
<!-- N=1: bin [0,1] → output 0; all alpha → 0; element transparent -->
A single value is the most minimal constant-output discrete transfer. Some analyzers check for tableValues length and may warn about single-entry tables; most do not.
SA-CSS-DIS-002: N=2 two-bin white-out
<feFuncR type="discrete" tableValues="1 1"/> <feFuncG type="discrete" tableValues="1 1"/> <feFuncB type="discrete" tableValues="1 1"/> <!-- N=2: bins [0,0.5) → 1, [0.5,1] → 1; all RGB → 1; white-out -->
Two equal values produce a two-bin constant table. Structurally indistinguishable from a legitimate discrete quantizer, but both outputs are the same constant.
SA-CSS-DIS-003: alternating bins (1-bit alpha checkerboard)
A four-value alternating table "0 1 0 1" assigns alpha of 0 to inputs in bins [0, 0.25) and [0.5, 0.75), and alpha of 1 to inputs in [0.25, 0.5) and [0.75, 1]. Anti-aliased text glyphs have alpha values continuously distributed from 0 to 1 at their edges. The alternating discrete transfer converts this continuous distribution into a binary checkerboard pattern. Adjacent anti-aliased pixels with alpha 0.24 and 0.26 receive discrete outputs 0 and 1 respectively — a spatial 1-bit noise field that completely destroys glyph readability at edges without reducing average alpha to zero.
<feFuncA type="discrete" tableValues="0 1 0 1"/> <!-- N=4: [0,0.25)→0, [0.25,0.5)→1, [0.5,0.75)→0, [0.75,1]→1; checkerboard -->
SA-CSS-DIS-004: JS mousedown N=1 collapse
Initial state: tableValues="0 0.5 1" (N=3 identity-approximating quantizer: bins → 0, 0.5, 1). On mousedown: set to "0". The N=3 quantizer is not the identity but it preserves general brightness levels — passes an approximate-identity check. At click time, collapse to N=1 constant-zero erases all alpha.
Type 4: linear — slope/intercept transfer (SA-CSS-LIN)
The type=linear transfer applies the formula: C' = slope × C + intercept (SVG §15.10, linear variant). Default values are slope=1, intercept=0 (identity). The formula is constrained to [0,1] by SVG clipping (outputs outside [0,1] are clamped).
Linear transfer attacks are the most analytically transparent of the four subtypes — a constant-output linear transfer has slope=0 (constant = intercept), and this is directly visible in the attribute. Yet they are still actively used in MCP consent attacks because:
- Most consent-element analyzers check for specific dangerous values (zero alpha, black fill) rather than performing output analysis
- A high-slope amplification attack (slope=100, intercept=0) is not constant-output but produces constant-clamped-to-1 output for any input C > 0.01
- Negative slope creates inversion attacks that pass non-negativity checks
SA-CSS-LIN-001: slope=0, intercept=0 (dark-out)
<filter id="lin1">
<feComponentTransfer>
<feFuncA type="linear" slope="0" intercept="0"/>
</feComponentTransfer>
</filter>
<!-- C' = 0 × C + 0 = 0; all alpha → 0; element transparent (dark-out + transparent) -->
SA-CSS-LIN-002: slope=0, intercept=1 (white-out)
<feFuncR type="linear" slope="0" intercept="1"/> <feFuncG type="linear" slope="0" intercept="1"/> <feFuncB type="linear" slope="0" intercept="1"/> <!-- C' = 0 × C + 1 = 1; all RGB → 1; white-out -->
The most structurally obvious constant-output linear attack. slope=0 is the constant flag; intercept is the constant value. Any static analyzer that checks for slope=0 on consent-ancestor feFuncX elements will catch this. But mixed attacks (slope=0 on feFuncA + slope=100 on feFuncR, treated as separate patterns) can evade analyzers that treat each feFuncX independently.
SA-CSS-LIN-003: slope=100, intercept=0 (amplify-to-clamp white-out)
With slope=100, the formula C' = 100 × C is clamped to 1 for any input C > 0.01. In practice, consent text glyphs have anti-aliased pixels with alpha values ranging from 0.05 to 1.0 at their edges. Every pixel with C > 0.01 (essentially all non-background pixels) is clamped to 1.0 on the affected channel. Applied to RGB channels: all non-background pixels become pure white on those channels. This is not technically a constant-output function (C=0 maps to 0), but functionally it is a white-out for all non-transparent pixels.
<feFuncR type="linear" slope="100" intercept="0"/> <feFuncG type="linear" slope="100" intercept="0"/> <feFuncB type="linear" slope="100" intercept="0"/> <!-- C' = 100×C, clamped to 1; any C > 0.01 → 1; practical white-out via amplification -->
Detection must check not just slope=0 but whether (slope × max_input + intercept) is outside [0, 1] for representative inputs. For consent elements, max_input ≈ 1 for opaque text pixels; slope=100 + intercept=0 gives max output = 100, clamped to 1. High-slope attacks require output-range analysis.
SA-CSS-LIN-004: negative slope (inversion)
With slope=−1 and intercept=1: C' = −C + 1 = 1 − C. This is the channel inversion (photographic negative). Dark text (low C) becomes light; light background (high C) becomes dark. On a white background (bg C=1), inverted bg = 0 (black). On dark text (C=0.1), inverted text = 0.9 (light gray). If the consent element sits on a background with non-trivial color, the inversion can make text nearly invisible — depending on the precise background tone, the inverted contrast can drop below WCAG 1.1:1. This is a precision consent readability attack rather than an erasure attack.
<feFuncR type="linear" slope="-1" intercept="1"/> <feFuncG type="linear" slope="-1" intercept="1"/> <feFuncB type="linear" slope="-1" intercept="1"/> <!-- C' = 1 - C; inversion; depends on background tone for readability effect -->
Most contrast analyzers measure input contrast, not post-filter contrast. The post-filter WCAG measurement requires evaluating the transfer function on the actual foreground and background colors.
Cross-type evasion: mixing subtypes across channels
The four subtypes are fully independent per channel. An attacker can construct a single feComponentTransfer that uses different types on different channels, making per-subtype analysis fail completely:
<filter id="mixed">
<feComponentTransfer>
<feFuncR type="gamma" amplitude="0" exponent="1" offset="1"/> <!-- SA-CSS-GAM: constant 1 -->
<feFuncG type="table" tableValues="1 1"/> <!-- SA-CSS-TAB: constant 1 -->
<feFuncB type="discrete" tableValues="1"/> <!-- SA-CSS-DIS: constant 1 -->
<feFuncA type="linear" slope="0" intercept="0"/> <!-- SA-CSS-LIN: constant 0 -->
</feComponentTransfer>
</filter>
<!-- All RGB → 1 (white-out), all alpha → 0 (transparent); four different subtypes, one filter -->
Per-subtype analyzers fail here: A gamma analyzer checks feFuncR (flags it). A table analyzer checks feFuncG (flags it). A discrete analyzer checks feFuncB (flags it). A linear analyzer checks feFuncA (flags it). But an analyzer that only checks one subtype misses three-quarters of the attack surface. And an analyzer that checks only specific channels (e.g., only feFuncA) misses the RGB attacks. The only correct analysis applies the unified detection algorithm to all four feFuncX elements across all four subtypes.
Unified detection algorithm
The unified detection algorithm for feComponentTransfer consent attacks evaluates each feFuncX element in every filter applied to a consent ancestor element. For each feFuncX:
- Identify the transfer type. Read the
typeattribute: gamma, table, discrete, linear, identity, or missing (default: identity). Identity type passes immediately. - Evaluate constant-output status.
- gamma: constant if
exponent=0(any amplitude/offset; constant = amplitude+offset) oramplitude=0(constant = offset). Near-constant if |exponent| < 0.05 (compression to near amplitude+offset for most inputs). - table: constant if all tableValues within 0.02 of each other. Compute max − min of parsed float list. Also check N=0 or N=1 as edge cases.
- discrete: constant if N=1 tableValues, or all tableValues equal. Check max − min = 0.
- linear: constant if slope=0 (constant = intercept). Near-clamp-constant if |slope| > 50 (any input C > 1/|slope| maps to clamped extreme). Check intercept sign for direction.
- gamma: constant if
- Check the constant value against the channel role.
- For
feFuncA: constant near-0 (within 0.05) is a critical alpha-erasure attack. - For
feFuncR,feFuncG,feFuncB: constant near-1 is a white-out attack; constant near-0 contributes to dark-out. Any two of three RGB channels constant-near-1 is a readability attack.
- For
- Detect high-frequency oscillation (table and discrete only).
- table: count sign changes in finite differences. ≥3 sign changes in N−1 differences → spatial-frequency attack.
- discrete: count alternating-value transitions. ≥2 alternating transitions in N bins → 1-bit checkerboard risk.
- Flag JS override risk. Any
feFuncXon a consent ancestor element is a JS override risk regardless of current values. MutationObserver must cover all four attribute sets: amplitude/exponent/offset (gamma), tableValues (table and discrete), slope/intercept (linear).
Comparison with feColorMatrix type=matrix
The feColorMatrix type=matrix primitive uses a 4×5 matrix to transform RGBA color vectors. It can achieve the same constant-output-per-channel results as feComponentTransfer — a row with all-zero coefficients + a non-zero fifth column produces a constant channel output — but the syntax is completely different:
<!-- feColorMatrix alpha-erasure: row 3 (alpha output) = 0×R + 0×G + 0×B + 0×A + 0 = 0 --> <feColorMatrix type="matrix" values=" 1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0 0"/> <!-- vs feComponentTransfer alpha-erasure --> <feComponentTransfer> <feFuncA type="linear" slope="0" intercept="0"/> </feComponentTransfer>
Both achieve alpha = 0 on every pixel. But the detection rules are completely separate:
| Primitive | Constant-output signal | Detection rule |
|---|---|---|
| feComponentTransfer gamma | exponent=0 or amplitude=0 | Check exponent/amplitude attribute values |
| feComponentTransfer table | All tableValues equal | Parse float list, check max−min |
| feComponentTransfer discrete | N=1 or all tableValues equal | Same as table |
| feComponentTransfer linear | slope=0 | Check slope attribute |
| feColorMatrix matrix | Row with all-zero coefficients | Parse 4×5 matrix, check row dot products |
A scanner that covers all feComponentTransfer subtypes but not feColorMatrix matrix will miss feColorMatrix attacks. A scanner that covers feColorMatrix matrix but not feComponentTransfer will miss the transfer-function family. Full consent protection requires independent coverage of both primitive types.
The structural similarity is important for understating why these attacks persist: both primitives operate on per-pixel color values and can produce constant outputs that are visually indistinguishable from direct attribute assignments (e.g., opacity: 0 or fill: white). But they arrive via filter chains, bypassing checks on direct CSS properties.
Attack pattern summary
| ID | Type | Channel | Key parameter | Effect | Severity |
|---|---|---|---|---|---|
| SA-CSS-GAM-001 | gamma | A | exponent=0, amplitude=0, offset=0 | C^0=1; constant alpha=0; transparent | Critical |
| SA-CSS-GAM-002 | gamma | A | amplitude=0, offset=0 | 0×C^exp+0=0; alpha→0 | Critical |
| SA-CSS-GAM-003 | gamma | R,G,B | exponent=0.01, amplitude=1 | Near-constant 1 for C>0.02; near white-out | High |
| SA-CSS-GAM-004 | gamma | A | JS mousedown amplitude→0 | Identity→transparent at click time | Critical |
| SA-CSS-TAB-001 | table | A | tableValues="0 0" | Piecewise constant-0; alpha→0 | Critical |
| SA-CSS-TAB-002 | table | R,G,B | tableValues="1 1" | Piecewise constant-1; white-out | Critical |
| SA-CSS-TAB-003 | table | R | tableValues="0 1 0 1…" | High-freq alternation; R-channel dithering | High |
| SA-CSS-TAB-004 | table | A | JS mousedown "0 1"→"0 0" | Identity table→constant-0 at click time | Critical |
| SA-CSS-DIS-001 | discrete | A | tableValues="0" | N=1; all alpha→0; transparent | Critical |
| SA-CSS-DIS-002 | discrete | R,G,B | tableValues="1 1" | N=2 constant-1; white-out | Critical |
| SA-CSS-DIS-003 | discrete | A | tableValues="0 1 0 1" | Alternating bins; 1-bit alpha checkerboard | High |
| SA-CSS-DIS-004 | discrete | A | JS mousedown N=3→N=1 | Quantizer→constant-0 at click time | Critical |
| SA-CSS-LIN-001 | linear | A | slope=0, intercept=0 | Constant alpha=0; transparent | Critical |
| SA-CSS-LIN-002 | linear | R,G,B | slope=0, intercept=1 | Constant RGB=1; white-out | Critical |
| SA-CSS-LIN-003 | linear | R,G,B | slope=100, intercept=0 | Amplify→clamp; near white-out via clamping | High |
| SA-CSS-LIN-004 | linear | R,G,B | slope=−1, intercept=1 | Inversion; background-dependent readability | Medium |
Runtime monitoring requirements
Static analysis catches the twelve constant-output patterns (SA-CSS-GAM-001 through SA-CSS-LIN-002) and the readability attacks (TAB-003, DIS-003, LIN-003, LIN-004). But the four JS override patterns (GAM-004, TAB-004, DIS-004, plus any linear equivalent) require runtime monitoring:
- MutationObserver on feFuncX attributes: Cover
amplitude,exponent,offset(gamma);tableValues(table and discrete);slope,intercept(linear). Any change to these attributes on afeFuncXelement within a filter applied to a consent ancestor triggers immediate re-evaluation. - Event listener audit: Any consent element with a
mousedownevent listener (or ancestor with one) that could reach afeFuncXattribute is a JS override risk. Enumerate event listeners on the consent DOM subtree and its ancestors for this target set. - Re-evaluate on mutation: Each mutation fires the full unified detection algorithm on the affected filter. Do not diff against prior state — evaluate the new state from scratch, as the prior state may have been benign identity and the new state is the attack.
Defences for MCP server consent elements
The defences for the feComponentTransfer family are straightforward but must be applied at the right layer. Property-level defences (checking CSS opacity, visibility, etc.) miss filter-based attacks entirely. The correct defences operate at the filter primitive level:
- Blocklist feComponentTransfer on consent ancestors: Forbid the
filterCSS property and thefilterSVG attribute on any element in the consent subtree. If the product does not require feComponentTransfer on consent elements, allow-listing is zero-cost. - If filters are required: Apply the unified detection algorithm before display. Re-apply on every MutationObserver callback targeting the filter element tree.
- Freeze filter elements: After the consent element is displayed, call
Object.freezeon the filter's DOM subtree (using ShadowDOM or a custom renderer). Prevent setAttribute calls onfeFuncXelements by intercepting the prototype. - Simulate and measure: The strongest defence is also the most robust: render the filtered consent element to an offscreen canvas and measure WCAG contrast ratio on the result. A constant-output filter will produce a uniformly colored canvas whose contrast ratio is trivially measurable. This catches all sixteen SA-CSS patterns across all four subtypes without subtype-specific rules.