MCP server CSS feSpotLight security: narrow cone word targeting, pointsAtX/Y calibration, specularFocus beam hardening, and animated beam rotation across consent text

Published 2026-10-01 — SkillAudit Research

The SVG feSpotLight element defines a spotlight: a light source at a fixed (x, y, z) position that illuminates pixels within a cone defined by the pointsAt direction and the limitingConeAngle. The cone's axis runs from the light source position to the pointsAtX, pointsAtY, pointsAtZ target point. Only pixels whose direction from the source falls within the half-angle specified by limitingConeAngle receive any illumination — pixels outside the cone receive zero contribution from this light source.

The illumination falloff within the cone follows a cosine power law: spot = pow(dot(-L, S), specularFocus), where L is the light direction vector at the pixel and S is the normalized cone axis direction. High specularFocus values produce a sharper beam edge; specularFocus=0 produces uniform illumination across the entire cone. The spotlight contribution is then multiplied by the standard specular or diffuse formula.

Cone-based word selection: feSpotLight's limitingConeAngle defines how many pixels receive any illumination at all. A narrow cone (5°–10°) from a light source high above the element, pointing at a specific word center, illuminates only that word's bounding region. This provides finer-grained targeting than fePointLight's soft gaussian falloff — a narrow spotlight has a sharp boundary, matching word bounding boxes more precisely.

Spotlight cone geometry

For a feSpotLight at position (lx, ly, lz=200) pointing at (px, py, pz=0), the cone axis direction is normalize((px-lx, py-ly, pz-lz)). A pixel at (qx, qy, 0) is illuminated only if the angle between the direction from the source to the pixel and the cone axis is less than limitingConeAngle. For a light at z=200 pointing at a word center 200 units above the element plane, a limitingConeAngle=8° produces an illuminated circle of radius approximately 200 × tan(8°) ≈ 28px — sufficient to cover a single word but not the adjacent words.

Attack findings

HIGH
Narrow limitingConeAngle with pointsAt targeting specific consent clause word — single-word illumination saturation
A feSpotLight at position (lx=word_center_x, ly=-200, lz=200) pointing at (word_center_x, word_center_y, 0) with limitingConeAngle="8" creates a beam covering approximately 28px radius around the word center. With feSpecularLighting specularConstant="20" specularExponent="32", the specular output inside the cone saturates to white. The targeted word becomes white against the white background — contrast 1:1 — while the surrounding text, outside the 8° cone, receives zero illumination from this source and renders normally. An aggregate contrast check over the full element passes (most pixels are unilluminated). Only a per-word bounding-box measurement with the word "agree" or "authorize" as the analysis unit reveals the attack.
<!-- word "agree" at layout position x=142, y=18 within filter region -->
<filter id="f1">
  <feSpecularLighting specularConstant="20" specularExponent="32"
      lighting-color="white" result="spec">
    <feSpotLight x="142" y="-200" z="200"
      pointsAtX="142" pointsAtY="18" pointsAtZ="0"
      limitingConeAngle="8" specularFocus="4"/>
  </feSpecularLighting>
  <feComposite in="SourceGraphic" in2="spec" operator="arithmetic"
    k2="1" k3="1" k4="0"/>
</filter>
HIGH
pointsAtX/Y derived from word layout coordinates — consent clause word targeting via getBoundingClientRect measurements
Attackers compute the target word's pixel position by measuring the consent element's layout at page load and setting the feSpotLight's pointsAtX and pointsAtY as integer pixel coordinates relative to the filter's user-space coordinate system. A 200px-wide consent element where the word "authorize" starts at offset 63px from the left and the element center is at y=10px in filter space: pointsAtX = 63 + word_width/2 = 87, pointsAtY = 10. This requires either static layout knowledge (hardcoded for a fixed viewport) or dynamic measurement. For a fixed-layout MCP server consent UI, the coordinates can be hardcoded for the target deployment viewport size. The SVG filter source markup contains integer coordinate values that are indistinguishable from coordinate noise in a filter optimized for visual effect.
MEDIUM
High specularFocus sharpens cone edge to match word bounding box boundaries
The specularFocus (or focus) attribute controls the beam edge sharpness. specularFocus="1" produces a gradual cosine falloff from the cone center to the cone edge. specularFocus="32" concentrates almost all illumination within the inner half of the cone — the beam edge is sharp, matching word bounding box boundaries. Combined with a limitingConeAngle calibrated to cover the target word's bounding box, the high specularFocus produces a near-rectangular illuminated region (matching word bounds) with sharp edges, rather than a soft circular falloff. This reduces illumination "leakage" onto adjacent words, maintaining their legibility while the targeted word is erased — making the aggregate contrast check even more likely to pass.
MEDIUM
SVG animate rotates feSpotLight pointsAt direction across consent text line at interaction time
The feSpotLight starts pointing far outside the consent element (pointsAtX="-500" pointsAtY="0" — beam points left, illumination falls outside filter region). An SVG animate element changes pointsAtX from -500 to the target word's x coordinate when begin="submitBtn.mouseenter" triggers, with dur="0s" (instantaneous) and fill="freeze". The spotlight beam instantly teleports to the target word position the moment the user's cursor touches the submit button, erasing the target word before the click is completed. A static DOM audit reads the initial pointsAtX="-500" — the light points off-screen, no consent text is affected. Only evaluating the animate element's to value in combination with the begin trigger type reveals the attack.
<feSpotLight x="142" y="-200" z="200" limitingConeAngle="8"
    pointsAtX="-500" pointsAtY="18" pointsAtZ="0">
  <animate attributeName="pointsAtX" from="-500" to="87"
    begin="submitBtn.mouseenter" dur="0s" fill="freeze"/>
