Security Guide

MCP server SVG feFuncG consent security — slope="0" intercept="1" green-channel max-force, intercept="0" magenta shift, type="discrete" tableValues="1" step function, and JS mousedown injection

The <feFuncG> element inside <feComponentTransfer> defines the transfer function for the green channel only, leaving red, blue, and alpha at identity unless explicitly overridden. Green is the channel with the greatest weight in the human luminance response — the eye is most sensitive to green light. Forcing green to maximum or zero therefore produces the most dramatic perceived color shift of any single channel. A consent element with feFuncG intercept=1 renders text at near-white luminance levels, which is invisible on light or green-tinted backgrounds. A consent element with feFuncG intercept=0 produces magenta text (R+B without G), which is invisible on purple, violet, or pink backgrounds. Both effects bypass getComputedStyle inspection because the CSS color property is not modified. Four attack patterns follow.

Attack 1: feFuncG slope="0" intercept="1" — green channel forced to maximum, high-luminance text invisible on bright backgrounds (SA-CSS-FCG-001)

The linear formula C_out = 0 × C_in + 1 = 1 forces every pixel's green component to maximum. For dark text such as rgb(17, 24, 39), the result is rgb(17, 255, 39) — a bright, highly saturated lime-green. On a lime or green-themed background, this text is invisible: both text and background have G=1 (maximum green), and the relatively small difference in R and B channels produces very low contrast.

Beyond pure green backgrounds, the attack is also effective on backgrounds where luminance is dominated by the green channel — near-white backgrounds (white = R=1, G=1, B=1) and yellow backgrounds (yellow = R=1, G=1, B=0) both have G=max, and text forced to G=max blends into both. Success-state UI panels (green banners, "approved" checkboxes, "success" notification bars) are a particularly relevant attack surface since they typically have high-G backgrounds and are commonly used for permission grants.

<!-- SA-CSS-FCG-001: feFuncG type=linear slope=0 intercept=1 — green channel forced to max
     C_out = 0 * C_in + 1 = 1 for all C_in ∈ [0,1]
     Text rgb(17,24,39) → rgb(17,255,39) — bright lime-green
     On success-green bg rgb(22,163,74) (Tailwind green-600):
       High G component on both sides → low contrast -->
<filter id="fcg-g-max">
  <feComponentTransfer>
    <feFuncG type="linear" slope="0" intercept="1"/>
    <!-- feFuncR, feFuncB: identity by default — R and B unchanged -->
  </feComponentTransfer>
</filter>

<div id="consent" style="filter:url(#fcg-g-max)">
  <p>Requesting: credential access, filesystem read, network egress.</p>
  <button id="allow">Allow</button>
</div>

// Color analysis after feFuncG slope=0 intercept=1:
// Original text: rgb(17, 24, 39) → filtered: rgb(17, 255, 39)
// Background #16a34a = rgb(22,163,74): luminance ≈ 0.35
// Text rgb(17,255,39): luminance ≈ 0.87 → contrast ≈ (0.87+0.05)/(0.35+0.05) ≈ 2.3:1
//   Below WCAG AA 4.5:1 threshold but still somewhat distinguishable
// Background #4ade80 = rgb(74,222,128): luminance ≈ 0.65
//   Text luminance ≈ 0.87 → contrast ≈ (0.92)/(0.70) ≈ 1.3:1 — very low contrast
// Background #ffffff = white: luminance 1.0
//   Text luminance 0.87 → contrast ≈ (1.05)/(0.92) ≈ 1.14:1 — nearly invisible on white

// Note: white text (rgb(255,255,255)) → after filter: rgb(255,255,255) — unchanged (already G=1)
// The attack is most effective for text with originally-low G values (dark gray, dark blue)

function auditFeFuncG(funcEl) {
  const type      = funcEl.getAttribute('type') || 'identity';
  const slope     = parseFloat(funcEl.getAttribute('slope')     || '1');
  const intercept = parseFloat(funcEl.getAttribute('intercept') || '0');
  if (type === 'linear' && slope === 0) {
    if (intercept >= 0.9) return { sev: 'HIGH', note: `feFuncG slope=0 intercept=${intercept}: G→max, invisible on green/bright backgrounds` };
    if (intercept === 0)   return { sev: 'HIGH', note: 'feFuncG slope=0 intercept=0: G→0, text becomes magenta, invisible on pink/purple' };
  }
  return null;
}

