← Blog

MCP server SVG feComponentTransfer linear consent security

Four SA-CSS-LIN consent attack patterns using feComponentTransfer type="linear" — the slope/intercept transfer function C' = slope × C + intercept exploited to erase alpha, produce white-out, amplify channels to the clamp boundary, and invert pixel values to destroy consent text readability.

By SkillAudit  |  23 Oct 2026

The feComponentTransfer SVG filter primitive supports four per-channel transfer function subtypes: gamma, table, discrete, and linear. The type=linear subtype applies the formula C' = slope × C + intercept (SVG §15.10). Default values are slope=1, intercept=0, which produce the identity function (C' = C). The output is clamped to [0, 1] after the linear calculation.

The linear subtype is the most analytically transparent of the four feComponentTransfer subtypes — the constant-output condition (slope=0) is directly visible in a single attribute. Yet it remains an active consent attack vector because: most consent element analyzers check for specific dangerous attribute values rather than performing transfer-function output analysis; high-slope amplification attacks (slope=100) are not technically constant-output but produce practically constant clamped output for nearly all consent text pixels; and JS mousedown overrides of the slope attribute bypass all static analysis.

Constant-output linear transfer: When slope=0, the formula reduces to C' = 0 × C + intercept = intercept — the output is exactly intercept regardless of input. This is the canonical constant-output condition for type=linear. Detection: check the slope attribute equals 0 (or is within 0.001 of 0) on any feFuncX within a filter applied to a consent ancestor element.

SA-CSS-LIN-001: feFuncA slope="0" intercept="0" (alpha erasure)

With slope=0 and intercept=0, the linear transfer maps all alpha values to 0. The element becomes fully transparent. The slope=0 flag is explicit in the attribute and this is the most easily detected SA-CSS-LIN pattern. It remains in active use because: many SVG validators check only for attribute value ranges (0 is within the legal range for slope); per-element checking (e.g., only checking feFuncA, not feFuncR/feFuncG/feFuncB) may be implemented inconsistently; and cross-type attackers mix this with other subtypes on other channels to fragment per-subtype detection.

<filter id="lin1">
  <feComponentTransfer>
    <feFuncA type="linear" slope="0" intercept="0"/>
  </feComponentTransfer>
</filter>
<!-- C' = 0×C + 0 = 0; all alpha → 0; element fully transparent -->

This pattern is equivalent in effect to SA-CSS-GAM-002 (gamma with amplitude=0, offset=0) but uses a completely different syntactic encoding. An analyzer checking only gamma amplitude will miss this.

SA-CSS-LIN-002: feFuncR/feFuncG/feFuncB slope="0" intercept="1" (white-out)

Applied to all three RGB channels, slope=0 + intercept=1 maps every channel value to 1. All RGB channels become 1.0 — pure white on every pixel. The element is a white rectangle, invisible against white or light backgrounds. The attribute values (slope=0, intercept=1) are both individually valid: slope=0 is not negative, intercept=1 is within [0,1]. An attribute-range validator will not flag either value.

<filter id="lin2">
  <feComponentTransfer>
    <feFuncR type="linear" slope="0" intercept="1"/>
    <feFuncG type="linear" slope="0" intercept="1"/>
    <feFuncB type="linear" slope="0" intercept="1"/>
  </feComponentTransfer>
</filter>
<!-- C' = 0×C + 1 = 1; all RGB → 1.0; white-out; alpha unchanged -->

Combined with SA-CSS-LIN-001 on feFuncA (making the element also transparent): a single feComponentTransfer that sets all four channels constant produces both white-out and transparent on the same element simultaneously. The fully transparent white element is invisible in all rendering contexts.

SA-CSS-LIN-003: slope="100" intercept="0" (amplify-to-clamp near-white-out)

