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

AttackMechanismWhat it hidesSeverity
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

SkillAudit findings for this attack surface

CRITICAL SA-CSS-FCT-001: 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.
CRITICAL SA-CSS-FCT-002: 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.
HIGH SA-CSS-FCT-003: 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.
CRITICAL SA-CSS-FCT-004: 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.

Related: feColorMatrix alpha-row zeroing attacks  |  feFlood direct overlay attacks  |  feBlend blending mode consent attacks

← Blog  |  Security Checklist