HIGH — SA-CSS-FCG-001: feFuncG slope="0" intercept="1" forces the green channel to maximum, producing bright lime-green text. On success-state or green-branded backgrounds, and on near-white or yellow backgrounds (all of which have high G), the contrast drops below the WCAG AA 4.5:1 threshold. An auditor testing on a dark background (where lime-green text is visually striking) misses the attack; testing on light or green-tinted backgrounds reveals it.

Attack 2: feFuncG slope="0" intercept="0" — green channel zeroed, magenta text invisible on purple/pink backgrounds (SA-CSS-FCG-002)

With slope="0" and intercept="0", the linear formula yields C_out = 0 for every green input. Text that was dark gray (rgb(17, 24, 39)) becomes rgb(17, 0, 39) — deep magenta (a reddish-purple color with R and B channels present but no green). On purple, violet, or pink backgrounds — which also have low or absent green components — the magenta text has minimal contrast and is invisible.

Green removal is the most luminance-impactful single-channel zeroing attack because green contributes approximately 59% of perceived luminance in the standard luminance formula (L = 0.2126R + 0.7152G + 0.0722B). Zeroing the green channel reduces perceived luminance by approximately 72% for a neutral gray text. The resulting text is very dark — near-black — which is invisible on dark backgrounds. On dark-mode purple UIs (very common in developer tools, terminal apps, and AI tool interfaces), this dark magenta text may be entirely invisible against a dark violet background.

<!-- SA-CSS-FCG-002: feFuncG type=linear slope=0 intercept=0 — green channel zeroed
     Text rgb(17,24,39) → rgb(17,0,39) — deep dark magenta
     Luminance: 0.2126*17/255 + 0.7152*0/255 + 0.0722*39/255 ≈ 0.003 (very dark)
     On dark violet bg rgb(76,29,149) (Tailwind violet-800): luminance ≈ 0.018
       contrast ≈ (0.018+0.05)/(0.003+0.05) ≈ 1.3:1 — invisible -->
<filter id="fcg-g-zero">
  <feComponentTransfer>
    <feFuncG type="linear" slope="0" intercept="0"/>
  </feComponentTransfer>
</filter>

// Luminance formula weight: G contributes 0.7152 of perceived luminance
// Zeroing G reduces luminance of rgb(17,24,39) from:
//   L = 0.2126*(17/255) + 0.7152*(24/255) + 0.0722*(39/255) ≈ 0.009
//   to: L = 0.2126*(17/255) + 0 + 0.0722*(39/255) ≈ 0.003
// Text becomes very dark; invisible on similarly-dark violet/purple backgrounds

// Dark-mode purple theme backgrounds:
// rgb(88, 28, 135) (Tailwind purple-800): L≈0.024 → contrast vs text ≈ 1.5:1
// rgb(46, 16, 101) (darker purple): L≈0.006 → contrast vs text ≈ 1.1:1 — invisible

// White text rgb(255,255,255) → after filter: rgb(255,0,255) = magenta
// On magenta background #ff00ff: contrast = 1:1 — perfectly invisible
// On purple bg #800080 = rgb(128,0,128): text magenta L≈0.29, bg L≈0.06 → contrast ≈ 5.8:1
//   Actually readable here — attack works for originally-dark text, not white text on purple

HIGH — SA-CSS-FCG-002: feFuncG slope="0" intercept="0" zeros the green channel, collapsing perceived luminance by ~72% and shifting text to deep magenta. On dark-mode purple or violet backgrounds — common in developer tools and AI client interfaces — the resulting near-black magenta text is invisible. Detection requires evaluating the luminance impact of the transfer function on the text color and comparing against the actual background color, not just checking for alpha-zeroing patterns.

Attack 3: feFuncG type="discrete" tableValues="1" — G=max via step function, bypasses linear-type-specific checks (SA-CSS-FCG-003)

The discrete transfer function maps each input to the nearest value in a step table. With tableValues="1" (a single entry), the discrete function maps every input value in [0, 1] to the single output value 1. The result is identical to type="linear" slope="0" intercept="1" — all green channel values are forced to maximum — but via a completely different SVG type attribute and syntax.

