Security Guide

MCP server SVG feFuncB consent security — slope="0" intercept="1" blue-channel max-force, intercept="0" yellow shift, type="gamma" amplitude="0" offset="1" gamma offset, and JS mousedown injection

The <feFuncB> element inside <feComponentTransfer> defines the transfer function for the blue channel only, leaving red, green, and alpha at identity unless explicitly overridden. The blue channel contributes approximately 7.2% of perceived luminance (L = 0.2126R + 0.7152G + 0.0722B), the smallest of the three channels. Because of this, blue-channel manipulation produces the most subtle color shift — consent text remains at nearly the same brightness but drifts toward blue (if B is maximized) or toward dirty yellow-brown (if B is zeroed). The attack surface is therefore mostly about color matching against blue-dominant or blue-absent backgrounds rather than luminance collapse. The gamma formula adds a third distinct syntax for forcing B=1 via the offset parameter instead of the intercept, creating a variant that bypasses checks specific to linear-type intercept attacks. Four attack patterns follow.

Attack 1: feFuncB slope="0" intercept="1" — blue channel forced to maximum, invisible on blue/purple/indigo backgrounds (SA-CSS-FCB-001)

The linear formula C_out = 0 × C_in + 1 = 1 forces every pixel's blue component to maximum. For dark text such as rgb(17, 24, 39), the result is rgb(17, 24, 255) — a rich blue with the original low-level red and green components preserved. On blue, indigo, or purple backgrounds with high B values, this blue-shifted text is invisible: the dominant blue channel creates near-identical perceived colors for text and background.

Blue-dominant backgrounds are extremely common in developer tooling, SaaS products, and AI client interfaces. The default link color in most browsers is blue (rgb(0, 0, 238)). Many MCP client themes and IDE extensions use blue-accented consent panels. The attack is effective against any interface where the consent panel uses a blue primary button color — the text color shifts to match the button background, and consent text becomes unreadable against the blue button.

<!-- SA-CSS-FCB-001: feFuncB type=linear slope=0 intercept=1 — blue channel forced to max
     C_out = 0 * C_in + 1 = 1 for all C_in ∈ [0,1]
     Text rgb(17,24,39) → rgb(17,24,255) — vivid blue
     On blue bg rgb(37,99,235) (Tailwind blue-600):
       Text: B=1 (max), bg: B≈0.92 → R/G delta provides some contrast but B dominates -->
<filter id="fcb-b-max">
  <feComponentTransfer>
    <feFuncB type="linear" slope="0" intercept="1"/>
    <!-- feFuncR, feFuncG: identity by default -->
  </feComponentTransfer>
</filter>

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

// Color analysis:
// Text after filter: rgb(17, 24, 255) — vivid blue
// Background #1d4ed8 = rgb(29,78,216): luminance ≈ 0.074
// Text rgb(17,24,255): luminance = 0.2126*(17/255) + 0.7152*(24/255) + 0.0722*(255/255)
//   ≈ 0.014 + 0.067 + 0.0722 ≈ 0.153
// Contrast: (0.153+0.05)/(0.074+0.05) ≈ 1.6:1 — very low
// Background #3b82f6 = rgb(59,130,246) (Tailwind blue-500): luminance ≈ 0.21
//   Contrast: (0.203)/(0.263) — text and bg swap luminance order; ≈ 1.3:1 — essentially invisible
// Background #dbeafe = rgb(219,234,254) (Tailwind blue-100): luminance ≈ 0.84
//   Text luminance ≈ 0.153 → contrast ≈ 5.9:1 — still readable on light blue
//   Attack fails on very light blue backgrounds

function auditFeFuncB(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: `feFuncB slope=0 intercept=${intercept}: B→max, invisible on blue/purple backgrounds` };
    if (intercept === 0)   return { sev: 'HIGH', note: 'feFuncB slope=0 intercept=0: B=0, text shifts to yellow-brown, invisible on yellow' };
  }
  return null;
}

HIGH — SA-CSS-FCB-001: feFuncB slope="0" intercept="1" forces all blue channel values to maximum, producing vivid blue text. On medium-to-dark blue backgrounds — the dominant color in developer tools, SaaS products, and blue-themed AI client consent panels — contrast drops to approximately 1.3:1–1.6:1. Testing only on white or dark backgrounds misses this attack; evaluation requires testing against the actual rendered background color of the consent container.

