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.
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
| ID | Channel | slope | intercept | Effect | Severity |
|---|---|---|---|---|---|
| SA-CSS-LIN-001 | A | 0 | 0 | Constant alpha=0 → transparent | Critical |
| SA-CSS-LIN-002 | R, G, B | 0 | 1 | Constant RGB=1 → white-out | Critical |
| SA-CSS-LIN-003 | R, G, B | 100 | 0 | Amplify→clamp; near-white-out via clamping | High |
| SA-CSS-LIN-004 | A (mousedown) | 1→0 | 0 | Identity→transparent at click time | Critical |
Defences
- Detect slope=0: For each
feFuncX type="linear"in a consent ancestor filter, parseslopeas a float. If |slope| ≤ 0.001, the transfer is constant-output — check intercept against erasure thresholds (near-0 for alpha, near-0 or near-1 for RGB channels). - Detect high-slope amplification: If |slope| > 20 on any RGB channel
feFuncX, evaluate slope × 1.0 + intercept (max-input output). If this value > 1 and intercept ≥ 0, the transfer amplifies non-zero inputs to the white clamp — flag as near-white-out. - Detect inversion (negative slope): If slope < −0.5 and |intercept − 1| < 0.1 on RGB channels, flag as color-inversion attack on consent readability. Evaluate post-filter WCAG contrast against the expected background color.
- MutationObserver on feFuncX slope and intercept: Runtime changes to
slopeorintercepton anyfeFuncXin a filter applied to a consent element require immediate re-evaluation. The identity initial state (slope=1, intercept=0) is specifically a mousedown-override risk pattern.
SkillAudit findings for this attack surface
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.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).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.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.