</feSpotLight>

Detection algorithm

function detectFeSpotLightAttacks(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 consentRect = consentEl.getBoundingClientRect();
  const elW = consentRect.width;
  const elH = consentRect.height;
  const findings = [];

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

    const lx  = parseFloat(lightEl.getAttribute('x')  ?? '0');
    const ly  = parseFloat(lightEl.getAttribute('y')  ?? '0');
    const lz  = parseFloat(lightEl.getAttribute('z')  ?? '100');

    // Resolve pointsAt — check animate children too
    let pAtX = parseFloat(lightEl.getAttribute('pointsAtX') ?? '0');
    let pAtY = parseFloat(lightEl.getAttribute('pointsAtY') ?? '0');

    const pAtXAnims = Array.from(lightEl.querySelectorAll('animate,set'))
      .filter(a => a.getAttribute('attributeName') === 'pointsAtX');
    const pAtYAnims = Array.from(lightEl.querySelectorAll('animate,set'))
      .filter(a => a.getAttribute('attributeName') === 'pointsAtY');

    // Check if animated pointsAt values fall within consent element
    for (const anim of [...pAtXAnims, ...pAtYAnims]) {
      const toVal = parseFloat(anim.getAttribute('to') ?? '');
      const begin = anim.getAttribute('begin') || '';
      const isInteraction = /click|mouseenter|mousedown|focus/.test(begin);
      if (!isNaN(toVal) && isInteraction) {
        const animAttr = anim.getAttribute('attributeName');
        findings.push({ severity: 'medium', el: anim,
          issue: `feSpotLight animate: ${animAttr} transitions to ${toVal} (begin="${begin}") — spotlight beam moves to consent element region at user interaction; static audit reads initial off-screen value` });
      }
    }

    const coneAngle = parseFloat(lightEl.getAttribute('limitingConeAngle') ?? '90');
    const focus     = parseFloat(lightEl.getAttribute('specularFocus') ?? lightEl.getAttribute('focus') ?? '1');

    // Compute beam radius at the element plane (z=0)
    const dist = Math.sqrt((pAtX-lx)**2 + (pAtY-ly)**2 + lz**2);
    const beamRadiusAtPlane = dist * Math.tan(coneAngle * Math.PI / 180);

    // Check if beam center (pointsAt) is within consent element filter region
    const inElX = pAtX >= 0 && pAtX <= elW;
    const inElY = pAtY >= 0 && pAtY <= elH;

    if (inElX && inElY) {
      const specConst = parseFloat(parent.getAttribute('specularConstant') ?? '1');
      const specExp   = parseFloat(parent.getAttribute('specularExponent') ?? '1');
      const maxOutput = isSpecular ? specConst : parseFloat(parent.getAttribute('diffuseConstant') ?? '1');

      if (maxOutput >= 1.0) {
        findings.push({
          severity: coneAngle < 20 ? 'high' : 'medium',
          el: lightEl,
          issue: `feSpotLight pointsAt=(${pAtX},${pAtY}) within consent element region; coneAngle=${coneAngle}° → beam radius ~${beamRadiusAtPlane.toFixed(0)}px; maxOutput=${maxOutput.toFixed(1)} — ${coneAngle < 20 ? 'word-level' : 'broad'} saturation attack`
        });
      }
    }
  }

  return findings.length ? findings : null;
}

Remediation

ControlHow it helps
For every feSpotLight in a consent element's filter, compute the beam center position (pointsAtX, pointsAtY) and beam radius from limitingConeAngle and source height (z); if the beam center falls within the consent element's filter region, evaluate the specular/diffuse output at the beam center and flag any combination that produces saturating output Narrow cone word targeting — the beam radius formula (dist × tan(limitingConeAngle)) determines the covered area; a radius less than ~30% of the element width indicates word-level targeting; pointsAt coordinates within the element region confirm targeting
Enumerate SVG animate and set children on feSpotLight elements that target pointsAtX, pointsAtY, or limitingConeAngle; for any animation with a user-interaction begin trigger (click, mouseenter, mousedown, focus), evaluate whether the final value places the beam center within the consent element's region Animated beam targeting — the attack is only active after the begin trigger fires; static DOM inspection reads the initial safe pointsAt value; only reading the animate element's to attribute combined with the begin trigger type reveals the attack state
Compute the per-word illumination coverage by projecting the beam cone onto each word's bounding box and checking whether the word center falls within the cone radius; flag any word whose center is within the beam radius if the beam would produce saturating output at that position; compare targeted word coverage (typically ≤ 20% of the element) against total element coverage Sub-element word-level detection — aggregate contrast measurements pass because most pixels are outside the cone; per-word projection maps the beam geometry to individual word positions, revealing selective erasure that aggregate checks miss

SkillAudit projects feSpotLight beam geometry onto word-level bounding regions, evaluates illumination saturation at each word center, and scans animate children for interaction-triggered pointsAt targeting within consent element bounds. Run a free audit on any MCP server GitHub URL to detect feSpotLight word-level consent erasure.