Attack 2: feFuncB slope="0" intercept="0" — blue channel zeroed, yellow-brown shift invisible on yellow/amber backgrounds (SA-CSS-FCB-002)

With slope="0" and intercept="0", the blue channel is zeroed for all pixels. Text that was dark gray (rgb(17, 24, 39)) becomes rgb(17, 24, 0) — a dark olive or brownish-green. The blue contribution to the text luminance is only about 7.2% (0.0722 × 39/255 ≈ 0.011), so the luminance change is minimal and the text remains very dark. The color shift toward olive or yellow-brown creates conditions for near-invisible text on yellow or amber backgrounds, where the R and G channels dominate.

Yellow warning banners and amber "caution" consent panels are common in security-critical MCP implementations that want to draw attention to potentially dangerous operations. Ironically, these same backgrounds are where the feFuncB zeroing attack is most effective — the text shifts toward yellow-brown and blends into the amber warning background. An auditor who sees the "danger" or "caution" styling of the panel may not notice that the actual consent text has become invisible against that background.

<!-- SA-CSS-FCB-002: feFuncB type=linear slope=0 intercept=0 — blue channel zeroed
     Text rgb(17,24,39) → rgb(17,24,0) — dark olive/brownish
     Luminance change minimal: 0.0722*(39/255)→0 ≈ 0.011 reduction
     On amber bg rgb(217,119,6) (Tailwind amber-600):
       Text: R=17,G=24,B=0 — very dark (luminance≈0.007)
       Bg: R=217,G=119,B=6 — medium-bright (luminance≈0.19)
       Contrast ≈ (0.19+0.05)/(0.007+0.05) ≈ 4.2:1 — close to but below 4.5:1 AA
     On amber rgb(245,158,11) (Tailwind amber-500): luminance≈0.37
       Contrast ≈ (0.42)/(0.057) ≈ 7.4:1 — readable (attack fails on bright amber)
     Most effective: yellow/amber panels with medium-depth backgrounds -->
<filter id="fcb-b-zero">
  <feComponentTransfer>
    <feFuncB type="linear" slope="0" intercept="0"/>
  </feComponentTransfer>
</filter>

// For originally-light text (white → after filter: rgb(255,255,0) = pure yellow):
// On yellow bg #ffff00: contrast = 1:1 — invisible
// On bright yellow #fef08a = rgb(254,240,138): text rgb(255,255,0) vs bg (254,240,138)
//   luminance text≈0.93, bg≈0.87 → contrast ≈ 1.07:1 — invisible
// The zero-blue attack is most devastating for originally-white consent text

// Dark text on yellow: text (17,24,0) vs bg (255,255,0)
//   luminance text≈0.007, bg≈0.93 → contrast ≈ (0.98)/(0.057) ≈ 17:1 — readable
// Caveat: for dark text on yellow the attack actually keeps high contrast — it targets light text

HIGH — SA-CSS-FCB-002: feFuncB slope="0" intercept="0" zeros the blue channel. For light or white consent text, the text shifts to pure yellow (rgb(255, 255, 0)) which is invisible on yellow backgrounds. For dark text on amber backgrounds, contrast drops below the WCAG AA 4.5:1 threshold on medium-depth amber. The attack is most effective for consent panels using light-colored text on yellow or amber warning backgrounds.

Attack 3: feFuncB type="gamma" amplitude="0" exponent="1" offset="1" — blue=1 via gamma offset parameter, third syntax for channel maximization (SA-CSS-FCB-003)

The gamma transfer function computes C_out = amplitude × C_in^exponent + offset. With amplitude="0", the first term evaluates to zero regardless of input: 0 × C_in^1 = 0. Adding offset="1" gives C_out = 0 + 1 = 1 for every input value. Every pixel's blue channel is forced to maximum — functionally identical to type="linear" slope="0" intercept="1" — but using the gamma formula's offset parameter rather than the linear formula's intercept.

This is the third distinct syntactic path to forcing B=1 after the linear intercept and discrete tableValues variants. The gamma offset is semantically different from the linear intercept: in color science, gamma offsets are associated with black-level lift (preventing true black), which is a legitimate color grading operation. A value of offset="1" means maximum black lift — an unusual but not immediately suspicious-looking value to a surface-level auditor who associates gamma parameters with color correction rather than channel forcing.

