Security Guide
MCP server SVG color-interpolation-filters consent security — linearRGB vs sRGB feFlood lightness mismatch, feBlend color space contrast, linearGradient stop interpolation space, and background-match detection bypass
The SVG color-interpolation-filters property controls whether filter primitive operations (feFlood, feBlend, feComposite, etc.) are performed in linearized RGB (linearRGB) or gamma-compressed RGB (sRGB) color space. The choice affects the perceived lightness of intermediate filter outputs and the way colors blend at edges. Background-matching detection logic that assumes sRGB arithmetic can be defeated by an attacker who picks flood colors that appear background-matching in linearRGB but are flagged as off-background in sRGB, or vice versa. Four attack patterns exploit color-space confusion to evade consent cover detection.
Attack 1: linearRGB feFlood lightness mismatch defeats sRGB background-matching (SA-CSS-CIF-001)
In the sRGB color space (the default for web content), the color #f0f0f0 (RGB 240,240,240) has a relative luminance of approximately 0.871 — very close to white (1.0). A background-matching check comparing this against a white background (#ffffff) might pass it as near-enough-to-background. However, with color-interpolation-filters="linearRGB" on the filter element, the feFlood output is computed in linear-light RGB. The linear-light equivalent of sRGB 240 is approximately (240/255)^2.2 ≈ 0.871 (already approximately linear since the value is close to 1.0). The key issue is the reverse: if the attacker uses a color that appears light-grey in sRGB (e.g., #e8e8e8, relative luminance ≈ 0.82 in sRGB) but the background-matching logic incorrectly linearizes it as approximately 0.79, it classifies the color as “not background-matching” and passes it. In practice, the gamma difference between sRGB and linear-light is largest in the mid-range (around 0.5 relative luminance, i.e., #bcbcbc) but still measurable for light greys. An attacker can exploit this window to pick a color that passes sRGB background-matching but renders as a visible-enough cover in linearRGB context.
<!-- SA-CSS-CIF-001: linearRGB filter on feFlood -- color space affects effective lightness -->
<filter id="lr-cov" color-interpolation-filters="linearRGB">
<!-- In sRGB: #e8e8e8 has relative luminance ~0.82 -- may not pass background-match
In linearRGB context: the filter arithmetic is linear; if the audit assumes sRGB
comparison, it may classify this as non-matching while the visual output is
a high-contrast grey-on-white cover reducing contrast ratio to ~1.1:1 -->
<feFlood flood-color="#e8e8e8" flood-opacity="0.95" result="flood"/>
<feComposite in="flood" in2="SourceGraphic" operator="over"/>
</filter>
// Detection: always resolve color-interpolation-filters and check relative luminance
// in both color spaces when the filter uses linearRGB
function checkColorInterpolationFilters(filterEl, floodColor) {
const cif = filterEl.getAttribute('color-interpolation-filters') ||
getComputedStyle(filterEl).getPropertyValue('color-interpolation-filters') ||
'sRGB';
const srgbLuminance = relativeLuminanceSRGB(floodColor);
const linearLuminance = relativeLuminanceLinear(floodColor);
const findings = [];
if (cif === 'linearRGB' || cif === 'auto') {
// Flag if either color space computation places color in background-matching range
if (srgbLuminance > 0.7 || linearLuminance > 0.7) {
findings.push({
severity: 'HIGH', code: 'SA-CSS-CIF-001',
note: `filter has color-interpolation-filters="${cif}"; feFlood "${floodColor}" relative luminance: sRGB=${srgbLuminance.toFixed(2)}, linear=${linearLuminance.toFixed(2)}; color space mismatch may cause audit to misclassify cover color; normalize to linear-light before comparison`
});
}
if (cif === 'linearRGB' && Math.abs(srgbLuminance - linearLuminance) > 0.1) {
findings.push({
severity: 'MEDIUM', code: 'SA-CSS-CIF-001',
note: `color-interpolation-filters="linearRGB" with flood-color "${floodColor}": sRGB vs linear luminance differ by ${Math.abs(srgbLuminance - linearLuminance).toFixed(2)} — background-match logic assuming sRGB may produce incorrect result`
});
}
}
return findings;
}
function relativeLuminanceSRGB(hexColor) {
const [r, g, b] = hexToRGB(hexColor).map(c => {
c /= 255;
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
});
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
HIGH — SA-CSS-CIF-001: color-interpolation-filters="linearRGB" on a filter element causes filter primitive operations to be performed in linear-light color space. Background-matching logic that computes relative luminance assuming sRGB may classify a cover color as non-matching when the filter’s linear-light output is actually background-matching. Detection must check the color-interpolation-filters attribute on all filter elements and compute relative luminance using the WCAG 2.1 linearization formula, not a raw sRGB channel average.
Attack 2: feBlend over linearRGB feFlood produces unexpected contrast from gamma difference (SA-CSS-CIF-002)
When feBlend composites SourceGraphic (which is in sRGB as default) over a feFlood result computed in linearRGB, the two inputs are in different color spaces. The SVG spec requires that both inputs to feBlend be converted to the filter’s operating color space before blending. With color-interpolation-filters="linearRGB", the SourceGraphic channels (which encode gamma-compressed sRGB values) are treated as if they were already linear. This causes the mid-tone consent text values to blend incorrectly: a text color of #333333 (sRGB) has a linear-light luminance of approximately 0.033, but if the linearRGB filter treats the sRGB-encoded value 0.2 (51/255) as linear, the blending arithmetic produces a lighter output than expected. The net effect is that consent text contrast may be reduced further than a pure sRGB blend would produce, exceeding the background-matching threshold while appearing to pass color-based audits that assume sRGB.
<!-- SA-CSS-CIF-002: feBlend in linearRGB context composites sRGB SourceGraphic incorrectly -->
<filter id="blend-lr" color-interpolation-filters="linearRGB">
<!-- SourceGraphic (sRGB-encoded) is blended in linearRGB space
The consent text's sRGB channels are misinterpreted as linear values
Result: text appears lighter than expected; contrast reduced below WCAG threshold -->
<feFlood flood-color="#f5f5f5" flood-opacity="0.8" result="bg"/>
<feBlend in="SourceGraphic" in2="bg" mode="multiply" result="blended"/>
</filter>
// Detection: flag feBlend in linearRGB filters that blend SourceGraphic
function checkFeBlendColorSpace(filterEl) {
const cif = filterEl.getAttribute('color-interpolation-filters') || 'sRGB';
if (cif !== 'linearRGB') return [];
const blends = filterEl.querySelectorAll('feBlend');
const findings = [];
for (const blend of blends) {
const inAttr = blend.getAttribute('in') || '';
const in2Attr = blend.getAttribute('in2') || '';
if (inAttr === 'SourceGraphic' || in2Attr === 'SourceGraphic') {
findings.push({
severity: 'HIGH', code: 'SA-CSS-CIF-002',
note: `feBlend composites SourceGraphic in linearRGB filter context (color-interpolation-filters="linearRGB") — sRGB-encoded SourceGraphic blended as if linear; contrast of blended output may not match sRGB prediction; WCAG contrast check on linearRGB filter output required`
});
}
}
return findings;
}
HIGH — SA-CSS-CIF-002: A feBlend operation compositing SourceGraphic in a color-interpolation-filters="linearRGB" filter treats the sRGB-encoded SourceGraphic channels as linear values. The resulting output may have lower contrast than a purely sRGB blend prediction suggests. Consent text contrast audits must account for the filter’s operating color space when computing expected luminance ratios.
Attack 3: linearGradient stop interpolation differs between linearRGB and sRGB (SA-CSS-CIF-003)
The color-interpolation attribute (not color-interpolation-filters) on a <linearGradient> controls how colors are interpolated between stops. With color-interpolation="linearRGB", the midpoint between two color stops is computed by averaging their linear-light values and then gamma-compressing the result. The midpoint between white (#ffffff, linear 1.0) and #bcbcbc (linear ≈ 0.21) in linearRGB is linear 0.605, which gamma-compresses to approximately sRGB #d0d0d0 — visibly lighter than the sRGB midpoint between the same stops (#d8d8d8 by sRGB average). An attacker sets stops such that the sRGB midpoint falls in a clearly-off-background range (e.g., #c0c0c0 with WCAG contrast ratio ~1.5:1 against white) but the linearRGB midpoint is lighter (#d4d4d4, contrast ratio ~1.2:1, below the WCAG 1.2:1 threshold for “effectively invisible” used by some detection tools).
<!-- SA-CSS-CIF-003: linearGradient color-interpolation=linearRGB changes mid-stop color -->
<defs>
<!-- color-interpolation=linearRGB on the gradient changes mid-point interpolation -->
<linearGradient id="lr-grad" x1="0" y1="0" x2="0" y2="1"
color-interpolation="linearRGB">
<stop offset="0" stop-color="#ffffff"/> <!-- white -->
<stop offset="0.5" stop-color="#c0c0c0"/> <!-- mid: clearly off-background in sRGB -->
<stop offset="1" stop-color="#ffffff"/> <!-- white -->
</linearGradient>
<!-- In linearRGB: the 0 and 1 stops are both white (linear 1.0)
Mid-point color is lighter than sRGB interpolation; #c0c0c0 stop contributes less
Effective mid-point rendered color approaches white in linearRGB context -->
</defs>
<rect fill="url(#lr-grad)" width="400" height="120"/>
// Detection: check color-interpolation attribute on gradient elements
function checkGradientColorInterpolation(svgEl) {
const findings = [];
for (const grad of svgEl.querySelectorAll('linearGradient, radialGradient')) {
const ci = grad.getAttribute('color-interpolation') || 'sRGB';
if (ci === 'linearRGB' || ci === 'auto') {
// Flag any gradient with linearRGB interpolation for manual color-space analysis
const stopColors = Array.from(grad.querySelectorAll('stop')).map(s =>
s.getAttribute('stop-color') || 'black'
);
findings.push({
severity: 'MEDIUM', code: 'SA-CSS-CIF-003',
note: `<linearGradient> has color-interpolation="${ci}"; stop colors [${stopColors.join(', ')}]; mid-point colors differ from sRGB prediction; background-matching logic must account for linearRGB interpolation when evaluating gradient cover visibility`
});
}
}
return findings;
}
MEDIUM — SA-CSS-CIF-003: color-interpolation="linearRGB" on a <linearGradient> changes how colors are blended between stops. Mid-point colors computed in linearRGB are systematically lighter than sRGB midpoints (due to gamma compression). An attacker can choose stop colors such that the sRGB-expected midpoint appears off-background to detection tools but the actual linearRGB-computed midpoint is background-matching. Detection must flag all gradients with color-interpolation="linearRGB" and recompute mid-stop colors using linearRGB blending before the background-match check.
Attack 4: detection must always normalize to linear-light before color comparison (SA-CSS-CIF-004)
The root vulnerability across all three color-space attacks is that naive background-matching logic compares raw sRGB channel values rather than perceptual or linear-light luminance. The correct approach — required by WCAG 2.1 and recommended for all consent-cover detection — is to always convert color channel values to relative luminance using the linearization formula before comparison. This normalizes both the flood color and the reference background color to linear-light regardless of the filter’s color-interpolation-filters setting, removing the attacker’s ability to exploit color-space confusion. Additionally, any filter element with color-interpolation-filters="linearRGB" should be flagged as requiring manual review, since the effective output luminance of filter operations in linearRGB cannot be reliably predicted from SVG attribute values alone without running the full filter graph.
// SA-CSS-CIF-004: always normalize to linear-light before color comparison
// WCAG 2.1 relative luminance (correct formula)
function relativeLuminance(hexOrRGB) {
const [r, g, b] = parseColor(hexOrRGB).map(c => {
c /= 255;
// sRGB linearization (IEC 61966-2-1)
return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
});
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
// Contrast ratio between two colors (WCAG 2.1 formula)
function contrastRatio(color1, color2) {
const l1 = relativeLuminance(color1);
const l2 = relativeLuminance(color2);
const lighter = Math.max(l1, l2);
const darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}
// Background-matching check: contrast ratio below 1.2:1 is effectively background-matching
function isBackgroundMatching(floodColor, pageBackground) {
return contrastRatio(floodColor, pageBackground) < 1.2;
}
// Always check color-interpolation-filters on ancestor filter elements
function getFilterColorSpace(filterEl) {
const own = filterEl.getAttribute('color-interpolation-filters') ||
getComputedStyle(filterEl).getPropertyValue('color-interpolation-filters');
if (own) return own;
let el = filterEl.parentElement;
while (el) {
const v = el.getAttribute ? el.getAttribute('color-interpolation-filters') : null;
if (v) return v;
el = el.parentElement;
}
return 'sRGB'; // default per SVG spec
}
function auditFloodColorWithColorSpace(filterEl, pageBackground = '#ffffff') {
const colorSpace = getFilterColorSpace(filterEl);
const findings = [];
for (const ff of filterEl.querySelectorAll('feFlood')) {
const color = ff.getAttribute('flood-color') ||
getComputedStyle(ff).getPropertyValue('flood-color') || 'black';
const opacity = parseFloat(
ff.getAttribute('flood-opacity') ||
getComputedStyle(ff).getPropertyValue('flood-opacity') || '1'
);
// Use linearized comparison regardless of color space
const ratio = contrastRatio(color, pageBackground);
if (ratio < 1.3 && opacity > 0.6) {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-CIF-004',
note: `feFlood "${color}" (contrast ratio ${ratio.toFixed(2)}:1 against page background) at flood-opacity=${opacity} in filter with color-interpolation-filters="${colorSpace}"; effective cover visible; linear-light normalized comparison used`
});
}
if (colorSpace === 'linearRGB') {
findings.push({
severity: 'MEDIUM', code: 'SA-CSS-CIF-004',
note: `filter has color-interpolation-filters="linearRGB"; feFlood color comparisons may not match sRGB-based detection logic; manual verification of filter graph output in linearRGB context recommended`
});
}
}
return findings;
}
Detection note — SA-CSS-CIF-004: The correct baseline for all consent cover color comparisons is the WCAG 2.1 relative luminance formula, which linearizes sRGB channel values before computing luminance. This normalizes the comparison to physical light regardless of the SVG filter’s color space setting. Any filter element with color-interpolation-filters="linearRGB" should also be flagged as a MEDIUM finding requiring manual review, since the effective output of complex filter graphs in linearRGB cannot be fully predicted by attribute inspection alone.
Summary table
| Attack | Mechanism | What it bypasses | Severity |
|---|---|---|---|
| SA-CSS-CIF-001: linearRGB feFlood lightness mismatch | color-interpolation-filters="linearRGB" on filter; feFlood flood-color chosen to pass sRGB background-match but render as visible cover in linearRGB output; gamma difference exploits tolerance band in sRGB-based detection | sRGB-only background-match logic; requires linear-light (WCAG 2.1) luminance normalization before color comparison; color-interpolation-filters attribute check on filter elements | High |
| SA-CSS-CIF-002: feBlend SourceGraphic in linearRGB context | feBlend in linearRGB filter composites sRGB SourceGraphic as if linear; text contrast differs from sRGB blend prediction; blended output luminance may fall below WCAG threshold | sRGB-predicted contrast ratio check; requires accounting for filter color space when evaluating feBlend output luminance; flag feBlend+SourceGraphic in linearRGB filters | High |
| SA-CSS-CIF-003: linearGradient linearRGB stop interpolation | color-interpolation="linearRGB" on linearGradient; stop mid-point colors lighter in linearRGB than sRGB due to gamma; sRGB-expected mid-point appears off-background; linearRGB mid-point is background-matching | sRGB-only gradient stop mid-point analysis; requires linearRGB interpolation formula for gradients with color-interpolation="linearRGB"; flag gradients for manual review | Medium |
| SA-CSS-CIF-004: detection normalization gap | Any color comparison using raw sRGB channels instead of WCAG relative luminance formula is vulnerable to color-space-based evasion; correct baseline: linearize all channels before comparison | Raw channel-value comparisons; sRGB-only background-match; requires WCAG 2.1 relative luminance normalization for all flood-color, gradient stop, and filter output comparisons | Medium |