With slope=100 and intercept=0, the formula is C' = 100 × C, clamped to [0, 1]. For any input C > 0.01: output = min(100 × C, 1) = 1. For consent text anti-aliased pixels, the alpha-blended foreground color channel values range from approximately 0.05 (very light anti-alias edge) to 1.0 (fully opaque foreground pixel). All of these map to 1.0 after amplification and clamping. The only pixels that do not map to 1.0 are those with C ≤ 0.01 — essentially the fully-transparent background pixels, which are irrelevant to consent readability.

<filter id="lin3">
  <feComponentTransfer>
    <feFuncR type="linear" slope="100" intercept="0"/>
    <feFuncG type="linear" slope="100" intercept="0"/>
    <feFuncB type="linear" slope="100" intercept="0"/>
  </feComponentTransfer>
</filter>
<!-- C' = 100×C, clamped; any C > 0.01 → 1.0; all non-background text pixels → white -->

Detection gap: SA-CSS-LIN-003 does not set slope=0, so analyzers checking for slope=0 will not flag it. The attack requires high-slope analysis: evaluate whether slope × max_representativeInput + intercept > 1. For RGB channels in consent text, max_representativeInput ≈ 1 (opaque foreground pixel), so the threshold is slope > (1 − intercept). With intercept=0, any slope > 1 satisfies this. A threshold of slope > 20 is a practical detection point for near-all-input amplification.

SA-CSS-LIN-004: JS mousedown sets feFuncA slope 1 → 0 (click-time alpha erasure)

The initial state is slope=1, intercept=0 — the exact linear identity. The element renders normally. On mousedown, the script sets slope to 0 (and optionally sets intercept to 0 as well, making both explicit). The linear transfer immediately becomes constant-zero on the alpha channel. The element disappears at click time.

consentElement.addEventListener("mousedown", () => {
  const fa = document.getElementById("consentFilter")
              .querySelector("feFuncA[type='linear']");
  fa.setAttribute("slope", "0");     // constant-output: 0×C + 0 = 0
  fa.setAttribute("intercept", "0"); // explicit redundancy
  // element immediately transparent; click proceeds on now-invisible element
});

Static analysis will pass this because the initial slope=1+intercept=0 is the exact identity function. The attack is only detectable via MutationObserver on the slope and intercept attributes of any feFuncX element within a filter applied to a consent ancestor.

Attack summary

IDChannelslopeinterceptEffectSeverity
SA-CSS-LIN-001A00Constant alpha=0 → transparentCritical
SA-CSS-LIN-002R, G, B01Constant RGB=1 → white-outCritical
SA-CSS-LIN-003R, G, B1000Amplify→clamp; near-white-out via clampingHigh
SA-CSS-LIN-004A (mousedown)1→00Identity→transparent at click timeCritical

Defences

SkillAudit findings for this attack surface

CRITICAL SA-CSS-LIN-001: feFuncA type="linear" slope="0" intercept="0" — constant-output alpha=0; C'=slope×C+intercept=0; element fully transparent; slope=0 is the canonical constant flag; detection: slope attribute within 0.001 of zero on feFuncA in consent ancestor filter.
CRITICAL SA-CSS-LIN-002: feFuncR/feFuncG/feFuncB type="linear" slope="0" intercept="1" — constant-output RGB=1; all channels map to 1 regardless of input; white-out; attribute values individually in-range; constant-output analysis required (slope=0 check + intercept threshold).
HIGH SA-CSS-LIN-003: feFuncR/feFuncG/feFuncB type="linear" slope="100" intercept="0" — amplify-to-clamp; C'=100×C clamped to 1; any C>0.01 (all non-transparent consent pixels) → 1.0; near-white-out via amplification; slope≠0 evades constant-output check; high-slope output-range analysis required.
CRITICAL SA-CSS-LIN-004: JS mousedown sets feFuncA slope from 1 (identity) to 0 — initial state linear identity; click-time alpha erasure; MutationObserver on feFuncA slope and intercept attributes in consent ancestor filters required.

Related: feComponentTransfer type=gamma attacks  |  feComponentTransfer type=table attacks  |  feComponentTransfer type=discrete attacks  |  feComponentTransfer unified synthesis

← Blog  |  Security Checklist