MCP server CSS feDistantLight security: elevation=90 maximum illumination whitewash, azimuth-calibrated stroke targeting, animated elevation activation, and CSS attribute override discrepancy

Published 2026-10-01 — SkillAudit Research

The SVG feDistantLight element defines a directional light source. Unlike fePointLight (which originates from a fixed position), a distant light has a uniform direction across the entire filter region — identical direction at every pixel. The light direction is specified by two angles: azimuth (degrees, horizontal direction measured counter-clockwise from the positive x-axis; 0° = right, 90° = up, 180° = left, 270° = down) and elevation (degrees above the surface plane; 0° = horizontal grazing, 90° = directly overhead).

When used inside feSpecularLighting, the illumination output per pixel is specularConstant × pow(dot(N, H), specularExponent), where H is the half-vector between the normalized light direction and the viewer direction (assumed (0,0,1) for a flat screen). At elevation=90°, the light direction is (0,0,1), so H = normalize((0,0,1) + (0,0,1)) = (0,0,1). The dot product with surface normal N=(0,0,1) = 1.0. For feDiffuseLighting, the diffuse contribution is diffuseConstant × dot(N, L) = diffuseConstant × 1.0. At maximum elevation, both produce their maximum possible output for every pixel.

Directional vs. positional light: feDistantLight illuminates the entire element uniformly — there is no falloff with distance from a center point. A single feDistantLight at elevation=90 with high specularConstant saturates the entire consent text element to white, not just a hotspot. This is a broader attack than fePointLight word-level targeting but is equally invisible to DOM property checks.

Elevation and diffuse dot product

The diffuse illumination per pixel for feDistantLight is:

N = (0, 0, 1)  // surface normal (flat screen)
L = (cos(elevation) × cos(azimuth),  cos(elevation) × sin(azimuth),  sin(elevation))
diffuse = diffuseConstant × dot(N, L) = diffuseConstant × sin(elevation)

At elevation=90°: sin(90°) = 1.0. Diffuse output = diffuseConstant × 1.0. If diffuseConstant=3, output = 3.0, clamped to 1.0 — fully white. The azimuth has no effect on diffuse intensity when elevation=90° (the light direction is straight down into the surface regardless of azimuth).

Attack findings

HIGH
feDistantLight elevation="90" with high diffuseConstant whitewashes entire consent element
At elevation=90°, the diffuse dot product is 1.0 for every pixel of the consent element (uniform directional light overhead). With feDiffuseLighting diffuseConstant="5" lighting-color="white", the output is 5.0 per channel — clamped to 1.0 (white). The feDiffuseLighting output is then composited over the SourceGraphic via feComposite operator="arithmetic" k2="1" k3="1", producing the original text plus the white illumination — effectively white on white. All DOM checks pass: layout box non-zero, fill reports original dark color, opacity=1. The attack appears identical to a subtle emboss or depth effect in the markup — feDistantLight with elevation near 90° is a common legitimate pattern for flat illumination; the high diffuseConstant is the distinguishing attack parameter.
<filter id="f1">
  <feDiffuseLighting diffuseConstant="5" lighting-color="white" result="light">
    <feDistantLight azimuth="45" elevation="90"/>
  </feDiffuseLighting>
  <feComposite in="SourceGraphic" in2="light" operator="arithmetic" k2="1" k3="1"/>
</filter>
HIGH
Azimuth calibrated to text stroke orientation — selective specular reflection destroys specific stroke visibility
At low elevation angles (e.g., elevation=5°), grazing illumination from feDistantLight interacts with the surface normal model of the SVG filter region. When the azimuth aligns with the dominant stroke direction of the consent text (e.g., azimuth=0° for horizontally-dominant strokes in sans-serif Latin text), feSpecularLighting produces maximum specular output along horizontal strokes — washing them to white while leaving vertical strokes relatively unaffected. The result is selective character feature destruction: horizontal crossbars of letters like "A", "H", "e", and "t" are erased while vertical strokes survive, making individual glyphs unrecognizable. This attack is subtle because the element appears to have "some" content visible — a rough check might pass — but the text is not legible. Detection requires evaluating stroke-level illumination contribution, not just element-level contrast.
MEDIUM
Animated feDistantLight elevation from 0 to 85 synchronized with button interaction
The feDistantLight starts at elevation="0" (horizontal grazing — minimal illumination contribution, negligible impact on consent text legibility). A <animate> element on the feDistantLight's elevation attribute transitions from 0 to 85 with begin="submitBtn.mouseenter" and fill="freeze". When the user moves their mouse over the submit button (before completing the click), the elevation rises to 85°, producing near-maximum illumination: sin(85°) = 0.9962. With diffuseConstant=4: output = 3.98, clamped to 1.0. The consent text element becomes white-on-white during the final interaction phase. A static DOM audit at load time reads elevation=0 — no attack. Only evaluating the animation element's target values and begin trigger reveals the attack state.
<feDistantLight azimuth="90" elevation="0">
  <animate attributeName="elevation" from="0" to="85"
    begin="submitBtn.mouseenter" dur="0.2s" fill="freeze"/>
</feDistantLight>
MEDIUM
CSS presentation attribute overrides SVG attribute elevation — markup inspection reads safe value
SVG presentation attributes (such as elevation) can be overridden by CSS. A feDistantLight element with elevation="5" in the markup and a CSS rule feDistantLight { elevation: 88; } in the document stylesheet renders with elevation=88 — producing near-maximum illumination — while static markup inspection or getAttribute('elevation') reads "5". The attack value is only visible via getComputedStyle(feDistantLightEl).elevation or by checking for CSS rules targeting light source elements. Many SVG security scanners read only attributes, not computed styles, and miss CSS overrides on SVG filter primitive child elements.

