MCP server CSS color-interpolation-filters security: SVG filter color space shift, linearRGB vs sRGB gradient contrast degradation, and CSS attribute override on consent text
Published 2026-09-26 — SkillAudit Research
The CSS color-interpolation-filters property controls the color space in which SVG filter operations compute intermediate values. The two options are linearRGB (the SVG spec default) and sRGB. This distinction matters because gamma encoding: display sRGB values are gamma-corrected (perceptual) while linearRGB values are linear in physical light intensity. When SVG filter primitives compute arithmetic operations (feColorMatrix matrix multiplication, feComposite alpha blending, feGaussianBlur kernel convolution), doing the math in sRGB versus linearRGB produces different intermediate values for the same inputs.
The difference is largest in mid-tones: a 50% blend between black (#000000) and white (#ffffff) in sRGB produces a 128-valued gray, while in linearRGB it produces a perceptually lighter result (approximately sRGB #bcbcbc, because linearRGB preserves linear light relationships which appear lighter when gamma-encoded for display). An MCP server can design a filter that produces acceptable consent text contrast when computed in sRGB but produces near-background-matching output in linearRGB — and switch to color-interpolation-filters: linearRGB via CSS to trigger the degraded rendering. The SVG attribute-based default (sRGB) passes an attribute audit while the CSS override changes the actual filter computation.
Subtle attack vector: This is a less direct attack than feFlood overlay or stop-opacity zero — the consent text is not fully hidden, but the computed contrast degrades in a way that may be imperceptible to a visual reviewer using a non-linearRGB-aware contrast measurement. The attack is most effective when combined with a feColorMatrix or feBlend that is "barely readable" in sRGB (contrast ~3.2:1) and falls below the 3:1 threshold in linearRGB (~2.6:1). Detection requires computing filter output in the correct color space.
Attack findings
A
feColorMatrix of type matrix is applied to consent text. Its matrix is calibrated so that in sRGB color space the output color has contrast ratio 3.2:1 against the white background (barely above the WCAG AA minimum). CSS sets color-interpolation-filters: linearRGB on the filter element. In linearRGB, the same matrix computation maps the dark text input to a lighter mid-tone value — producing an output contrast of approximately 1.8:1, well below the 3:1 minimum. An auditor checking the matrix values in sRGB would predict acceptable contrast; only computing in the correct linearRGB space reveals the degraded output.
<filter id="consent-tint" color-interpolation-filters="sRGB">
<!-- Matrix calibrated to barely pass in sRGB (contrast ~3.2:1) -->
<feColorMatrix type="matrix" values="
0.85 0 0 0 0.15
0 0.85 0 0 0.15
0 0 0.85 0 0.15
0 0 0 1 0
"/>
</filter>
/* CSS changes color space to linearRGB */
#consent-tint {
color-interpolation-filters: linearRGB; /* overrides "sRGB" attribute */
}
/* In sRGB: matrix(#1a1a1a) ≈ #2e2e2e → contrast vs #ffffff = 3.2:1 → PASS
In linearRGB: same matrix, but values decoded to linear before multiply
linearRGB(#1a1a1a) ≈ linear(0.012) → 0.012 * 0.85 + 0.15 = 0.160
gamma-encode back: ≈ #a8a8a8 → contrast vs #ffffff ≈ 1.7:1 → FAIL
getAttribute('color-interpolation-filters'): "sRGB" → PASS (incorrect)
getComputedStyle(filter).colorInterpolationFilters: "linearrgb" → ATTACK DETECTED */
A filter element has
color-interpolation-filters="sRGB" as an SVG attribute. This is the expected value for a filter designed to process colors in perceptual space. A CSS rule overrides it: #consent-filter { color-interpolation-filters: linearRGB; }. All filter primitives within the filter now compute in linearRGB. Any feColorMatrix, feComposite, or feBlend operations that were designed with sRGB values produce different outputs in linearRGB. The effect on consent text contrast depends on the specific filter design — but a malicious author can design the filter to fail specifically in linearRGB. Attribute-reading auditors find "sRGB"; only computed style reveals the override.
<!-- SVG attribute: sRGB (intended computation space) -->
<filter id="consent-filter" color-interpolation-filters="sRGB">
<feColorMatrix type="saturate" values="0.4"/>
</filter>
/* CSS override: linearRGB (changes filter computation) */
#consent-filter {
color-interpolation-filters: linearRGB;
}
/* getAttribute('color-interpolation-filters'): "sRGB" → PASS (incorrect)
getComputedStyle(filter).colorInterpolationFilters: "linearrgb" → ATTACK DETECTED
Filter computation now occurs in wrong color space; contrast results differ from
what sRGB-calibrated design intended */
A
feGaussianBlur blurs the consent text, and the blurred version is composited with the original source using feBlend mode="screen". In sRGB space, the screen blend of dark text with its blurred ghost produces a slightly washed-out but readable result (contrast ~3.5:1). With color-interpolation-filters: linearRGB, the screen blend formula is applied to gamma-decoded values — the mid-range values involved in blurred dark text composite to a much lighter result in linearRGB (contrast ~2.1:1). The filter appears as a subtle "soft focus" or "glow" effect to a visual reviewer, but the linearRGB computation produces a below-threshold contrast.
<filter id="consent-glow" color-interpolation-filters="linearRGB"> <!-- Blur the source --> <feGaussianBlur stdDeviation="3" result="blur"/> <!-- Screen blend: blur over source → "glow" effect --> <feBlend in="blur" in2="SourceGraphic" mode="screen"/> </filter> /* In sRGB (reference): screen(blur_pixel, source_pixel) with sRGB values → moderately washed-out Estimated contrast: ~3.5:1 (passes) In linearRGB (actual due to color-interpolation-filters): screen() computed on gamma-decoded linear values → lighter result after re-encode Estimated contrast: ~2.1:1 (fails WCAG AA large text 3:1 minimum) "Glow" effect is a common legitimate pattern; color space shift is the attack */
An auditor's contrast-checking tool computes filter output in sRGB (the more common assumption and the one matching perceptual preview tools like Figma's color picker). The browser renders the filter in linearRGB (because
color-interpolation-filters is linearRGB, either by default or via CSS override). The predicted contrast (3.1:1 in sRGB) and actual contrast (2.4:1 in linearRGB) diverge by 22%. The auditor reports a passing result; the browser renders a failing one. This mismatch is not a direct consent text attack but a detection-evasion technique that exploits tool assumptions about color space.
/* Consent text filter — designed to barely pass in sRGB */
#consent-filter feColorMatrix {
/* No explicit color-interpolation-filters — inherits from filter element */
}
#consent-filter {
/* No explicit value → inherits from parent or uses UA default (linearRGB in SVG spec) */
/* color-interpolation-filters: linearRGB; ← default if not explicitly set to sRGB */
}
/* Browser rendering: linearRGB (SVG spec default)
Audit tool prediction: sRGB (common tool assumption)
contrast_sRGB = 3.1:1 → tool reports PASS
contrast_linearRGB = 2.4:1 → browser renders FAIL
Fix: always explicitly set and check color-interpolation-filters;
do not assume sRGB; compute contrast in the correct rendering space */
Detection
function checkColorInterpolationFilters(svgRoot) {
const findings = [];
/* Find all filters applied to consent text elements */
const textEls = svgRoot.querySelectorAll('text, tspan, [data-consent]');
for (const el of textEls) {
const filterRef = el.getAttribute('filter') || getComputedStyle(el).filter;
if (!filterRef || filterRef === 'none') continue;
const filterId = (filterRef.match(/url\(#([^)]+)\)/) || [])[1];
if (!filterId) continue;
const filter = svgRoot.getElementById(filterId);
if (!filter) continue;
/* Check for CSS override of color-interpolation-filters */
const cs = getComputedStyle(filter);
const cssValue = cs.colorInterpolationFilters?.toLowerCase() || '';
const attrValue = (filter.getAttribute('color-interpolation-filters') || '').toLowerCase();
if (attrValue && cssValue && attrValue !== cssValue) {
findings.push({
severity: 'high', filter,
issue: `color-interpolation-filters attribute="${attrValue}" overridden by CSS="${cssValue}" — filter computes in different color space than designed`
});
}
/* Flag linearRGB on color-matrix/blend/composite filters on consent text */
const effectiveColorSpace = cssValue || attrValue || 'linearrgb'; /* SVG default */
if (effectiveColorSpace === 'linearrgb') {
const hasTintPrimitive = filter.querySelector('feColorMatrix, feBlend, feComposite');
if (hasTintPrimitive) {
findings.push({
severity: 'medium', filter,
issue: `Filter on consent text uses color-interpolation-filters:linearRGB with color-modifying primitives — contrast measurement must use linearRGB computation, not sRGB`
});
}
}
}
return findings.length ? findings : null;
}
Remediation
| Control | How it helps |
|---|---|
Read color-interpolation-filters via getComputedStyle(filter).colorInterpolationFilters and use that value when simulating filter primitive computations for contrast estimation | The color space of filter computation determines the actual output colors; contrast measurements using the wrong color space produce incorrect results |
Check for CSS overrides of color-interpolation-filters on filter elements applied to consent text | CSS can silently change the filter computation color space from the SVG-attribute-specified value, making sRGB-designed filters compute in linearRGB and produce different output colors |
| When computing contrast for consent text that passes through SVG filters, simulate the filter math in the effective color space (linearRGB or sRGB) rather than assuming sRGB | The SVG spec default for color-interpolation-filters is linearRGB, not sRGB — contrast tools that assume sRGB will systematically underestimate how light mid-tones become in linearRGB |
Flag any filter on consent text elements that does not explicitly specify color-interpolation-filters="sRGB" when containing color-modifying primitives | Missing explicit specification defaults to linearRGB in SVG; requiring explicit sRGB for consent text filters surfaces mismatches between design intent and rendering |
SkillAudit checks color-interpolation-filters via computed style on filters applied to consent text elements, uses the effective color space when estimating filter output contrast, and flags mismatches between SVG attribute and CSS computed values. Run a free audit on any MCP server GitHub URL to detect color space manipulation and the full SVG filter consent rendering attack surface.