A security scanner that checks for feFuncG type="linear" slope="0" but not for type="discrete" or type="table" misses this variant. The discrete type is more commonly used for posterization effects, making a tableValues="1" entry less suspicious on first glance than a slope="0" linear function. Multiple tableValues entries can also achieve forced G=max: tableValues="1 1 1 1 1" (five entries all one), tableValues="0.99 1" (near-max for lower inputs, max for upper), etc.

<!-- SA-CSS-FCG-003: feFuncG type=discrete tableValues=1 — G=max via step function
     Discrete step lookup: for single entry [1], all inputs → output[0] = 1
     tableValues="1": n=1 → floor(C_in*1)=0 for all C_in ∈ [0,1) → tableValues[0]=1
     Every pixel's green channel → 1 (maximum green)
     Identical effect to slope=0 intercept=1 but different SVG type attribute -->
<filter id="fcg-g-discrete">
  <feComponentTransfer>
    <feFuncG type="discrete" tableValues="1"/>
  </feComponentTransfer>
</filter>

// Also achievable via:
// type="discrete" tableValues="1 1" — two steps both mapping to 1
// type="discrete" tableValues="1 1 1 1 1" — five steps all 1 (looks like posterization)
// type="table" tableValues="1 1 1 1 1" — piecewise linear between all-1 values → always 1
// type="gamma" amplitude=5 exponent=0.1 — near-max for nearly all inputs (as with feFuncR)
// type="table" tableValues="0.95 0.98 1 1 1" — near-1 for all values (not exactly 1, but ≥0.95)

function auditFeFuncGDiscrete(funcEl) {
  const type = funcEl.getAttribute('type') || 'identity';
  if (type === 'discrete' || type === 'table') {
    const vals = (funcEl.getAttribute('tableValues') || '').trim()
                  .split(/\s+/).map(Number);
    const allNearMax = vals.every(v => v >= 0.9);
    if (allNearMax && vals.length > 0) {
      return { sev: 'HIGH', note: `feFuncG type="${type}" tableValues all ≥0.9: G→max on all pixels` };
    }
    const allZero = vals.every(v => v === 0);
    if (allZero) {
      return { sev: 'HIGH', note: `feFuncG type="${type}" tableValues all-zero: G=0, text shifts to magenta` };
    }
  }
  return null;
}

HIGH — SA-CSS-FCG-003: feFuncG type="discrete" tableValues="1" forces green to maximum via the step function. The effect is identical to slope=0 intercept=1 but uses a different SVG type. Scanners checking only for type="linear" slope="0" miss this. Comprehensive audits must check all five transfer function types (identity, linear, gamma, table, discrete) for near-maximum output on each channel.

Attack 4: JS mousedown injects feFuncG intercept="1" before click fires (SA-CSS-FCG-004)

A capture-phase mousedown listener modifies the <feFuncG> element's intercept attribute just before the click event fires. Initially, the filter may have intercept="0" (green unchanged), making the page pass a static audit. When the user's mouse button depresses over the consent button, the JS handler sets intercept="1" — the consent text immediately shifts to lime-green. If the button's active state or hover state renders a green background (common in confirmation dialogs and "approve" flows), the text becomes invisible at the exact moment of the click.

This dynamic variant is particularly dangerous in combination with CSS :active states: many consent button designs highlight the button in green on active to indicate acceptance. The feFuncG injection aligns with this active-state color change, making the text-to-background color shift appear visually coherent while hiding the consent label under the lime-green filter just as the user confirms.

<!-- SA-CSS-FCG-004: JS mousedown injects feFuncG intercept=1 before click
     Filter element starts with intercept=0 (G unchanged)
     On mousedown: intercept changed to 1 (G→max)
     Click fires: consent text lime-green, invisible against green active-state bg -->
<filter id="fcg-dynamic">
  <feComponentTransfer>
    <feFuncG id="fcg-funcg" type="linear" slope="0" intercept="0"/>
  </feComponentTransfer>
</filter>
<button id="allow" style="filter:url(#fcg-dynamic)">Allow</button>
<script>
document.getElementById('allow').addEventListener('mousedown', () => {
  document.getElementById('fcg-funcg').setAttribute('intercept', '1');
}, { capture: true });
</script>

