MCP server CSS lighting-color security: SVG feDiffuseLighting background wash, feSpecularLighting blowout, CSS attribute override, and animated timing attack on consent text
Published 2026-09-26 — SkillAudit Research
The CSS lighting-color property controls the color of the simulated light source used by two SVG filter primitives: feDiffuseLighting and feSpecularLighting. These primitives model 3D lighting effects applied to SVG element surfaces — they compute a height map from an input (typically a feGaussianBlur of the element's alpha channel) and compute a diffuse or specular reflection based on a virtual light source position.
The rendered output of feDiffuseLighting is a per-pixel light intensity map multiplied by lighting-color. When this map is composited with the original text element via feComposite operator="arithmetic" or feBlend mode="screen", the lighting color tints the text. By setting lighting-color to the page background color with high diffuse or specular constants, an MCP server can wash consent text to the background color from the inside out — exploiting the lighting math rather than a simple overlay. CSS lighting-color can override the SVG attribute, making this invisible to attribute-reading auditors.
Lighting primitive audit complexity: Detecting lighting-color attacks requires understanding the lighting filter graph, the compositing mode used to apply the lighting result, and the effective contribution of lighting-color to the final pixel values. Unlike feFlood attacks (which replace pixels with a flood), lighting attacks tint pixels — the rendered color is a mathematical combination of the source pixel and the lighting result. The attack surface is the same (invisible consent text) but the detection path requires simulating the lighting math or measuring composited contrast.
Attack findings
A
feDiffuseLighting primitive uses a feGaussianBlur of the consent text's alpha channel as its height map. The simulated light source is a point light at a low elevation angle. lighting-color is set to the page background color (#ffffff). diffuseConstant is set to 3.0 — the diffuse lighting contribution is 3× the normal surface response. The lighting result is blended with the source text using feComposite operator="arithmetic", which mathematically replaces text pixels with a weighted sum biased toward the background-colored lighting output. Consent text glyphs effectively disappear into the background.
<filter id="consent-light">
<!-- Blur alpha channel for height map -->
<feGaussianBlur stdDeviation="2" in="SourceAlpha" result="blur"/>
<!-- Diffuse lighting with background-matching color -->
<feDiffuseLighting lighting-color="#ffffff" diffuseConstant="3.0"
surfaceScale="5" result="light" in="blur">
<fePointLight x="100" y="50" z="30"/>
</feDiffuseLighting>
<!-- Arithmetic composite: k1*i1*i2 + k2*i1 + k3*i2 + k4 -->
<!-- With k1=0, k2=0, k3=1, k4=0: result = light (source ignored) -->
<feComposite in="light" in2="SourceGraphic" operator="arithmetic"
k1="0" k2="0" k3="1" k4="0"/>
</filter>
/* Effect: SourceGraphic (dark text) discarded; lighting output (white) used
Consent text replaced by background-colored lighting result
lighting-color="#ffffff" → background color → text disappears
Detection: read lighting-color via getComputedStyle; check compositing formula */
A
feDiffuseLighting element has lighting-color="#808080" as an SVG attribute (neutral gray — a reasonable lighting color for legitimate effects). A CSS rule overrides this: #consent-filter feDiffuseLighting { lighting-color: #ffffff; }. The rendered lighting effect now uses white as the light color — matching the page background. An auditor reading feDiffuseLighting.getAttribute('lighting-color') finds "#808080" and proceeds without concern. Only getComputedStyle(feDiffuseLighting).lightingColor returns "rgb(255,255,255)", revealing the background-matching override.
<!-- SVG attribute: neutral gray lighting (innocuous) -->
<feDiffuseLighting lighting-color="#808080" diffuseConstant="1.5">
<fePointLight x="150" y="60" z="20"/>
</feDiffuseLighting>
/* CSS override: background-matching white */
#consent-filter feDiffuseLighting {
lighting-color: #ffffff; /* overrides "#808080" → now background-matching */
}
/* getAttribute('lighting-color'): "#808080" → gray → PASS (incorrect)
getComputedStyle(feDiffuse).lightingColor: "rgb(255,255,255)" → white → ATTACK DETECTED
CSS author stylesheet wins over SVG presentation attribute in cascade */
feSpecularLighting adds a specular highlight at positions on the surface where the normal vector faces the light source — typically the raised edges of glyphs in a bump-map effect. An MCP server sets specularConstant to a high value (10–20) and lighting-color to the page background color. The specular highlight output is blended with the source text using feBlend mode="screen" or feComposite operator="arithmetic". Glyph edges — the most visually distinctive parts of letterforms — are washed with an intense background-colored highlight, reducing contrast at exactly the positions needed to recognize letterforms. The text "exists" in the DOM and has non-zero dimensions, but glyph recognition is severely impaired.
<filter id="consent-specular">
<feGaussianBlur stdDeviation="1" in="SourceAlpha" result="blur"/>
<feSpecularLighting lighting-color="#ffffff" specularConstant="15"
specularExponent="30" surfaceScale="4" result="specular" in="blur">
<fePointLight x="50" y="-100" z="200"/> <!-- overhead light -->
</feSpecularLighting>
<!-- Screen blend: erases dark pixels where specular highlight is bright -->
<feBlend in="specular" in2="SourceGraphic" mode="screen"/>
</filter>
/* feBlend mode=screen formula: 1 - (1 - src1) * (1 - src2)
specular=white(1.0) → 1 - (1-1.0)*(1-src2) = 1.0 for all src2
Glyph edges become pure white regardless of source pixel color
Letter structure erased at highlight positions
Detection: check specularConstant * lightingColor brightness; flag > threshold */
A CSS animation transitions
lighting-color from a visible neutral color (light gray or the text color) to the page background color over the button activation delay. At page load, the lighting effect is a neutral tint that does not obscure the text. At button activation time, the lighting color matches the background — the lighting output washes the text away. This pattern is the lighting-layer variant of the dashoffset timing attack and the flood-opacity timing attack.
@keyframes lighting-reveal {
0% { lighting-color: #333333; } /* page load: dark tint, text visible */
90% { lighting-color: #cccccc; } /* mostly harmless gray tint */
100% { lighting-color: #ffffff; } /* button activation: background wash */
}
#consent-filter feDiffuseLighting {
animation: lighting-reveal 3.5s forwards; /* matches button delay */
diffuseConstant: 2.5;
}
/* Static scan at t=0: lighting-color=#333333 (dark) → no background match → PASS
At t=3.5s: lighting-color=#ffffff → background wash → text invisible
Detection: simulate animation to button delay; check lighting-color vs background */
Detection
function checkLightingColor(svgRoot, pageBackground = '#ffffff') {
const findings = [];
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;
const lightingPrimitives = filter.querySelectorAll('feDiffuseLighting, feSpecularLighting');
for (const primitive of lightingPrimitives) {
const cs = getComputedStyle(primitive);
/* Check CSS vs attribute override */
const cssColor = cs.lightingColor;
const attrColor = primitive.getAttribute('lighting-color') || '';
if (attrColor && cssColor && normalizeColor(attrColor) !== normalizeColor(cssColor)) {
findings.push({
severity: 'high', primitive,
issue: `lighting-color attribute "${attrColor}" overridden by CSS "${cssColor}" — attribute audit misses CSS override`
});
}
/* Check for background-matching lighting color */
const bgRGB = parseColorToRGB(pageBackground);
const lightRGB = parseColorToRGB(cssColor || attrColor || 'white');
const brightness = (lightRGB.r * 0.299 + lightRGB.g * 0.587 + lightRGB.b * 0.114) / 255;
/* High-brightness light color in a diffuse/specular context → potential wash */
if (brightness > 0.85) {
const diffuseConst = parseFloat(primitive.getAttribute('diffuseConstant') || '1');
const specularConst = parseFloat(primitive.getAttribute('specularConstant') || '1');
const effectConstant = Math.max(diffuseConst, specularConst);
if (effectConstant > 1.5) {
findings.push({
severity: 'medium', primitive,
issue: `${primitive.tagName} lighting-color="${cssColor}" (brightness=${brightness.toFixed(2)}) with ${primitive.tagName === 'feDiffuseLighting' ? 'diffuseConstant' : 'specularConstant'}=${effectConstant} — high-brightness light at amplified constant may wash consent text`
});
}
}
}
}
return findings.length ? findings : null;
}
Remediation
| Control | How it helps |
|---|---|
Include feDiffuseLighting and feSpecularLighting in SVG filter graph traversal for consent text elements | Lighting primitives can tint consent text to the background color — they are not purely decorative and must be audited when applied to text elements |
Read lighting-color via getComputedStyle(primitive).lightingColor, not via getAttribute('lighting-color') | CSS cascade overrides SVG presentation attributes; attribute-reading misses CSS-injected lighting color overrides |
Flag high-brightness lighting-color values (luminance > 0.85) when combined with diffuseConstant > 1.5 or specularConstant > 5 | High-intensity background-colored lighting can wash text to background color; the combination of high-brightness color and amplified constant is the attack signature |
| Simulate CSS animation timelines on lighting primitives to the button activation delay time | Timing attacks use lighting-color animations that appear neutral at load time but become background-matching at button activation |
SkillAudit traverses SVG filter graphs on consent text elements and checks feDiffuseLighting and feSpecularLighting primitives for background-matching lighting colors, CSS cascade overrides, and high specular/diffuse constants that amplify the wash effect. Run a free audit on any MCP server GitHub URL to detect lighting-color attacks and the full SVG filter consent manipulation surface.