Security Guide
MCP server SVG feComponentTransfer consent security — feFuncA type="discrete" tableValues="0" alpha-zeroing, feFuncA type="linear" slope="0", feFuncR/G/B slope="0" intercept="0" RGB collapse, and feFuncA type="gamma" amplitude="0"
The <feComponentTransfer> SVG filter primitive applies independent transfer functions to each color and alpha channel of its input. Each channel is controlled by a child <feFuncR>, <feFuncG>, <feFuncB>, or <feFuncA> element, each specifying a mapping from input channel values (in [0, 1]) to output values. The SVG specification defines five transfer function types: identity (no change), linear (C_out = slope × C_in + intercept), gamma (C_out = amplitude × C_in^exponent + offset), table (piecewise linear from a table of values), and discrete (step function from a table of values). All five types can be used to zero out the alpha channel or the RGB channels of a consent element, making the consent text fully transparent or invisible against common backgrounds. Four attack patterns follow.
Attack 1: feFuncA type="discrete" tableValues="0" — alpha-zero via step function (SA-CSS-FCT-001)
The discrete transfer function maps each input value to the nearest value in a table of output values, using a step function. With tableValues="0" (a single entry), the discrete function maps every input value in [0, 1] to the single output value 0. Applied to the alpha channel via feFuncA, every pixel in the consent element — regardless of its original alpha value — has its alpha set to 0. The entire consent element becomes fully transparent.
The attack is distinct from feColorMatrix alpha-row zeroing in that it uses a per-channel transfer function rather than a full 4×5 matrix multiplication. An auditor who checks only for feColorMatrix with an alpha-zero matrix row will miss this pattern.
<!-- SA-CSS-FCT-001: feFuncA type=discrete tableValues=0 — alpha-zeroing via step function
discrete tableValues=0: single step value = 0
For input alpha in [0,1]: output = tableValues[floor(input * n)] where n=1
input [0,1) → floor(input*1)=0 → tableValues[0]=0
All alpha values mapped to 0 regardless of input. Entire consent element transparent. -->
<filter id="fct-alpha-discrete">
<feComponentTransfer>
<feFuncA type="discrete" tableValues="0"/>
<!-- feFuncR, feFuncG, feFuncB not specified → implicit identity (unchanged) -->
</feComponentTransfer>
</filter>
<div id="consent" style="filter:url(#fct-alpha-discrete)">
<p>Requesting: credential access, filesystem read, network egress.</p>
<button id="allow">Allow</button>
</div>
// discrete table lookup: tableValues="0" means one step: all inputs → 0
// tableValues="0 0 0 0 0" also maps all inputs to 0 (five steps, all zero)
// tableValues="1" maps all inputs to 1 (identical — no change, but suspicious)
// tableValues="0 0" maps lower half to 0, upper half to 0 (same result)
// tableValues="0 0 0 0 0 0 0 0 0 0" (ten zeros) — same effect, harder to spot
// Detection: parse tableValues as a whitespace-separated list
// If all values are 0 (or sum is 0), flag CRITICAL for feFuncA, HIGH for feFuncR/G/B
function auditFeFuncA(funcEl) {
const type = funcEl.getAttribute('type') || 'identity';
if (type === 'discrete' || type === 'table') {
const vals = (funcEl.getAttribute('tableValues') || '').trim()
.split(/\s+/).map(Number);
const allZero = vals.every(v => v === 0);
if (allZero) return { sev: 'CRITICAL', note: `feFuncA type="${type}" tableValues all-zero → alpha=0` };
}
if (type === 'linear') {
const slope = parseFloat(funcEl.getAttribute('slope') || '1');
const intercept = parseFloat(funcEl.getAttribute('intercept') || '0');
if (slope === 0 && intercept === 0) {
return { sev: 'CRITICAL', note: 'feFuncA type=linear slope=0 intercept=0 → alpha=0 for all inputs' };
}
}
if (type === 'gamma') {
const amplitude = parseFloat(funcEl.getAttribute('amplitude') || '1');
const offset = parseFloat(funcEl.getAttribute('offset') || '0');
if (amplitude === 0 && offset === 0) {
return { sev: 'CRITICAL', note: 'feFuncA type=gamma amplitude=0 offset=0 → alpha=0 for all inputs' };
}
}
return null;
}
CRITICAL — SA-CSS-FCT-001: feFuncA type="discrete" tableValues="0" maps all alpha values to 0, making the entire consent element fully transparent. The attack uses the discrete step function rather than a matrix operation, bypassing checks specific to feColorMatrix alpha-row zeroing. Detection requires parsing the tableValues attribute and checking whether all values are zero. The same effect is achievable with any number of zero entries in tableValues.
Attack 2: feFuncA type="linear" slope="0" intercept="0" — alpha-zero via linear transfer (SA-CSS-FCT-002)
The linear transfer function applies the formula C_out = slope × C_in + intercept. With slope="0" and intercept="0", the output is 0 × C_in + 0 = 0 for all input values. Applied to the alpha channel, every pixel's alpha is set to 0 — the consent element is fully transparent, identical in effect to the discrete tableValues=0 variant but using a different SVG type attribute.
A security scanner that checks for feFuncA type="discrete" or feFuncA type="table" but not feFuncA type="linear" will miss this variant. The three types (discrete, table, linear) are semantically distinct but can all produce zero output for all inputs. An auditor must check all four non-identity transfer function types (linear, gamma, table, discrete) for alpha-zeroing conditions.
<!-- SA-CSS-FCT-002: feFuncA type=linear slope=0 intercept=0 — alpha-zero via linear formula
C_out = slope * C_in + intercept = 0 * C_in + 0 = 0 for all C_in ∈ [0,1]
Every pixel alpha → 0. Entire consent element fully transparent.
Distinct type from discrete/table — bypasses type-specific checks. -->
<filter id="fct-alpha-linear">
<feComponentTransfer>
<feFuncA type="linear" slope="0" intercept="0"/>
</feComponentTransfer>
</filter>
// Variants of linear feFuncA alpha attacks:
// slope=0 intercept=0: C_out = 0 → always transparent (CRITICAL)
// slope=0 intercept=0.1: C_out = 0.1 → near-transparent (HIGH if < 0.2)
// slope=0.1 intercept=0: C_out = 0.1*C_in → scaled down; opaque text (C_in=1) → 0.1 alpha
// slope=-1 intercept=1: C_out = 1-C_in → alpha inversion (transparent↔opaque)
// transparent background (A=0) → A=1
// opaque text (A=1) → A=0
// Consent text transparent; background opaque (inverted)
// Alpha inversion (slope=-1, intercept=1) is a particularly subtle attack:
// The element appears as a negative — background areas have a color, text areas are transparent.
// On a dark background, this renders the consent panel as a dark rectangle with transparent text shapes.
// The text shapes are readable in outline (they're transparent holes) but not as legible text.
CRITICAL — SA-CSS-FCT-002: feFuncA type="linear" slope="0" intercept="0" uses the linear transfer formula to zero all alpha values. This is the second syntax for alpha-zeroing via feComponentTransfer, distinct from the discrete and table variants. Also check for the inversion attack (slope="-1" intercept="1") which makes text transparent while making the background opaque — an unusual rendering that may appear as a styling artifact rather than a security attack.
Attack 3: feFuncR/G/B type="linear" slope="0" intercept="0" — RGB channel collapse, invisible on dark backgrounds (SA-CSS-FCT-003)
The prior two attacks zero the alpha channel, making the entire consent element transparent. The RGB-collapse variant instead zeros the R, G, and B channels while leaving the alpha channel unchanged. Applied to consent text, the text glyph pixels become rgba(0, 0, 0, 1) — fully opaque pure black. On a light background (white or near-white), black text on white is actually high-contrast and readable. On a dark background (common in modern UIs: #1a1a1a, #0f0f0f, dark-mode consent panels), fully black text on a near-black background has a contrast ratio near 1:1 — invisible.
The attack is therefore background-dependent. An auditor testing on a light background might not notice the attack; the same consent panel in a dark-mode UI or a dark-themed MCP client renders the text as transparent black on a black background. The feFuncR/G/B slope=0 pattern leaves the element's color CSS property unchanged (getComputedStyle(el).color still reports the original text color), but the filter overrides the rendered RGB values to zero.
<!-- SA-CSS-FCT-003: feFuncR/G/B slope=0 intercept=0 — zeros all RGB channels
Text pixels: rgb(17,25,39) → rgb(0,0,0) with alpha=1 (fully opaque black)
On dark background #1a1a1a = rgb(26,26,26):
WCAG contrast: (0.05 + 0.05) / (0.003 + 0.05) ≈ 1.04:1 — invisible
On light background #fff:
WCAG contrast: (1.0 + 0.05) / (0.0 + 0.05) = 21:1 — fully readable
Attack is background-dependent; testing on light background passes -->
<filter id="fct-rgb-zero">
<feComponentTransfer>
<feFuncR type="linear" slope="0" intercept="0"/>
<feFuncG type="linear" slope="0" intercept="0"/>
<feFuncB type="linear" slope="0" intercept="0"/>
<!-- feFuncA not specified → identity → alpha unchanged -->
</feComponentTransfer>
</filter>
// getComputedStyle(el).color reports original CSS value (#111827 or whatever)
// Filter overrides rendered RGB to 0 — CSSOM blind spot for filter effects
// Dark-mode detection requirement:
// Auditor must test consent rendering on both light (#fff) and dark (#0f0f0f) backgrounds
// RGB-zeroing attack is invisible in dark mode, fully readable in light mode
// SkillAudit simulates both backgrounds when checking feComponentTransfer RGB effects
// Related: feFuncR slope=0 intercept=1 sets R=1 (forces all red), feFuncG slope=0 intercept=1
// sets G=1, feFuncB slope=0 intercept=1 sets B=1 → output rgb(255,255,255) = white
// With white background: contrast 1:1 — consent text invisible (same as feFlood white)
// With dark background: contrast ≈ 16:1 — consent text readable but changed to white
HIGH — SA-CSS-FCT-003: feFuncR/G/B slope="0" intercept="0" zeroes all RGB channels, converting consent text to fully opaque black. On dark backgrounds, black text on a near-black background is invisible (contrast ≈ 1:1). On light backgrounds, the text is readable (contrast = 21:1 on white). The attack is background-dependent: a security audit that tests only on light backgrounds may pass. Detection requires checking whether the net output RGB values produce acceptable contrast on all likely background colors, including dark-mode backgrounds.
Attack 4: feFuncA type="gamma" amplitude="0" exponent="1" offset="0" — alpha-zero via gamma formula (SA-CSS-FCT-004)
The gamma transfer function applies the formula C_out = amplitude × C_in^exponent + offset. With amplitude="0" and offset="0" (any exponent), the output is 0 × C_in^exponent + 0 = 0 for all positive input values. (For C_in=0: 0^exponent = 0 for exponent>0, so output = 0 + 0 = 0 as well.) Applied to the alpha channel, the gamma formula zeros all alpha values — a third syntax for alpha-zeroing alongside the discrete and linear variants.
The gamma type is often associated with gamma correction or color grading, which may cause auditors to treat it as a benign visual styling adjustment rather than a security threat. A feFuncA type="gamma" amplitude="0.5" exponent="2.2" offset="0" is a genuine gamma correction that reduces alpha values (not to zero), while amplitude="0" exponent="1" offset="0" is an alpha-zeroing attack that happens to use the gamma syntax. Auditors must evaluate the amplitude and offset values, not just the function type.
<!-- SA-CSS-FCT-004: feFuncA type=gamma amplitude=0 — alpha-zero via gamma formula
C_out = amplitude * C_in^exponent + offset
= 0 * C_in^1 + 0
= 0
For all C_in ∈ [0,1]. Entire consent element fully transparent.
Third syntax for alpha-zeroing: distinct from discrete and linear. -->
<filter id="fct-alpha-gamma">
<feComponentTransfer>
<feFuncA type="gamma" amplitude="0" exponent="1" offset="0"/>
</feComponentTransfer>
</filter>
// Gamma formula variants:
// amplitude=0, offset=0 (any exponent): C_out = 0 → CRITICAL (alpha-zeroing)
// amplitude=1, exponent=2.2, offset=0: C_out = C_in^2.2 → darkens (gamma correction)
// For C_in=1 (opaque): C_out = 1 (still opaque) — not an attack
// For C_in=0.5: C_out ≈ 0.22 (reduced opacity)
// amplitude=0.1, exponent=1, offset=0: C_out = 0.1*C_in → near-transparent for text (C_in=1 → 0.1)
// HIGH: 10% opacity consent text — effectively invisible
// amplitude=0, exponent=1, offset=0.05: C_out = 0.05 → 5% opacity for all pixels
// HIGH: near-transparent
// feFuncA type=table tableValues=0 (also alpha-zeroing):
// table lookup: for n values v_0 through v_{n-1}
// k = input * n (then linearly interpolate between v_floor and v_ceil)
// tableValues="0" → n=1, all outputs = v_0 = 0 → alpha=0
// tableValues="0 0" → n=2, linear interp between 0 and 0 → still 0
// tableValues="0 0 0 0 0" → n=5, all outputs = 0 → alpha=0
// Any tableValues string containing only zeros → alpha-zeroing attack
CRITICAL — SA-CSS-FCT-004: feFuncA type="gamma" amplitude="0" offset="0" uses the gamma transfer formula to zero all alpha values — the third distinct syntax for alpha-zeroing in feComponentTransfer. Detection must check all four non-identity types (linear, gamma, table, discrete) for zero-output conditions on the alpha channel. A comprehensive check evaluates: linear (slope=0, intercept=0), gamma (amplitude=0, offset=0), discrete (all tableValues=0), and table (all tableValues=0). Any of these produces a fully transparent consent element.
Summary table
| Attack | Mechanism | What it hides | Severity |
|---|---|---|---|
SA-CSS-FCT-001: feFuncA type="discrete" tableValues="0" |
Discrete step function: all tableValues entries are 0; every input alpha in [0,1] maps to output alpha=0; entire consent element fully transparent; bypasses feColorMatrix alpha-row checks; tableValues with any number of all-zero entries produces the same result; getComputedStyle does not expose filter function parameters | All consent text, permission scopes, button labels rendered fully transparent; consent DOM unchanged; visual output is a blank transparent area; click events may still fire depending on pointer-events CSS | Critical |
SA-CSS-FCT-002: feFuncA type="linear" slope="0" intercept="0" |
Linear formula C_out = 0*C_in + 0 = 0 for all inputs; alpha-zeroing via linear type; distinct from discrete/table — bypasses type-specific checks; alpha inversion variant (slope=-1, intercept=1) makes text transparent and background opaque; all four non-identity types can zero alpha | Same as FCT-001: entire consent transparent; alpha inversion renders text as transparent holes in an opaque background — different visual effect but still not readable as normal consent text | Critical |
SA-CSS-FCT-003: feFuncR/G/B slope="0" intercept="0" |
Linear formula C_out=0 for R, G, B channels; alpha unchanged; text becomes fully opaque black (0,0,0,1); background-dependent attack: invisible on dark backgrounds (contrast ≈ 1:1 on #1a1a1a), fully visible on light backgrounds; getComputedStyle.color reports original value — CSSOM blind spot; intercept=1 variant forces all pixels to white → invisible on white backgrounds | Consent text invisible on dark-mode backgrounds; auditors testing on light backgrounds pass this attack; background-dependent detection required; dark-mode MCP clients are particularly vulnerable | High |
SA-CSS-FCT-004: feFuncA type="gamma" amplitude="0" offset="0" |
Gamma formula C_out = 0*C_in^exponent + 0 = 0; alpha-zeroing via gamma type; third distinct syntax alongside discrete and linear; gamma is associated with color correction (legitimate use), making amplitude=0 appear as an overly aggressive correction rather than an attack; any amplitude=0, offset=0 gamma function on alpha channel is an attack | Same as FCT-001 and FCT-002: entire consent element transparent; gamma type specifically may escape type-based detection rules that focus on discrete and linear; detection requires evaluating amplitude and offset values, not just the type attribute | Critical |
Defences
- Check all four non-identity feFuncA types for zero-output conditions — linear (slope=0, intercept=0), gamma (amplitude=0, offset=0), discrete (all tableValues=0), and table (all tableValues=0) all produce fully transparent consent elements. Detection rules that check only one type miss the other three variants.
- Zero-filter policy on alpha-modifying primitives — prohibit any
feComponentTransferwith afeFuncAchild in consent element filter chains. Legitimate consent UI rendering does not require alpha manipulation via feComponentTransfer. Flag the presence of feFuncA as CRITICAL regardless of current parameter values (dynamic activation concern). - Check RGB channel transfer functions for background-dependent invisibility — evaluate the predicted output RGB values against both light and dark backgrounds. A feFuncR/G/B that zeros RGB channels produces invisible text on dark backgrounds and readable text on light backgrounds. Audits must simulate both background colors.
- Resolve filter URLs and walk the full primitive chain —
getComputedStyle(el).filterreturns only the URL; the feFuncA parameters are inside the<feComponentTransfer>element which must be resolved viadocument.querySelector. Shadow DOM filter definitions require shadow root traversal. - Flag the alpha inversion pattern (slope=−1, intercept=1) — this makes the consent text transparent while making background pixels opaque, creating a negative image. It is not detected by zero-output checks (the intercept=1 produces non-zero output for transparent pixels) but produces an unreadable consent rendering that fails WCAG contrast requirements.
SkillAudit findings for this attack surface
feComponentTransfer with feFuncA type="discrete" tableValues="0" on the consent element — all alpha values mapped to 0 via discrete step function; entire consent element fully transparent; bypasses feColorMatrix alpha-row checks; any number of all-zero tableValues entries produces the same result; dynamic activation via tableValues mutation possible.feFuncA type="linear" slope="0" intercept="0" — linear formula C_out=0 for all alpha; second distinct syntax for alpha-zeroing; also check alpha inversion (slope=−1, intercept=1) which makes text transparent and background opaque; all four non-identity types must be evaluated.feFuncR/G/B type="linear" slope="0" intercept="0" — RGB channels zeroed; text becomes fully opaque black; invisible on dark backgrounds (contrast ≈ 1:1 on #1a1a1a); readable on light backgrounds; background-dependent attack; testing on light backgrounds produces false negative; CSSOM color property unchanged.feFuncA type="gamma" amplitude="0" exponent="1" offset="0" — gamma formula C_out=0 for all alpha; third distinct syntax for alpha-zeroing; gamma type associated with color correction may appear legitimate; detection requires evaluating amplitude and offset values, not just the type; any amplitude=0, offset=0 feFuncA is an attack regardless of exponent value.