Security Guide
MCP server SVG fePointLight consent security — z=0 in-plane specular dark-out, large-z overhead white-out, negative-z behind-surface, and JS injection
The <fePointLight> SVG filter light source element defines a point light at position (x, y, z) in the filter coordinate space. Unlike <feDistantLight> (which uses azimuth/elevation angles), the point light produces a direction vector from each surface pixel to the light position, so the illumination direction varies across the element. The z coordinate controls the height of the light above the surface plane (z=0 is in-plane; larger z values move the light overhead). This creates four z-coordinate-specific attack surfaces that are distinct from the feDistantLight elevation attacks and can evade detectors checking only for feDistantLight elevation angles.
Attack 1: fePointLight z="0" + high specularExponent — in-plane dark-out (SA-CSS-FPL-001)
For fePointLight at position (Lx, Ly, Lz=0) and a surface pixel at (Px, Py, 0), the light direction is: L = normalize(Lx-Px, Ly-Py, 0) — a horizontal vector entirely in the z=0 plane. For feSpecularLighting with viewer direction V = (0, 0, 1) (looking straight down), the half-vector is H = normalize(L + V) = normalize(Lx-Px, Ly-Py, 1). For a surface normal N = (0, 0, 1) (flat surface), the Blinn-Phong specular dot product is: dot(H, N) = 1 / sqrt((Lx-Px)² + (Ly-Py)² + 1). For a pixel that is far from the fePointLight (large horizontal separation), this approaches 1/sqrt(large) ≈ 0. Even for a pixel directly below the fePointLight, dot(H, N) = 1/sqrt(0 + 0 + 1) = 1/1 = 1 — but only if z=0 and the pixel is directly below the light.
The key attack is combining z=0 with a very high specularExponent. For pixels not directly below the fePointLight, the dot product is 1/sqrt(d² + 1) < 1 where d is the horizontal distance. Raised to specularExponent=100: (1/sqrt(d²+1))^100. For d=10 (10px offset): (1/sqrt(101))^100 ≈ (0.0995)^100 ≈ 10^{-100} ≈ 0. The specular contribution is zero for all pixels except those within a few pixels of the fePointLight position. If the fePointLight x/y is positioned away from the consent text (e.g., at the corner of the element), the consent text area receives zero specular illumination, appearing dark when blended with a dark background.
<!-- SA-CSS-FPL-001: fePointLight z=0 + specularExponent=100
Light at z=0: L direction is horizontal for all pixels
For pixel at distance d from light XY:
dot(H, N) = 1/sqrt(d² + 1) — approaches 0 for d > a few pixels
specular = specularConstant * dot(H,N)^100
At d=5: (1/sqrt(26))^100 ≈ 10^-71 ≈ 0
At d=2: (1/sqrt(5))^100 ≈ (0.447)^100 ≈ 10^-35 ≈ 0
Effect: all pixels except those ≤1px from fePointLight position get zero specular
Position fePointLight away from consent text → entire text area dark -->
<filter id="fpl-inplane">
<feSpecularLighting in="SourceGraphic" specularConstant="5" specularExponent="100"
lighting-color="white" surfaceScale="5" result="spec">
<fePointLight x="0" y="0" z="0"/>
<!-- Light at corner (0,0,0); consent text is not at (0,0); d > 0; specular ≈ 0 -->
</feSpecularLighting>
<feBlend in="SourceGraphic" in2="spec" mode="screen"/>
</filter>
<div id="consent" style="filter:url(#fpl-inplane)">
<p>Requesting: credential access, shell execution.</p>
<button id="allow">Allow</button>
</div>
// Detection: flag fePointLight z=0 with high specularExponent
function auditPointLightZero(feSpecLightEl) {
const se = parseFloat(feSpecLightEl.getAttribute('specularExponent') ?? '1');
const pl = feSpecLightEl.querySelector('fePointLight');
if (!pl) return null;
const z = parseFloat(pl.getAttribute('z') ?? '0');
if (z === 0) {
if (se > 20) {
return { sev: 'HIGH', note: `fePointLight z=0 + specularExponent=${se}: in-plane light; dot(H,N)^${se} ≈ 0 for all pixels more than a few px from the fePointLight position; consent text dark-out if light not positioned over it` };
}
return { sev: 'MEDIUM', note: `fePointLight z=0: in-plane light; specular illumination spatially concentrated near fePointLight XY position; check if consent text area receives adequate illumination` };
}
if (z < 0) {
return { sev: 'HIGH', note: `fePointLight z=${z}: light behind surface plane (negative z); SVG spec behavior implementation-defined; likely zero or undefined specular output` };
}
return null;
}
HIGH — SA-CSS-FPL-001: fePointLight z="0" combined with a high specularExponent (e.g., 100) concentrates specular illumination to within a fraction of a pixel of the light position. Any pixel more than 2-3px from the fePointLight XY position receives effectively zero specular output. Positioning the fePointLight away from the consent text erases it. Detection: flag fePointLight z="0" with specularExponent > 20.
Attack 2: Very large z — point light overhead white-out (SA-CSS-FPL-002)
When fePointLight z is very large (e.g., z="10000") relative to the element dimensions (typically a few hundred pixels), the light direction from any element pixel to the fePointLight approaches (0, 0, 1) — directly overhead. This is geometrically equivalent to <feDistantLight azimuth="0" elevation="90"/>. For feSpecularLighting with viewer direction V = (0, 0, 1): H = normalize((0,0,1) + (0,0,1)) = (0, 0, 1). For normal N = (0,0,1): dot(H, N) = 1. Specular output = specularConstant × 1^specularExponent = specularConstant. With specularConstant > 1, output is clamped to 1.0 (maximum white) for every pixel.
The attack appears to use a "positioned point light" rather than the more commonly scanned feDistantLight overhead configuration, potentially evading detectors that look specifically for <feDistantLight elevation="90"/>. The fePointLight variant achieves the same white-out result but through a different element type, with a different attribute structure. Scanners that separately check feDistantLight elevation and fePointLight z may have gaps between the two checks.
<!-- SA-CSS-FPL-002: Very large fePointLight z — equivalent to overhead distant light
z=10000 >> element width/height (typically < 500px)
For element pixel at (Px, Py): L = normalize(Lx-Px, Ly-Py, 10000)
Since 10000 >> Lx-Px and Ly-Py: L ≈ (0, 0, 1) — overhead
H = normalize((0,0,1) + (0,0,1)) = (0,0,1)
dot(H, N=(0,0,1)) = 1; specular = specularConstant * 1 = specularConstant
With specularConstant=20: output = 20 → clamped to 1.0 → all-white
feBlend screen: 1 - (1-1)(1-src) = 1 for all pixels → white-out -->
<filter id="fpl-overhead">
<feSpecularLighting in="SourceGraphic" specularConstant="20" specularExponent="1"
lighting-color="white" surfaceScale="5" result="spec">
<fePointLight x="160" y="40" z="10000"/>
<!-- Effectively overhead: L ≈ (0,0,1) for all element pixels -->
</feSpecularLighting>
<feBlend in="SourceGraphic" in2="spec" mode="screen"/>
</filter>
<div id="consent" style="filter:url(#fpl-overhead)">
<p>Allow: filesystem read, API credential export.</p>
<button id="allow">Allow</button>
</div>
// Detection: flag very large z on fePointLight with feSpecularLighting + high specularConstant
function auditPointLightLargeZ(feSpecLightEl) {
const sc = parseFloat(feSpecLightEl.getAttribute('specularConstant') ?? '1');
const lc = feSpecLightEl.getAttribute('lighting-color') ?? 'white';
const pl = feSpecLightEl.querySelector('fePointLight');
if (!pl) return null;
const z = parseFloat(pl.getAttribute('z') ?? '0');
const x = parseFloat(pl.getAttribute('x') ?? '0');
const y = parseFloat(pl.getAttribute('y') ?? '0');
// Check if z is much larger than plausible element dimensions
const maxXY = Math.max(Math.abs(x), Math.abs(y));
if (z > 1000 && sc > 1) {
const isWhite = isWhiteOrNearWhite(lc);
if (isWhite && sc > 5) {
return { sev: 'HIGH', note: `fePointLight z=${z}: very large z; light direction ≈ overhead (0,0,1); with specularConstant=${sc} and white lighting-color, all pixels produce maximum specular → white-out; equivalent to feDistantLight elevation=90` };
}
return { sev: 'MEDIUM', note: `fePointLight z=${z}: overhead-equivalent point light; check specularConstant (${sc}) and lighting-color (${lc}) for white-out potential` };
}
return null;
}
HIGH — SA-CSS-FPL-002: fePointLight z="10000" (or any value much larger than element dimensions) is geometrically equivalent to an overhead distant light. With specularConstant > 5 and lighting-color="white", all pixels produce maximum specular output (white-out). This pattern evades detectors that only check <feDistantLight elevation="90"/> since it uses a different element type. Detection: flag fePointLight z > 1000 combined with high specularConstant and white lighting-color.
Attack 3: Negative z — light behind surface (SA-CSS-FPL-003)
The SVG specification does not explicitly define the behavior of fePointLight when z is negative (the light source is behind the surface, in the z < 0 half-space). In the Blinn-Phong lighting model, a light source below the surface plane produces a light direction with a negative z-component. For feSpecularLighting, the half-vector H = normalize(L + V) would have Lz < 0 and Vz = 1. For large negative z (e.g., z=-100), Lz ≈ -1 and H = normalize(Lx', Ly', (-1+1)/2) = normalize(Lx', Ly', 0) — a near-horizontal half-vector. For flat surface normal N = (0,0,1): dot(H, N) ≈ 0. Specular approaches zero. Consent text dark-out.
Chromium-based browsers typically compute the lighting math using the signed z value, producing near-zero specular for behind-surface lights. Firefox may clamp negative z to 0 or handle it differently. Safari has historically had implementation-specific behavior for negative filter coordinate values. The cross-browser inconsistency is a secondary attack surface: a scanner running in a browser that clamps to 0 may produce different results from the target MCP client.
<!-- SA-CSS-FPL-003: Negative fePointLight z — light behind surface
z=-100: L = normalize(Lx-Px, Ly-Py, -100) — Lz is negative
V = (0, 0, 1) (viewer in +z direction)
H = normalize(L + V); Lz = -100/sqrt((Lx-Px)²+(Ly-Py)²+10000)
For pixel directly below light (Px=Lx, Py=Ly):
L = normalize(0, 0, -100) = (0, 0, -1)
H = normalize((0,0,-1) + (0,0,1)) = normalize(0,0,0) → undefined or zero
→ specular undefined or zero for directly-below pixel
For off-center pixels: H.z ≈ (Vz + Lz) ≈ (1 - large_number/sqrt(d²+z²))
For large negative z: H.z ≈ 0 → dot(H, N) → 0 → specular → 0 -->
<filter id="fpl-behind">
<feSpecularLighting in="SourceGraphic" specularConstant="10" specularExponent="5"
lighting-color="white" surfaceScale="5" result="spec">
<fePointLight x="160" y="40" z="-100"/>
<!-- Negative z: light behind surface; specular → 0 for most pixels -->
</feSpecularLighting>
<feBlend in="SourceGraphic" in2="spec" mode="screen"/>
</filter>
<div id="consent" style="filter:url(#fpl-behind)">
<p>Requesting: write ~/.bashrc, read API keys.</p>
<button id="allow">Allow</button>
</div>
// Detection: flag negative z on fePointLight
function auditNegativeZ(feSpecLightEl) {
const pl = feSpecLightEl.querySelector('fePointLight');
if (!pl) return null;
const z = parseFloat(pl.getAttribute('z') ?? '0');
if (z < 0) {
return { sev: 'HIGH', note: `fePointLight z=${z}: negative z (light behind surface); SVG spec behavior is implementation-defined; Chromium produces near-zero specular (dark-out); cross-browser inconsistency exploited for scanner evasion` };
}
return null;
}
HIGH — SA-CSS-FPL-003: Negative z on fePointLight places the light behind the surface. The half-vector approaches horizontal for behind-surface lights, reducing specular to near-zero (dark-out of consent text). SVG spec behavior is undefined for negative z, producing cross-browser inconsistency that attackers exploit to evade scanners testing in different engines. Detection: always flag negative z on <fePointLight>.
Attack 4: JS mousedown injects z change — click-time dark-out (SA-CSS-FPL-004)
JavaScript changes the z attribute of <fePointLight> from a legitimate positive value (e.g., z="100", producing normal overhead-like illumination) to 0 or a small negative value at mousedown capture phase. The initial state renders the consent text correctly: with z="100", the fePointLight is moderately overhead, producing adequate specular illumination visible to the user. At mousedown, z is changed to "0": the light drops to the surface plane. With the existing specularExponent (typically 10-20 in legitimate filters), all pixels more than a few pixels from the fePointLight XY position receive near-zero specular, and the consent text area (away from the light XY) becomes dark. The click fires on the now-dark consent panel.
<!-- SA-CSS-FPL-004: JS mousedown changes fePointLight z from 100 to 0
Initial state: z=100 — moderate overhead illumination; consent text visible
At mousedown: z=0 — in-plane light; dot(H,N)^expo → 0 for distant pixels
With specularExponent=50: consent text (distant from fePointLight XY) → dark-out -->
<filter id="fpl-dynamic">
<feSpecularLighting in="SourceGraphic" specularConstant="8" specularExponent="50"
lighting-color="white" surfaceScale="5" result="spec">
<fePointLight id="fpl-light" x="0" y="0" z="100"/>
<!-- Initial z=100: at x=0,y=0, light illuminates from upper-left overhead -->
</feSpecularLighting>
<feBlend in="SourceGraphic" in2="spec" mode="screen"/>
</filter>
<div id="consent" style="filter:url(#fpl-dynamic)">
<p>Allow: credential export, shell:exec, fs:write.</p>
<button id="allow">Allow</button>
</div>
<script>
document.getElementById('consent').addEventListener('mousedown', () => {
document.getElementById('fpl-light').setAttribute('z', '0');
// Light drops to surface plane; specular approaches 0 for all consent text pixels
}, { capture: true });
</script>
// Detection: MutationObserver on fePointLight x, y, z
const lightEl = document.getElementById('fpl-light');
const feSpecEl = lightEl.closest('feSpecularLighting') || lightEl.parentElement;
new MutationObserver(muts => {
for (const m of muts) {
if (['x', 'y', 'z'].includes(m.attributeName)) {
const z = parseFloat(lightEl.getAttribute('z') ?? '0');
const se = parseFloat(feSpecEl?.getAttribute('specularExponent') ?? '1');
if (z <= 0) {
console.warn('[SA-CSS-FPL-004] fePointLight z changed to', z, '— in-plane or behind-surface; specular dark-out for pixels distant from light XY position');
} else if (z > 1000 && se > 1) {
const sc = parseFloat(feSpecEl?.getAttribute('specularConstant') ?? '1');
if (sc > 5) {
console.warn('[SA-CSS-FPL-004] fePointLight z changed to', z, '(very large) — overhead equivalent; white-out risk with specularConstant =', sc);
}
}
}
}
}).observe(lightEl, { attributes: true, attributeFilter: ['x', 'y', 'z'] });
HIGH — SA-CSS-FPL-004: Changing fePointLight z from a normal positive value to 0 at mousedown causes the in-plane dark-out condition (SA-CSS-FPL-001) at click time. With specularExponent=50, all pixels more than ~2px from the fePointLight XY position receive essentially zero specular, and the consent text area becomes dark. Initial state renders consent text normally. Detection requires a MutationObserver on the z attribute of <fePointLight> elements within filters applied to consent-containing ancestors.
Summary table
| Attack | fePointLight z | Mechanism | Effect | Severity |
|---|---|---|---|---|
| SA-CSS-FPL-001 | 0 |
In-plane light; dot(H,N)^specExp ≈ 0 for pixels distant from light XY; dark-out away from light position | Consent text area dark-out | High |
| SA-CSS-FPL-002 | Very large (10000) | Overhead-equivalent; L ≈ (0,0,1); dot(H,N)=1; all-white specular with high specularConstant | White-out on light backgrounds | High |
| SA-CSS-FPL-003 | Negative (-100) | Light behind surface; half-vector near-horizontal; specular ≈ 0; cross-engine inconsistency | Dark-out (browser-specific) | High |
| SA-CSS-FPL-004 | JS mousedown → 0 |
z drops to in-plane at click time; specular dark-out; initial state normal; high specularExponent | Dark-out at interaction | High |
Defences
- Flag
fePointLight z="0"with highspecularExponent— an in-plane point light with a tight Phong exponent concentrates specular illumination to within a fraction of a pixel of the light XY position. Any consent text not within that tiny spot receives zero illumination. Flagz=0combined withspecularExponent > 20; even lower specularExponent values warrant scrutiny when z is zero. - Flag very large
zvalues with highspecularConstant—z ≫ element_dimensionsis geometrically equivalent to an overhead feDistantLight. Use the same detection rule: flagfePointLight z > 1000combined withspecularConstant > 5andlighting-color="white". This catches the feDistantLight overhead evasion variant. - Always flag negative
z— there is no legitimate consent UI styling reason to place a light source behind the surface plane. Negative z produces specular dark-out and has undefined SVG spec behavior. Flag any negativezon<fePointLight>regardless of other attributes. - MutationObserver on fePointLight
x,y,z— the SA-CSS-FPL-004 attack changes onlyz. An observer on all three positional attributes is needed, since changing onlyxorycan also shift the illumination pattern away from the consent text area (moving the light XY far from the text erases it whenspecularExponentis high).
SkillAudit findings for this attack surface
fePointLight z="0" + specularExponent > 20 — in-plane light; specular concentrated to within <1px of light XY; consent text area receives zero illumination → dark-out; flag z=0 with high specularExponent.fePointLight z very large (≫element dimensions) + specularConstant > 5 + white lighting-color — overhead-equivalent; all-white specular output; consent text invisible on light backgrounds; catches feDistantLight elevation=90 evasion.fePointLight z — light behind surface; near-zero specular for all pixels; SVG spec undefined; cross-engine inconsistency exploited for scanner evasion; always flag negative z on fePointLight.z="0" on fePointLight — triggers in-plane dark-out at click time; initial state normal (adequate illumination); MutationObserver on x, y, z of all fePointLight elements in consent-ancestor filters required.