MCP server CSS feComposite security: SVG feComposite operator="in" transparent flood consent erase, operator="arithmetic" coefficient mapping, CSS operator override, and feComposite consent text hide
Published 2026-09-26 — SkillAudit Research
The SVG feComposite filter primitive composites two images using Porter-Duff operators or an arithmetic formula. The available operators are over, in, out, atop, xor, and arithmetic. Each operator uses different alpha-channel logic to combine its two inputs. The in operator in particular is dangerous in the context of consent text: it outputs a pixel only where both inputs have non-zero alpha, and multiplies the color of the first input by the alpha of the second. If the second input is a transparent feFlood, the output is transparent everywhere — the consent text is completely erased from the rendered image while the DOM structure remains intact.
The arithmetic operator adds a further dimension: it computes result = k1×in×in2 + k2×in + k3×in2 + k4, giving an attacker four free coefficients to map input colors to arbitrary outputs. Setting k4=0.93 produces a constant near-white output regardless of input, or k2=-1, k4=1 inverts the input. Neither of these manipulations changes the element's fill, opacity, display, or visibility properties — they operate entirely within the filter graph. CSS can additionally override the operator presentation attribute, so SVG source audits find the benign operator="over" while the browser applies operator="in".
Complete erase attack: The feComposite operator="in" with a transparent flood input is one of the most complete consent text attacks possible — it reduces the opacity of every rendered pixel to exactly zero while leaving getBoundingClientRect, fill, opacity, and visibility checks fully intact. No standard DOM-based consent check detects this attack without filter graph traversal.
Attack findings
The feComposite filter takes SourceAlpha (the alpha channel of the original text) as
in and a transparent feFlood output as in2, with operator="in". The "in" operator produces output only where both inputs have non-zero alpha, and colors each output pixel with the color from in (SourceAlpha). SourceAlpha is grayscale; feFlood with flood-opacity="0" is transparent. The result: pixels where the source text was opaque are now colored with SourceAlpha but at the transparency of the flood (zero alpha). The element occupies its layout space; all pixels are transparent. A getBoundingClientRect check finds positive dimensions; fill, stroke, opacity checks pass.
<filter id="consent-alpha-erase">
<!-- SourceAlpha: grayscale alpha channel of consent text -->
<!-- flood: transparent (flood-opacity="0") -->
<feFlood flood-color="#000000" flood-opacity="0" result="transparent-flood"/>
<!-- operator="in": output = transparent-flood colored by SourceAlpha → transparent pixels -->
<!-- where source is opaque, result is SourceAlpha color (black) × flood-opacity (0) = transparent -->
<feComposite in="SourceAlpha" in2="transparent-flood" operator="in"/>
</filter>
<!-- All standard element checks pass:
getBoundingClientRect → positive dimensions ✓
getComputedStyle(el).fill → "rgb(45,45,45)" (non-transparent) ✓
getComputedStyle(el).opacity → "1" ✓
display, visibility → present ✓
Only filter graph traversal reveals transparent output -->
feComposite operator="arithmetic" computes: result = k1×in×in2 + k2×in + k3×in2 + k4. With carefully chosen k values, this can map any input color to near-white. For example: k1=0, k2=0, k3=0, k4=0.95 produces a near-white (alpha=0.95) constant output regardless of input. Or k1=0, k2=-1, k3=0, k4=1 inverts the image (subtracts input from 1). On dark text, inverted = near-white. The operator="arithmetic" looks legitimate to a reviewer familiar with it as an alpha-compositing/blending primitive.
<filter id="consent-arith">
<!-- k4=0.93: constant near-white output; input text color irrelevant -->
<feComposite operator="arithmetic" k1="0" k2="0" k3="0" k4="0.93"/>
</filter>
<!-- Output: constant alpha=0.93, no RGB component from source
Rendered as near-transparent white overlay, not text
Invert variant: k1=0, k2=-1, k3=0, k4=1
Output_R = -source_R + 1 → dark(0.1) becomes light(0.9)
dark consent text (#1a1a1a) → inverted → near-white (#e5e5e5) → contrast ~1.3:1 -->
The feComposite element has
operator="over" as an SVG attribute (the standard compositing operation). A CSS rule overrides it: #consent-composite { operator: in; }. With operator="in" and appropriate input pairing, the output changes from the standard "over" (additive) to "in" (intersection mask). Since operator is a presentation attribute, CSS can override it. The SVG source shows "over"; the browser applies "in".
<!-- SVG attribute: operator="over" (normal compositing, benign) -->
<feComposite id="consent-composite" in="SourceGraphic" in2="BackgroundImage" operator="over"/>
<style>
/* CSS overrides to operator="in" — intersects with BackgroundImage alpha */
/* If BackgroundImage has zero alpha in consent text region, output = transparent */
#consent-composite {
operator: in;
}
</style>
<!-- getAttribute('operator') → "over" → PASS
Effective rendered operator: "in" → ATTACK -->
feComposite with
operator="over" computes alpha-blending. For semi-transparent consent text (opacity 0.7), the blended output color with the background differs between sRGB and linearRGB computation. With color-interpolation-filters: linearRGB, the blend uses linear-light values — mid-tones become lighter after re-encoding. A consent text that blends to contrast 3.5:1 in sRGB may blend to contrast 2.3:1 in linearRGB, depending on the specific text and background colors. The attack uses this predictable lightening of linearRGB blends to degrade contrast of semi-transparent consent text.
<filter id="consent-blend-filter" color-interpolation-filters="linearRGB">
<!-- Semi-transparent consent text composited over background -->
<feComposite in="SourceGraphic" in2="BackgroundImage" operator="over"/>
</filter>
<!-- text opacity: 0.7
sRGB blend: output ≈ 0.7×text + 0.3×bg → contrast ≈ 3.5:1
linearRGB blend: decode → blend → re-encode → mid-tone lightening → contrast ≈ 2.3:1
color-interpolation-filters:linearRGB is the attack enabler, not operator="over" itself -->
Detection
function checkFeComposite(svgRoot) {
const findings = [];
const composites = svgRoot.querySelectorAll('feComposite');
for (const comp of composites) {
const operator = getComputedStyle(comp).operator || comp.getAttribute('operator') || 'over';
const inRef = comp.getAttribute('in') || '';
const in2Ref = comp.getAttribute('in2') || '';
// Check: operator="arithmetic" with suspicious k values
if (operator === 'arithmetic') {
const k4 = parseFloat(comp.getAttribute('k4') || '0');
const k2 = parseFloat(comp.getAttribute('k2') || '0');
if (k4 > 0.8) findings.push({ severity: 'high', issue: `feComposite arithmetic k4=${k4} produces near-constant near-white output regardless of input` });
if (k2 < -0.7) findings.push({ severity: 'high', issue: `feComposite arithmetic k2=${k2} heavily weights negative input term — near-inversion of dark consent text` });
}
// Check: operator="in" pairing — find what in2 resolves to
if (operator === 'in') {
// If in2 resolves to a feFlood with near-zero opacity, output is transparent
const filter = comp.closest('filter');
const in2El = filter?.querySelector(`[result="${in2Ref}"]`);
if (in2El?.tagName === 'feFlood') {
const opacity = parseFloat(getComputedStyle(in2El).floodOpacity || in2El.getAttribute('flood-opacity') || '1');
if (opacity < 0.1) findings.push({ severity: 'critical', issue: 'feComposite in="SourceAlpha" in2=transparent-flood operator="in" → transparent output; all consent text pixels erased' });
}
}
}
return findings.length ? findings : null;
}
Remediation
| Control | How it helps |
|---|---|
Traverse filter primitives to detect feComposite operator="arithmetic" and simulate k-coefficient output on consent text fill color | The arithmetic formula with specific k values can produce near-white constant output or invert dark text to near-white; simulating the k computation on the actual fill color reveals the attack |
| Check feComposite operator via computed style (not just SVG attribute) since CSS can override presentation attributes | CSS can silently change operator="over" to operator="in" via the presentation attribute cascade; getAttribute will return the benign SVG value while the browser applies the attack operator |
For operator="in" composites on consent text, verify that the in2 input chain does not resolve to a near-zero-opacity flood primitive | A transparent feFlood as the in2 input of an operator="in" composite will erase all consent text pixels to fully transparent, regardless of fill, opacity, or visibility attribute values |
Apply filter simulation in the effective color-interpolation-filters color space (linearRGB vs sRGB) to get accurate output contrast | For semi-transparent consent text composited with operator="over", the blend result differs between linearRGB and sRGB; linearRGB produces lighter mid-tone outputs that may fall below the 3:1 contrast minimum |
SkillAudit traverses the complete filter primitive graph for consent text elements, simulates feComposite arithmetic output using the actual k coefficients, checks operator via computed style to detect CSS overrides, and resolves in/in2 references to detect transparent-flood erase patterns. Run a free audit on any MCP server GitHub URL to detect feComposite consent text attacks and the full SVG filter attack surface.