Security Guide

MCP server SVG feDistantLight consent security — elevation="0" zero-illumination dark-out, grazing-angle attacks, specular white-out, and JS mousedown injection

The <feDistantLight> element is a child of <feDiffuseLighting> or <feSpecularLighting> that defines a directional light source at infinite distance. Its two attributes, azimuth (compass heading 0–360°) and elevation (angle above the XY plane, −90 to 90°), control the direction of incoming light. For feDiffuseLighting, the output intensity at each pixel is proportional to diffuseConstant × max(0, dot(lightDir, surfaceNormal)). When elevation="0", the light direction is horizontal: the dot product with the vertical surface normal is exactly zero, so the entire filter output is black. When elevation="90" paired with a large specularConstant, the specular output goes to maximum — white — washing out consent text against white backgrounds. Four attack patterns follow.

Attack 1: feDistantLight elevation="0" — zero diffuse illumination, black output (SA-CSS-FDL-001)

For a flat surface (all pixels have the same outward normal (0,0,1)), the diffuse shading formula is: I_diffuse = diffuseConstant × lighting-color × max(0, dot(L, N)). The light direction for feDistantLight in 3D is: L = (cos(elevation)×cos(azimuth), cos(elevation)×sin(azimuth), sin(elevation)). When elevation=0°: L = (cos(azimuth), sin(azimuth), 0) — the light is purely horizontal. dot(L, N) = dot((x,y,0),(0,0,1)) = 0. The diffuse output is zero for every pixel. The filter outputs a solid black image regardless of what the consent text contains.

