← Blog

MCP server SVG feComponentTransfer linear alpha slope/intercept consent security

Four SA-CSS-CTL consent attack patterns using feComponentTransfer feFuncA type="linear" — the linear formula output = slope × input + intercept allows precise reduction of consent text opacity to any level below the WCAG threshold while CSSOM reports the original opacity unchanged.

By SkillAudit  |  29 Oct 2026

The feFuncA type="linear" transfer function computes output_A = slope × input_A + intercept for each pixel’s alpha channel. Unlike type="discrete" (step function) or type="table" (piecewise linear), the linear mode maps the full continuous range [0, 1] through a single affine transform. This gives attackers precise control over the final opacity of consent text: slope=0.1 reduces any fully opaque glyph to 10% opacity; slope=0, intercept=0.05 sets every pixel to exactly 5% opacity regardless of original value.

CSSOM completely blind: getComputedStyle(consentEl).opacity returns "1" regardless of what feFuncA linear does to pixel alpha values. CSS opacity and SVG filter alpha transforms operate at different levels of the rendering pipeline.

Linear transfer function mechanics

/* feFuncA type="linear": output = slope × input + intercept */
/* output is clamped to [0, 1] */

/* Identity: slope=1, intercept=0 → output = input (no change) */
/* Full erase: slope=0, intercept=0 → output = 0 (all transparent) */
/* Opacity reduction: slope=0.1, intercept=0 → all A values ×0.1 */
/* Flat opacity: slope=0, intercept=0.05 → all pixels at A=0.05 */

/* Multiple chained feComponentTransfer: each stage multiplies slope */
/* Stage 1: slope=0.5 → A' = 0.5×A */
/* Stage 2: slope=0.5 → A'' = 0.5×A' = 0.25×A */
/* Stage 3: slope=0.5 → A''' = 0.5×A'' = 0.125×A */
/* Combined: 1.0 → 0.125 (12.5% opacity) via three 50%-each stages */

SA-CSS-CTL-001: feFuncA linear slope=0.1 — 90% opacity reduction

A direct single-primitive attack: feFuncA type="linear" slope="0.1" intercept="0" reduces every fully opaque glyph pixel (A=1.0) to A=0.10 (10% opacity). The text remains positioned correctly and has the same color values, but at 10% opacity over white, WCAG contrast is approximately 1.22:1 — far below the 4.5:1 threshold. The filter is a single primitive: no feFlood, no feComposite, no feBlend. Only a feComponentTransfer with a single feFuncA.

<filter id="ctl001" color-interpolation-filters="sRGB">
  <feComponentTransfer>
    <feFuncA type="linear" slope="0.1" intercept="0"/>
  </feComponentTransfer>
</filter>

/* Effect on opaque glyph pixel (input_A = 1.0):
   output_A = 0.1 × 1.0 + 0 = 0.10

   WCAG calculation:
   text L ≈ 0.002 (dark, rgb(17,17,17))
   perceived L at A=0.10 over white (L=1.0):
   L_perceived = 0.10 × 0.002 + 0.90 × 1.0 = 0.9002
   Contrast: (1.0+0.05)/(0.9002+0.05) = 1.05/0.9502 ≈ 1.10:1 — FAIL

   Alternate calc (at center of thin stroke, some glyph coverage):
   If 50% pixel coverage (subpixel glyph stroke center):
   L_perceived = 0.10 × 0.002 + 0.90 × 1.0 → same ≈ 1.10:1

   Evades:
   - opacity threshold checks (CSSOM opacity=1)
   - feFlood/feComposite/feBlend detection (no such primitives)
   - filter attribute presence check (filter is present but feComponentTransfer is unremarkable)

   Detection:
   - Enumerate feComponentTransfer in consent ancestor filters
   - For each feFuncA with type=linear: compute max_output for A ∈ [0,1]
   - max_output = max(slope×0 + intercept, slope×1 + intercept) = max(intercept, slope+intercept)
   - If max_output ≤ 0.25 → CRITICAL (consent text opacity likely below WCAG) */