// Detection: MutationObserver on feFuncG element
const funcG = document.getElementById('fcg-funcg');
const obs = new MutationObserver(muts => {
  for (const m of muts) {
    if (m.attributeName) {
      const intercept = parseFloat(funcG.getAttribute('intercept') || '0');
      const slope     = parseFloat(funcG.getAttribute('slope')     || '1');
      if (slope === 0 && intercept >= 0.9) {
        console.warn('[SA-CSS-FCG-004] feFuncG modified at mousedown: G→max');
      }
    }
  }
});
obs.observe(funcG, { attributes: true, attributeFilter: ['type', 'slope', 'intercept', 'tableValues', 'amplitude', 'exponent', 'offset'] });

// Also cover: JS that removes the original feFuncG and appends a new one with intercept=1
// (MutationObserver on feComponentTransfer parent for childList changes)
const ct = funcG.closest('feComponentTransfer');
new MutationObserver(muts => {
  for (const m of muts) {
    for (const node of m.addedNodes) {
      if (node.tagName && node.tagName.toLowerCase() === 'fefuncg') {
        const i = parseFloat(node.getAttribute('intercept') || '0');
        if (i >= 0.9) console.warn('[SA-CSS-FCG-004] New feFuncG appended with G→max');
      }
    }
  }
}).observe(ct, { childList: true });

HIGH — SA-CSS-FCG-004: JS mousedown injection of feFuncG intercept="1" shifts consent text to lime-green just as the user clicks. Combined with a green button active state, the text matches the background at the moment of click. Detection requires installing MutationObservers on all <feFuncG> elements and their parent <feComponentTransfer> containers before any user interaction events are registered, and treating intercept or tableValues changes during mousedown as suspicious.

Summary table

AttackMechanismBackground required for invisibilitySeverity
SA-CSS-FCG-001: feFuncG slope="0" intercept="1" Linear formula forces G=1 on all pixels; text becomes bright lime-green; high luminance (G has 71.5% weight in luminance); invisible on green, lime, near-white, and yellow backgrounds where G is also near-maximum; getComputedStyle.color blind spot; applies to any SVG filter on a consent element Green, lime, yellow, or near-white backgrounds; success/approved-state UI banners; green-branded MCP client themes High
SA-CSS-FCG-002: feFuncG slope="0" intercept="0" Linear formula zeros G channel; luminance drops ~72%; text becomes dark magenta (R+B only); invisible on dark-mode purple/violet/magenta backgrounds common in developer tools and AI client interfaces; getComputedStyle.color unchanged (CSSOM blind spot) Dark violet, purple, or magenta backgrounds; dark-mode developer UIs with violet/purple accent colors; AI client interfaces with purple branding High
SA-CSS-FCG-003: feFuncG type="discrete" tableValues="1" Discrete step function maps all inputs to single value 1; identical effect to slope=0 intercept=1 but different type attribute; bypasses linear-type-specific checks; also achievable via tableValues="1 1 1", type=table all-ones, extreme gamma; all five transfer function types must be checked Same as FCG-001 (green/bright backgrounds); net effect functionally equivalent to slope=0 intercept=1 for all input values High
SA-CSS-FCG-004: JS mousedown injection Intercept changed from 0 to 1 via JS at mousedown (capture phase); attack window ~50ms between mousedown and click; combined with CSS :active green button background creates visually coherent camouflage; initial page state correct; MutationObserver required on feFuncG element and feComponentTransfer parent Active/hover states with green backgrounds; confirmation buttons with green ":active" highlighting; success-state consent panels High

Defences

SkillAudit findings for this attack surface

HIGH SA-CSS-FCG-001: feFuncG slope="0" intercept="1" on consent element — green channel forced to max; text becomes lime-green; invisible on green/bright/near-white backgrounds; getComputedStyle.color blind spot.
HIGH SA-CSS-FCG-002: feFuncG slope="0" intercept="0" — green channel zeroed; luminance drops 72%; text becomes dark magenta; invisible on dark-mode violet/purple backgrounds common in AI client UIs.
HIGH SA-CSS-FCG-003: feFuncG type="discrete" tableValues="1" — G=max via step function; bypasses linear-type checks; also via table all-ones or extreme gamma; all five transfer types must be evaluated.
HIGH SA-CSS-FCG-004: JS mousedown feFuncG intercept="1" injection — text shifts to lime-green during click; combined with green :active button state; MutationObserver on feFuncG and feComponentTransfer parent required.

Related: feComponentTransfer overview (all channels)  |  feFuncR red channel attacks  |  feFuncB blue channel attacks  |  feFuncA alpha channel attacks

← Blog  |  Security Checklist