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

AttackfePointLight zMechanismEffectSeverity
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

SkillAudit findings for this attack surface

HIGH SA-CSS-FPL-001: 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.
HIGH SA-CSS-FPL-002: 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.
HIGH SA-CSS-FPL-003: Negative 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.
HIGH SA-CSS-FPL-004: JS mousedown injects 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.

Related: feDistantLight elevation attacks  |  feSpotLight cone attacks  |  surfaceScale consent attacks  |  feConvolveMatrix kernel attacks

← Blog  |  Security Checklist