function detectFeFuncALinear(consentEl) {
  let el = consentEl;
  while (el && el !== document.body) {
    const f = getComputedStyle(el).filter || el.getAttribute('filter') || '';
    const m = f.match(/url\(#([^)]+)\)/);
    if (m) {
      const filterEl = document.getElementById(m[1]);
      if (filterEl) {
        for (const fct of filterEl.querySelectorAll('feComponentTransfer')) {
          const funcA = fct.querySelector('feFuncA');
          if (funcA && funcA.getAttribute('type') === 'linear') {
            const slope = parseFloat(funcA.getAttribute('slope') || '1');
            const intercept = parseFloat(funcA.getAttribute('intercept') || '0');
            const maxOutput = Math.min(1, Math.max(intercept, slope + intercept));
            if (maxOutput <= 0.25) {
              return { risk: 'CRITICAL', slope, intercept, maxOutput };
            }
          }
        }
      }
    }
    el = el.parentElement;
  }
  return null;
}
CRITICAL
SA-CSS-CTL-001 — feFuncA linear slope=0.1. Opaque glyphs → 10% opacity. WCAG ≈1.10:1. Single primitive, no feFlood/feComposite. Detection: max_output ≤ 0.25 on any feFuncA linear in consent ancestor filter → CRITICAL.

SA-CSS-CTL-002: feFuncA linear slope=0 intercept=0.05 — fixed flat opacity

With slope=0 and intercept=0.05, the linear formula produces output_A = 0×input_A + 0.05 = 0.05 for all input values. Every pixel — transparent or opaque — is rendered at exactly 5% opacity. This is functionally identical to the discrete tableValues="0.05" attack (SA-CSS-CTA-002) but uses a different primitive type. Detectors checking only for type=discrete or checking only for opacity=0 exact values will miss this.

<filter id="ctl002">
  <feComponentTransfer>
    <!-- slope=0: multiply input by 0 (remove all variation)
         intercept=0.05: add flat 5% to every pixel -->
    <feFuncA type="linear" slope="0" intercept="0.05"/>
  </feComponentTransfer>
</filter>

/* output_A = 0 × input_A + 0.05 = 0.05 for all pixels
   WCAG at 5% opacity: ≈ 1.05:1 — FAIL

   Evades:
   - type=discrete specific detection (this is type=linear)
   - CSS opacity=0 exact-match rules (A=0.05 ≠ 0)
   - opacity≤0.01 threshold (0.05 > 0.01)
   - max_output = max(0.05, 0+0.05) = 0.05 → ≤ 0.25 → CRITICAL */
CRITICAL
SA-CSS-CTL-002 — feFuncA linear slope=0 intercept=0.05. All pixels at 5% opacity. WCAG 1.05:1. Evades discrete-type checks and opacity≤0.01 thresholds. Detection: same max_output ≤ 0.25 test catches this variant.

SA-CSS-CTL-003: chained feComponentTransfer — multi-stage opacity reduction evasion

A single feFuncA slope=0.5 produces 50% opacity — which a detection rule might catch (max_output = 0.5, above a 0.25 CRITICAL threshold but worth flagging). To evade a per-stage threshold, the attacker chains three separate feComponentTransfer primitives each with slope=0.5. Each stage reduces opacity by half; the combined product after three stages is 0.53 = 0.125 (12.5% opacity). WCAG ≈ 1.33:1 — fail. Any detector checking only single-stage slope values will find each stage individually acceptable.

<filter id="ctl003">
  <!-- Stage 1: A → 0.5×A -->
  <feComponentTransfer result="s1">
    <feFuncA type="linear" slope="0.5" intercept="0"/>
  </feComponentTransfer>

  <!-- Stage 2: 0.5A → 0.25A -->
  <feComponentTransfer in="s1" result="s2">
    <feFuncA type="linear" slope="0.5" intercept="0"/>
  </feComponentTransfer>

  <!-- Stage 3: 0.25A → 0.125A -->
  <feComponentTransfer in="s2">
    <feFuncA type="linear" slope="0.5" intercept="0"/>
  </feComponentTransfer>
</filter>

/* Combined: A_out = 0.5³ × A_in = 0.125 × A_in
   Opaque glyph (A=1.0) → A=0.125 (12.5% opacity)
   WCAG: L_perceived ≈ 0.125×0.002 + 0.875×1.0 ≈ 0.875
   Contrast: (1.05)/(0.875+0.05) = 1.05/0.925 ≈ 1.13:1 — FAIL

   Per-stage check: each feFuncA has slope=0.5 → max_output=0.5 → HIGH at most
   But the correct detection is: multiply all chained slope values in linear filter path
   Combined slope: 0.5 × 0.5 × 0.5 = 0.125 → ≤ 0.25 → CRITICAL

   Detection: trace feComponentTransfer chain via result/in attributes;
              multiply all feFuncA linear slopes along the chain;
              if product ≤ 0.25 → CRITICAL */
HIGH
SA-CSS-CTL-003 — Three chained feComponentTransfer each slope=0.5. Combined: 12.5% opacity, WCAG 1.13:1 fail. Each stage individually appears at HIGH threshold. Detection requires multiplying slopes across the full chain in the filter graph.

SA-CSS-CTL-004: JS mousedown slope swap — TOCTOU linear attack

This TOCTOU variant begins with feFuncA slope="1" intercept="0" (the identity transform: no visual change) and swaps the slope to 0.05 at mousedown. The 95% opacity reduction activates instantaneously at the moment of click confirmation, while the consent text was readable throughout the review period.

<svg style="position:absolute;width:0;height:0" aria-hidden="true">
  <defs>
    <filter id="ctl004">
      <feComponentTransfer>
        <!-- Initially identity: slope=1, intercept=0 → no change -->
        <feFuncA id="ctl004-funcA" type="linear" slope="1" intercept="0"/>
      </feComponentTransfer>
    </filter>
  </defs>
</svg>

<span style="filter:url(#ctl004)">
  Grant this MCP server write access to your entire file system.
</span>
<button onmousedown="
  document.getElementById('ctl004-funcA').setAttribute('slope','0.05')
">I Agree</button>

/* MutationObserver: detect slope/intercept attribute changes */
const funcA = document.getElementById('ctl004-funcA');
const mo = new MutationObserver(muts => {
  for (const m of muts) {
    if (m.attributeName === 'slope' || m.attributeName === 'intercept') {
      const slope = parseFloat(funcA.getAttribute('slope') || '1');
      const intercept = parseFloat(funcA.getAttribute('intercept') || '0');
      const maxOut = Math.min(1, Math.max(intercept, slope + intercept));
      if (maxOut <= 0.25) {
        reportFinding('CRITICAL', 'SA-CSS-CTL-004', {
          slope, intercept, maxOutput: maxOut,
          note: 'feFuncA linear slope reduced at mousedown — TOCTOU opacity attack'
        });
      }
    }
  }
});
mo.observe(funcA, { attributes: true, attributeOldValue: true,
                    attributeFilter: ['slope', 'intercept'] });
CRITICAL
SA-CSS-CTL-004 — feFuncA linear slope swapped from 1 (identity) to 0.05 at mousedown. Consent readable during review; 5% opacity at confirmation. MutationObserver on slope and intercept attributes with max_output ≤ 0.25 threshold required.

Summary of SA-CSS-CTL findings

IDSeverityMechanismWCAGEvades
SA-CSS-CTL-001 CRITICAL feFuncA linear slope=0.1 — 90% opacity reduction 1.10:1 CSSOM opacity, feFlood/feComposite detection
SA-CSS-CTL-002 CRITICAL feFuncA linear slope=0 intercept=0.05 — flat 5% opacity 1.05:1 discrete-type detection, opacity≤0.01 threshold
SA-CSS-CTL-003 HIGH Three chained stages slope=0.5 each — combined 12.5% 1.13:1 Per-stage slope threshold checks
SA-CSS-CTL-004 CRITICAL JS mousedown slope 1→0.05 TOCTOU swap ~1.05:1 at click Static analysis, screenshot at rest

Related pages