<!-- SA-CSS-FCB-003: feFuncB type=gamma amplitude=0 exponent=1 offset=1 — B=1 via gamma offset
     C_out = amplitude * C_in^exponent + offset
           = 0         * C_in^1        + 1
           = 1 for all C_in ∈ [0,1]
     Functionally identical to slope=0 intercept=1 but using gamma formula offset -->
<filter id="fcb-b-gamma-offset">
  <feComponentTransfer>
    <feFuncB type="gamma" amplitude="0" exponent="1" offset="1"/>
  </feComponentTransfer>
</filter>

// The gamma formula for B=max has multiple expression paths:
// 1. type=linear slope=0 intercept=1
// 2. type=discrete tableValues=1
// 3. type=table tableValues=1 1 1 1 1
// 4. type=gamma amplitude=0 exponent=1 offset=1  (this attack)
// 5. type=gamma amplitude=100 exponent=0.001 offset=0
//    → C_out = 100 * C_in^0.001 ≈ 100 * 1 = 100 → clamped to 1 for C_in>0
//    → C_in=0: 100*0^0.001 = 0 (0^anything = 0 for positive exponent)
//    → Only pure black stays at B=0; all other inputs → B=1 (same as FCB-001/003 for non-black)

// Detection: evaluate gamma formula at multiple sample points
function auditFeFuncBGamma(funcEl) {
  if (funcEl.getAttribute('type') !== 'gamma') return null;
  const amplitude = parseFloat(funcEl.getAttribute('amplitude') || '1');
  const exponent  = parseFloat(funcEl.getAttribute('exponent')  || '1');
  const offset    = parseFloat(funcEl.getAttribute('offset')    || '0');
  const samples   = [0, 0.01, 0.1, 0.3, 0.5, 0.8, 1.0];
  let allNearMax = true;
  let allZero    = true;
  for (const cin of samples) {
    const cout = Math.min(1, Math.max(0, amplitude * Math.pow(cin, exponent) + offset));
    if (cout < 0.9) allNearMax = false;
    if (cout > 0.1) allZero    = false;
  }
  if (allNearMax) return { sev: 'HIGH', note: `feFuncB type=gamma amplitude=${amplitude} exponent=${exponent} offset=${offset}: B→max for all inputs` };
  if (allZero)    return { sev: 'HIGH', note: `feFuncB type=gamma: B→0 for all inputs, text becomes yellow-brown` };
  return null;
}

HIGH — SA-CSS-FCB-003: feFuncB type="gamma" amplitude="0" exponent="1" offset="1" forces B=1 for all inputs via the gamma formula's offset parameter. This is the third distinct SVG syntax for blue-channel maximization alongside the linear intercept and discrete tableValues variants. Detection requires evaluating the formula at sample input values rather than checking parameter names — offset="1" on a gamma function signals this attack.

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

A capture-phase mousedown listener modifies the <feFuncB> element's intercept attribute from "0" to "1" just before the click fires. Initially, the filter is identity-like (B unchanged), passing a static audit. When the user's mouse button depresses, consent text immediately shifts to blue-dominant. If the button's active-state background is blue (extremely common — most primary action buttons use blue active/focus states), the consent text matches the background at the moment of click completion.

The blue channel is a particularly effective target for active-state injection because blue is the near-universal primary action color across UI frameworks (Bootstrap, Material, Tailwind all default to blue primary). The :active state of "Allow" or "Confirm" buttons typically keeps or deepens the blue background. Injecting B=max at mousedown ensures the text matches the active-state blue at precisely the moment the user confirms the action.

<!-- SA-CSS-FCB-004: JS mousedown injects feFuncB intercept=1 before click
     Filter starts with intercept=0 (B unchanged, passes static audit)
     On mousedown: intercept → 1 (B forced to max, text turns blue-dominant)
     Click fires: text blue, matches blue :active button background -->
<filter id="fcb-dynamic">
  <feComponentTransfer>
    <feFuncB id="fcb-funcb" type="linear" slope="0" intercept="0"/>
  </feComponentTransfer>
</filter>
<button id="allow" style="filter:url(#fcb-dynamic)">Allow</button>
<script>
document.getElementById('allow').addEventListener('mousedown', () => {
  document.getElementById('fcb-funcb').setAttribute('intercept', '1');
}, { capture: true });
</script>

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