When this black output is piped through feComposite operator="arithmetic" k3="1" to copy the lighting result, or composited via feBlend mode="multiply" over the consent element, the result is black text on a black or dark background — invisible. The attack is effective against dark-themed MCP clients (VS Code dark theme, terminal emulators, any UI with background: #0a0a0a or similar).

<!-- SA-CSS-FDL-001: feDistantLight elevation=0 — zero diffuse illumination
     L = (cos(0)=1, 0, sin(0)=0) for azimuth=0, elevation=0
     dot(L, N=(0,0,1)) = 0 → diffuse = diffuseConstant × 0 = 0
     Output is solid black; all consent text pixels have RGB=(0,0,0) -->
<filter id="fdl-zero">
  <feDiffuseLighting in="SourceGraphic" diffuseConstant="1" surfaceScale="1" result="lighting">
    <feDistantLight azimuth="0" elevation="0"/>
  </feDiffuseLighting>
  <feComposite in="SourceGraphic" in2="lighting" operator="arithmetic" k1="0" k2="0" k3="1" k4="0"/>
</filter>

<div id="consent" style="filter:url(#fdl-zero)">
  <p>Requesting: filesystem read, credential access, network egress.</p>
  <button id="allow">Allow</button>
</div>

// Detection: check elevation attribute for zero or near-zero values
function auditFeDistantLight(lightEl, parentFilter) {
  const elevation = parseFloat(lightEl.getAttribute('elevation') ?? '0');
  const parentTag = lightEl.parentElement?.tagName?.toLowerCase();
  if (parentTag === 'fediffuselighting') {
    // diffuse illumination = max(0, sin(elevation_rad))
    const elevRad = elevation * Math.PI / 180;
    const diffuseFactor = Math.max(0, Math.sin(elevRad));
    if (diffuseFactor < 0.05) {
      return { sev: 'HIGH', note: `feDistantLight elevation=${elevation}° → diffuse factor ${diffuseFactor.toFixed(3)} — near-zero illumination` };
    }
  }
  return null;
}

HIGH — SA-CSS-FDL-001: feDistantLight elevation="0" inside feDiffuseLighting produces zero illumination at every pixel. The lighting filter outputs solid black. When composited over or instead of consent text in dark-themed UIs, the text becomes invisible. Detection requires computing the expected diffuse illumination factor from the elevation angle, not just checking for the element's presence.

Attack 2: feDistantLight elevation="-1" — below-horizon grazing light, near-zero diffuse (SA-CSS-FDL-002)

The SVG spec allows negative elevation values (light source below the XY plane). For elevation="-1": sin(-1°) = -0.0175, so max(0, sin(-1°)) = 0. Any negative elevation produces zero or negative sine, clamped to zero by the max(0,...) in the diffuse formula. The output is again solid black — but the attribute value -1 evades exact-zero detection that checks elevation === "0". A scanner that flags elevation="0" will miss elevation="-1", elevation="-45", and elevation="-89", all of which produce zero diffuse illumination.

This attack is functionally equivalent to FDL-001 for diffuse lighting (zero illumination either way), but the choice of a non-zero negative attribute value deliberately targets scanners that pattern-match on exact attribute values. The correct detection is evaluating sin(elevation_degrees × π / 180) and comparing the result to a threshold, not string-matching the attribute.

<!-- SA-CSS-FDL-002: feDistantLight elevation=-45 — below horizon, zero diffuse
     sin(-45°) = -0.707 → max(0, -0.707) = 0
     Same zero-illumination output as elevation=0 but evades equality-check detection -->
<filter id="fdl-negative">
  <feDiffuseLighting in="SourceGraphic" diffuseConstant="2" surfaceScale="1" result="lighting">
    <feDistantLight azimuth="90" elevation="-45"/>
  </feDiffuseLighting>
  <feComposite in="lighting" in2="SourceGraphic" operator="in"/>
</filter>

// Note: feComposite in= clips lighting result to the alpha of SourceGraphic.
// Even though in= uses SourceGraphic alpha as a mask, the RGB values come from
// the lighting result (black) — consent text appears black regardless of text color.

// Correct detection: evaluate sin, do not string-match
function checkElevationIllumination(elevation_deg) {
  const elevRad = elevation_deg * Math.PI / 180;
  return Math.max(0, Math.sin(elevRad)); // returns 0 for any elevation ≤ 0
}

HIGH — SA-CSS-FDL-002: Any negative elevation value produces zero diffuse illumination. Scanners that check for elevation="0" by string equality miss elevation="-1" through elevation="-90". The correct check evaluates max(0, sin(elevation × π/180)) and flags values below a threshold (e.g., 0.05, corresponding to elevation <2.9°).

Attack 3: feDistantLight elevation="90" + high specularConstant — specular white-out (SA-CSS-FDL-003)

For feSpecularLighting, the output formula is: I_specular = specularConstant × max(0, dot(H, N))^specularExponent × lighting-color, where H is the half-vector between the light direction and the view direction. Per SVG spec, the viewer is at position (x,y,z=∞), so the view direction is (0,0,1). For feDistantLight elevation=90°, the light direction is also (0,0,1) (straight down). The half-vector H = normalize(L + V) = normalize((0,0,1)+(0,0,1)) = (0,0,1). On a flat surface, dot(H,N) = dot((0,0,1),(0,0,1)) = 1. The specular output is: specularConstant × 1^specularExponent × lighting-color.

With specularConstant="20" and lighting-color="white", the output is 20 × 1 × 1 = 20, clamped to 1.0 (maximum white). The entire consent element is overridden with full white. Against a white page background, consent text disappears entirely. The large specularConstant is what forces the clamp — any value above 1.0 (after dot product and exponent) would be sufficient, but large values ensure all pixels exceed the clamp threshold even if the element has non-flat alpha-channel normals.

<!-- SA-CSS-FDL-003: feDistantLight elevation=90 + feSpecularLighting specularConstant=20
     H = normalize((0,0,1)+(0,0,1)) = (0,0,1); dot(H,N=(0,0,1)) = 1
     I_specular = 20 * 1^1 * white = 20 → clamped to 1.0 (all-white output)
     Consent text invisible against white page backgrounds -->
<filter id="fdl-specwhite">
  <feSpecularLighting in="SourceGraphic" specularConstant="20" specularExponent="1"
                      lighting-color="white" surfaceScale="1" result="spec">
    <feDistantLight azimuth="0" elevation="90"/>
  </feSpecularLighting>
  <feBlend in="SourceGraphic" in2="spec" mode="screen"/>
</filter>

// feBlend mode="screen" formula: C_out = 1 - (1-C_src)*(1-C_dest)
// C_spec = 1.0 (all-white from feSpecularLighting)
// C_out = 1 - (1-C_original)*(1-1) = 1 - (1-C_original)*0 = 1
// All pixels become white regardless of original consent text color.

// Detection: compute specular output for flat surface + overhead light
function checkSpecularWhiteout(specLightingEl) {
  const sc = parseFloat(specLightingEl.getAttribute('specularConstant') ?? '1');
  const se = parseFloat(specLightingEl.getAttribute('specularExponent') ?? '1');
  const lightingColor = specLightingEl.getAttribute('lighting-color') ?? 'white';
  const distantLight = specLightingEl.querySelector('feDistantLight');
  if (!distantLight) return null;
  const elevation = parseFloat(distantLight.getAttribute('elevation') ?? '0');
  const elevRad = elevation * Math.PI / 180;
  // For flat surface, dot(H,N) ≈ sin(elevation/2+45°) (simplified; exact formula depends on view)
  // At elevation=90°: half-vector = (0,0,1), dot=1
  const dotHN = Math.sin((elevation/2 + 45) * Math.PI / 180); // approximation
  const specOutput = Math.min(1, sc * Math.pow(Math.max(0, dotHN), se));
  if (specOutput > 0.9 && isWhiteColor(lightingColor)) {
    return { sev: 'HIGH', note: `feSpecularLighting specularConstant=${sc}: specular output=${specOutput.toFixed(2)} → white-out on white backgrounds` };
  }
  return null;
}

HIGH — SA-CSS-FDL-003: feDistantLight elevation="90" with feSpecularLighting specularConstant="20" and lighting-color="white" produces an all-white output for every pixel of a flat surface. When blended over consent text via feBlend mode="screen" or composited directly, the text is invisible against white backgrounds. Detection requires computing the specular formula for the overhead-light + flat-surface case: specularConstant × 1^specularExponent × 1; flag any result >0.9 paired with a white lighting-color.

Attack 4: JS mousedown injects feDistantLight elevation change (SA-CSS-FDL-004)

JavaScript listens at capture phase for mousedown on the consent element. On mousedown, it modifies the feDistantLight's elevation attribute from a normal value (e.g., 45) to 0 (for dark-background clients) or 90 (for white-background clients). The initial state shows correctly illuminated consent text; the filter output shifts just as the user completes the click gesture. Because the filter state is valid at page load and during passive viewing, no pre-interaction static analysis will catch the attack without runtime monitoring.

<!-- SA-CSS-FDL-004: JS mousedown injects feDistantLight elevation change
     Initial state: elevation=45 (normal diffuse lighting, text visible)
     On mousedown: elevation changed to 0 → zero illumination → black output -->
<filter id="fdl-dynamic">
  <feDiffuseLighting in="SourceGraphic" diffuseConstant="1" surfaceScale="1">
    <feDistantLight id="fdl-light" azimuth="45" elevation="45"/>
  </feDiffuseLighting>
</filter>
<div id="consent" style="filter:url(#fdl-dynamic)">
  <p>Requesting: filesystem access, credential read.</p>
  <button id="allow">Allow</button>
</div>
<script>
document.getElementById('consent').addEventListener('mousedown', () => {
  document.getElementById('fdl-light').setAttribute('elevation', '0');
}, { capture: true });
</script>

// Detection: MutationObserver on feDistantLight elevation and azimuth attributes
const lightEl = document.getElementById('fdl-light');
new MutationObserver(muts => {
  for (const m of muts) {
    if (m.attributeName === 'elevation' || m.attributeName === 'azimuth') {
      const elevation = parseFloat(lightEl.getAttribute('elevation') ?? '0');
      const elevRad = elevation * Math.PI / 180;
      const diffuseFactor = Math.max(0, Math.sin(elevRad));
      if (diffuseFactor < 0.05) {
        console.warn('[SA-CSS-FDL-004] feDistantLight elevation changed to near-zero during interaction:', elevation);
      }
    }
  }
}).observe(lightEl, { attributes: true, attributeFilter: ['elevation', 'azimuth'] });

HIGH — SA-CSS-FDL-004: JS mousedown injection of feDistantLight elevation="0" shifts consent text to zero illumination (black) at the moment of user click. Initial page state is valid. Detection requires a MutationObserver installed on all <feDistantLight> elements before interaction, watching elevation and azimuth attribute changes and evaluating the resulting diffuse/specular illumination factor.

Summary table

AttackMechanismBackground requiredSeverity
SA-CSS-FDL-001: feDistantLight elevation="0" Horizontal light; dot(L,N)=0; feDiffuseLighting output = black; consent text invisible in dark-themed clients Dark-themed MCP clients (#0a0a0a, dark gray backgrounds) High
SA-CSS-FDL-002: feDistantLight elevation="-45" (negative) Negative elevation → sin(neg) → max(0,neg)=0 → same zero-illumination; evades string-equality elevation=0 check Same as FDL-001; detection requires formula evaluation not string match High
SA-CSS-FDL-003: elevation="90" + feSpecularLighting specularConstant="20" Overhead light + high specular constant; H=(0,0,1), dot(H,N)=1; output=max (white); feBlend screen over consent = all-white White or near-white page backgrounds High
SA-CSS-FDL-004: JS mousedown elevation injection Elevation changed from normal to 0 at mousedown capture phase; initial state valid; ~50ms attack window Dark-themed clients (elevation→0) or white-themed clients (elevation→90 with high specularConstant) High

Defences

SkillAudit findings for this attack surface

HIGH SA-CSS-FDL-001: feDistantLight elevation="0" inside feDiffuseLighting — zero illumination; consent text rendered black; invisible on dark-themed MCP clients; detection requires sin(elevation) formula evaluation.
HIGH SA-CSS-FDL-002: Negative elevation attribute (e.g., "-45") — sin(neg)=neg; max(0,neg)=0; zero diffuse illumination same as FDL-001; evades string equality check on "0".
HIGH SA-CSS-FDL-003: feDistantLight elevation="90" with feSpecularLighting specularConstant="20" lighting-color="white" — all-white specular output via overhead-light geometry; feBlend screen or feComposite arithmetic causes white-out; invisible on white backgrounds.
HIGH SA-CSS-FDL-004: JS mousedown injection of elevation=0 or elevation=90 on feDistantLight — initial state valid; attack window ~50ms; MutationObserver on elevation/azimuth attributes required.

Related: feDiffuseLighting consent attacks  |  feSpecularLighting consent attacks  |  feSpotLight cone attacks  |  lighting-color white-out attacks

← Blog  |  Security Checklist