Detection algorithm

function detectFeDistantLightAttacks(consentEl) {
  const style = getComputedStyle(consentEl);
  const filterVal = style.filter;
  if (!filterVal || filterVal === 'none') return null;

  const filterId = filterVal.match(/url\(["']?#([^"')]+)["']?\)/)?.[1];
  if (!filterId) return null;

  const svgRoot = consentEl.closest('svg') || document;
  const filter = svgRoot.querySelector(`filter#${filterId}`);
  if (!filter) return null;

  const findings = [];

  for (const lightEl of filter.querySelectorAll('feDistantLight')) {
    const parent = lightEl.parentElement;
    if (!parent) continue;
    const isSpecular = parent.tagName.toLowerCase() === 'fespecularlighting';
    const isDiffuse  = parent.tagName.toLowerCase() === 'fediffuselighting';
    if (!isSpecular && !isDiffuse) continue;

    // Resolve elevation: check getComputedStyle first (CSS override), then getAttribute
    let elevation = parseFloat(getComputedStyle(lightEl).elevation
      ?? lightEl.getAttribute('elevation') ?? '0');

    // Also check for inline animate children targeting elevation
    const animateEls = Array.from(lightEl.querySelectorAll('animate, set'))
      .filter(a => a.getAttribute('attributeName') === 'elevation');

    let maxAnimatedElevation = elevation;
    for (const anim of animateEls) {
      const toVal = parseFloat(anim.getAttribute('to') ?? anim.getAttribute('values')?.split(';').pop() ?? '0');
      if (toVal > maxAnimatedElevation) maxAnimatedElevation = toVal;
    }

    const elevToCheck = maxAnimatedElevation;
    const sinElev = Math.sin(elevToCheck * Math.PI / 180);

    if (isDiffuse) {
      const diffConst = parseFloat(parent.getAttribute('diffuseConstant') ?? '1');
      const maxOutput = diffConst * sinElev;
      if (maxOutput >= 0.9) {
        findings.push({ severity: 'high', el: lightEl,
          issue: `feDistantLight elevation=${elevToCheck}°, diffuseConstant=${diffConst} → diffuse output ${maxOutput.toFixed(2)} (≥0.9) — consent text washed to white` });
      }
    }

    if (isSpecular) {
      const specConst = parseFloat(parent.getAttribute('specularConstant') ?? '1');
      const specExp   = parseFloat(parent.getAttribute('specularExponent') ?? '1');
      // At elevation=90, half-vector H = (0,0,1), dot(N,H) = 1.0, output = specConst
      const dotNH = sinElev; // simplified: elevation = 90 → dot(N,H) = 1
      const maxOutput = specConst * Math.pow(dotNH, specExp);
      if (maxOutput >= 0.9) {
        findings.push({ severity: 'high', el: lightEl,
          issue: `feDistantLight elevation=${elevToCheck}°, specularConstant=${specConst}, specularExponent=${specExp} → specular output ${maxOutput.toFixed(2)} — consent text overexposed to white` });
      }
    }

    // Check CSS-vs-attribute discrepancy
    const attrElevation = parseFloat(lightEl.getAttribute('elevation') ?? '0');
    const cssElevation  = parseFloat(getComputedStyle(lightEl).elevation ?? attrElevation.toString());
    if (Math.abs(cssElevation - attrElevation) > 5) {
      findings.push({ severity: 'medium', el: lightEl,
        issue: `feDistantLight elevation discrepancy: attribute="${attrElevation}" but computed="${cssElevation}" — CSS override hiding attack elevation value` });
    }

    // Check animate with high target elevation
    for (const anim of animateEls) {
      const toVal = parseFloat(anim.getAttribute('to') ?? '0');
      if (toVal > 60) {
        const begin = anim.getAttribute('begin') || '';
        findings.push({ severity: 'medium', el: anim,
          issue: `feDistantLight animate: elevation transitions to ${toVal}° (begin="${begin}") — static audit reads initial value; elevation rises to saturating at "${begin}" trigger` });
      }
    }
  }

  return findings.length ? findings : null;
}

Remediation

ControlHow it helps
For every feDistantLight in a consent element's filter, compute the maximum diffuse or specular illumination contribution using sin(elevation) (diffuse) or the specular half-vector formula; flag any combination of elevation and diffuseConstant/specularConstant that would produce output ≥ 0.9 for a white lighting-color Elevation whitewash attack — the relationship between elevation and illumination output is deterministic; simulating the lighting formula is sufficient to detect the attack without rendering; the 0.9 threshold allows a small safety margin for visible but non-saturating effects
Always resolve feDistantLight elevation via getComputedStyle (which incorporates CSS cascade), not only via getAttribute; compare the two values and flag discrepancies greater than 5° as potential CSS override attacks CSS attribute override — SVG presentation attributes can be overridden by the CSS cascade; attribute-only readers miss this attack; getComputedStyle on SVG child elements is supported in all modern browsers and reflects the fully-cascaded value
Enumerate all SVG animate and set child elements of feDistantLight that target the elevation attribute; evaluate the to value (or final value in a values sequence) and apply the same illumination formula to detect attacks that are triggered at interaction time; also check the begin attribute — user-interaction triggers ("click", "mouseenter") are high-risk Animated elevation timing attack — static DOM reads only the initial state; the attack state is reached at the begin trigger; only reading animate child elements reveals the maximum elevation the element will reach during use

SkillAudit evaluates feDistantLight elevation and constant parameters using the full diffuse/specular illumination formula, checks for CSS-vs-attribute elevation discrepancies, and scans animate children for interaction-triggered elevation changes on consent text filters. Run a free audit on any MCP server GitHub URL to detect feDistantLight consent text manipulation.