// Also cover: JS that appends a new feFuncB rather than modifying the existing one
// MutationObserver on feComponentTransfer parent:
const ct = funcB.closest('feComponentTransfer');
new MutationObserver(muts => {
  for (const m of muts) {
    for (const node of m.addedNodes) {
      if (node.tagName && node.tagName.toLowerCase() === 'fefuncb') {
        const i = parseFloat(node.getAttribute('intercept') || '0');
        if (i >= 0.9) console.warn('[SA-CSS-FCB-004] New feFuncB appended with B→max');
      }
    }
  }
}).observe(ct, { childList: true });

// Third vector: JS modifies the filter's `in` attribute to use a white feFlood source
// instead of SourceGraphic — feFuncB then operates on the white source (all channels=1)
// and produces all-white output. Monitor filter primitive 'in' attribute changes.

HIGH — SA-CSS-FCB-004: JS mousedown injection of feFuncB intercept="1" shifts consent text to blue-dominant just as the user clicks. Combined with the near-universal blue primary button active state, this is one of the highest-probability active-state color-match attacks. Detection requires MutationObservers on all <feFuncB> elements and their parent <feComponentTransfer> containers installed before any user interaction events fire.

Summary table

AttackMechanismBackground required for invisibilitySeverity
SA-CSS-FCB-001: feFuncB slope="0" intercept="1" Linear formula forces B=1 on all pixels; text shifts to vivid blue; blue contributes only 7.2% of luminance so brightness change is small — color shift is the attack, not luminance collapse; invisible on blue/indigo/purple backgrounds common in developer tools and AI client UIs; getComputedStyle.color unchanged (CSSOM blind spot) Blue, indigo, purple, or blue-violet backgrounds; primary button active states; developer-tool blue-accented consent panels; SaaS-blue consent dialogs High
SA-CSS-FCB-002: feFuncB slope="0" intercept="0" Linear formula zeros B channel; text shifts to yellow-brown (dark text) or pure yellow (light text); pure yellow text (white → rgb(255,255,0)) is invisible on yellow backgrounds at 1:1 contrast; dark text attack most effective on medium-depth amber warning panels which are common for danger/caution consent dialogs Yellow, amber, or golden backgrounds; warning-style consent panels with amber/caution styling; also any background where R and G are both high and B is also high (white → zero-B becomes yellow vs white bg) High
SA-CSS-FCB-003: feFuncB type="gamma" amplitude="0" offset="1" Gamma formula C_out=0*C_in^1+1=1 forces B=1 via offset parameter rather than intercept; third distinct syntax for blue-channel maximization; gamma offset=1 may appear as "black-level lift" color correction; detection must evaluate formula at sample inputs, not check parameter names; equivalent large-amplitude extreme-exponent variant also exists Same as FCB-001 (blue/purple backgrounds); net effect identical to slope=0 intercept=1 for all inputs High
SA-CSS-FCB-004: JS mousedown injection Intercept changed from 0 to 1 via JS at mousedown (capture phase); blue is near-universal primary action color — nearly all "Allow/Confirm" button active states are blue; timing makes visual state change coincide exactly with user interaction; MutationObserver on feFuncB and feComponentTransfer parent required; also monitor filter primitive 'in' attribute changes Blue :active button states; blue-branded consent confirmation dialogs; any page with blue-dominant primary action color High

Defences

SkillAudit findings for this attack surface

HIGH SA-CSS-FCB-001: feFuncB slope="0" intercept="1" on consent element — blue channel forced to max; text shifts to vivid blue; invisible on blue/purple/indigo backgrounds; getComputedStyle.color blind spot; blue is the most common primary action color.
HIGH SA-CSS-FCB-002: feFuncB slope="0" intercept="0" — blue channel zeroed; light text becomes pure yellow (invisible on yellow bg); dark text becomes yellow-brown (invisible on amber warning panels); 1:1 contrast for white-text-to-yellow scenario.
HIGH SA-CSS-FCB-003: feFuncB type="gamma" amplitude="0" offset="1" — B=1 via gamma offset rather than linear intercept; third syntax for blue-channel maximization; offset=1 appears as "black-level lift"; detection requires formula evaluation at sample inputs.
HIGH SA-CSS-FCB-004: JS mousedown feFuncB intercept="1" injection — text turns blue during click; matches near-universal blue :active button state; MutationObserver on feFuncB and feComponentTransfer parent required.

Related: feComponentTransfer overview (all channels)  |  feFuncR red channel attacks  |  feFuncG green channel attacks  |  feFuncA alpha channel attacks

← Blog  |  Security Checklist