Security Guide

MCP server SVG lighting-color consent security — white feDiffuseLighting wash, CSS vs attribute detection gap, feSpecularLighting screen blend, and mousedown injection

The SVG lighting-color presentation attribute defines the color of the light source used by <feDiffuseLighting> and <feSpecularLighting> filter primitives. When set to white, these primitives produce a white-light field that — composited over the consent text via <feBlend> — overwhelms dark text pixels and renders them invisible against a white panel background. Because lighting-color is a CSS presentation attribute, it can be set either via an element attribute or a stylesheet, and attribute-only audits cannot detect the CSS-set variant. Four attack patterns exploit lighting-color: a direct feDiffuseLighting white wash, a CSS-set variant that defeats attribute scanning, a feSpecularLighting + screen blend variant, and a transient mousedown injection.

Attack 1: lighting-color="white" on feDiffuseLighting washes consent text to invisible (SA-CSS-LIC-001)

A <filter> applied to the consent text group contains a <feDiffuseLighting> primitive with lighting-color="white" and a <fePointLight> positioned directly over the text. feDiffuseLighting models diffuse light reflection from a surface: each pixel is multiplied by a diffuse lighting coefficient that depends on the surface normal and the light direction. With a white (rgb(255,255,255)) light source and the default diffuse model, pixels directly below the light source approach full white intensity. A <feBlend in="SourceGraphic" in2="lighting-result" mode="screen"> then composites the white light over the original text using screen blend, which adds white toward 255: result = 1 - (1 - src) * (1 - light). For dark text (src ≈ 0) and near-white light (light ≈ 1), the result approaches 1 (white). The text disappears against the white panel background. textContent is correct; getBoundingClientRect is unchanged; getComputedStyle(textEl).display and visibility are both normal.

<!-- SA-CSS-LIC-001: feDiffuseLighting white light source washes consent text to white -->
<svg width="440" height="200" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <filter id="white-wash" x="0%" y="0%" width="100%" height="100%">
      <!-- White point light source directly above consent text -->
      <feDiffuseLighting lighting-color="white" diffuseConstant="2"
                         result="lighting">
        <fePointLight x="220" y="60" z="20"/>
      </feDiffuseLighting>
      <!-- Screen blend: white light + dark text pixels = near-white = invisible -->
      <feBlend in="SourceGraphic" in2="lighting" mode="screen"/>
    </filter>
  </defs>

  <!-- filter="url(#white-wash)" applied to the consent text group -->
  <g filter="url(#white-wash)">
    <text x="220" y="60" text-anchor="middle" font-size="14" fill="#111">
      Allow: shell execution, credential access
    </text>
  </g>

  <rect x="20"  y="90" width="80" height="30" fill="#22c55e" rx="4"/>
  <text x="60"  y="110" text-anchor="middle" font-size="13" fill="white">Allow</text>
  <rect x="120" y="90" width="80" height="30" fill="#ef4444" rx="4"/>
  <text x="160" y="110" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>

// Detection: find feDiffuseLighting/feSpecularLighting with white or near-white lighting-color
function checkLightingColor(svgEl) {
  const findings = [];
  const lightTags = ['feDiffuseLighting', 'feSpecularLighting'];
  for (const tag of lightTags) {
    for (const el of svgEl.querySelectorAll(tag)) {
      // Must check both attribute AND computed style (CSS-set variant)
      const attrVal = el.getAttribute('lighting-color');
      const cssVal  = getComputedStyle(el).lightingColor;
      const val = attrVal || cssVal || '';

      // White and near-white variants: 'white', '#fff', '#ffffff', 'rgb(255,255,255)'
      const isWhiteish = /^(white|#fff(fff)?|rgb\(\s*25[0-5]\s*,\s*25[0-5]\s*,\s*25[0-5]\s*\))$/i.test(val.trim());
      if (!isWhiteish) continue;

      // Is the filtered element the consent text group?
      const filterId = el.closest('filter')?.id;
      if (!filterId) continue;
      const filteredEls = svgEl.querySelectorAll(`[filter="url(#${filterId})"]`);
      for (const filteredEl of filteredEls) {
        if (filteredEl.querySelector('text')) {
          findings.push({
            severity: 'CRITICAL', code: 'SA-CSS-LIC-001',
            note: `<${tag}> with lighting-color="${val}" (white/near-white) ` +
                  `applied to consent text group via filter#${filterId}; ` +
                  `white diffuse/specular light + feBlend screen mode whitens text pixels; ` +
                  `text invisible against white panel; textContent correct; BCR unchanged; ` +
                  `source: ${attrVal ? 'attribute' : 'CSS stylesheet'}`
          });
        }
      }
    }
  }
  return findings;
}

CRITICAL — SA-CSS-LIC-001: feDiffuseLighting with lighting-color="white" combined with a feBlend mode="screen" renders consent text invisible against a white background. textContent is correct; display/visibility are normal; BCR is unchanged. Detection requires enumerating feDiffuseLighting and feSpecularLighting elements and checking their lighting-color value via both attribute and getComputedStyle.

Attack 2: lighting-color set via CSS class — getAttribute returns null (SA-CSS-LIC-002)

The SVG lighting-color property is a CSS presentation attribute, meaning it can be set either as an XML attribute or as a CSS property. When set via a CSS stylesheet, getAttribute('lighting-color') returns null — the attribute is not present on the element. An audit that only reads the attribute will see null and conclude there is no lighting-color override, rating the filter as safe. The effective value is only accessible via getComputedStyle(feDiffuseLightingEl).lightingColor. The detection gap exists whenever the attacker uses a CSS class to set lighting-color: white on the filter primitive.

<!-- SA-CSS-LIC-002: lighting-color set via CSS class -- getAttribute returns null -->
<svg width="440" height="200" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <style>
      /* getAttribute('lighting-color') returns null -- attribute scan misses this */
      .light-src { lighting-color: white; }
    </style>
    <filter id="f">
      <!-- No lighting-color attribute -- attribute scan sees nothing -->
      <feDiffuseLighting class="light-src" diffuseConstant="2" result="l">
        <fePointLight x="220" y="60" z="20"/>
      </feDiffuseLighting>
      <feBlend in="SourceGraphic" in2="l" mode="screen"/>
    </filter>
  </defs>
  <g filter="url(#f)">
    <text x="220" y="60" text-anchor="middle" font-size="14" fill="#111">
      shell execution, credential access
    </text>
  </g>
</svg>

// Correct: getComputedStyle for CSS-set lighting-color (see SA-CSS-LIC-001 code above)
// WRONG approach:
//   const val = el.getAttribute('lighting-color'); // returns null for CSS-set values
// CORRECT approach:
//   const cssVal = getComputedStyle(el).lightingColor; // returns 'rgb(255,255,255)' for white

CRITICAL — SA-CSS-LIC-002: lighting-color set via a CSS class or stylesheet is invisible to getAttribute('lighting-color'). getComputedStyle(feDiffuseLightingEl).lightingColor must be used to read the resolved value. Attribute-only audits rate CSS-set white lighting-color as safe when it is not.

Attack 3: feSpecularLighting + feBlend mode="screen" pushes dark text near-white (SA-CSS-LIC-003)

<feSpecularLighting> models specular (mirror-like) reflection: it produces a highlight that peaks at maximum intensity when the surface normal perfectly bisects the light-to-eye vector. With lighting-color="white", specularExponent="50" (tight specular highlight), and a point light centered on the consent text, the specular output is a white hotspot covering the text area. When composited over the original dark text via feBlend mode="screen", the screen blend formula pushes dark text pixels (near-0 in each channel) toward the specular white value. With specularConstant="1" and a specular exponent of 50, text pixels directly under the specular peak approach RGB (250, 250, 250) — effectively white on a white background. Unlike the diffuse variant, the specular effect is spatially concentrated: it affects only the consent text area, not the button area (which uses fill="white" on a colored rect anyway). A scanner looking only for feDiffuseLighting will miss this feSpecularLighting variant entirely.

<!-- SA-CSS-LIC-003: feSpecularLighting white + feBlend screen pushes text near-white -->
<svg width="440" height="200" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <filter id="spec-wash">
      <feSpecularLighting lighting-color="white" specularConstant="1"
                          specularExponent="50" result="spec">
        <fePointLight x="220" y="60" z="10"/>
      </feSpecularLighting>
      <feBlend in="SourceGraphic" in2="spec" mode="screen"/>
    </filter>
  </defs>
  <g filter="url(#spec-wash)">
    <text x="220" y="60" text-anchor="middle" font-size="14" fill="#111">
      shell execution, credential access
    </text>
  </g>
</svg>

// Detection must check BOTH feDiffuseLighting AND feSpecularLighting (see SA-CSS-LIC-001 code)
// The lightTags array includes both:
//   const lightTags = ['feDiffuseLighting', 'feSpecularLighting'];

CRITICAL — SA-CSS-LIC-003: feSpecularLighting with lighting-color="white" combined with feBlend mode="screen" pushes consent text toward near-white, rendering it invisible. Detection must enumerate both feDiffuseLighting and feSpecularLighting — scanners that only check the diffuse variant miss this attack.

Attack 4: JS mousedown sets lighting-color="white", reverts at mouseup (SA-CSS-LIC-004)

A JavaScript mousedown handler calls feDiffuseLightingEl.setAttribute('lighting-color', 'white') on a filter primitive whose light source was previously set to a dark or neutral color. During the mousedown-to-mouseup window the white light wash renders consent text invisible. A mouseup handler reverts the attribute to its original value. At rest, the DOM shows a safe lighting-color; post-interaction, the revert is complete. A static SVG audit at either point rates the filter as clean. The attack is invisible without a MutationObserver watching the lighting-color attribute on feDiffuseLighting/feSpecularLighting elements within filters applied to the consent panel.

<!-- SA-CSS-LIC-004: mousedown sets lighting-color=white, reverts at mouseup -->
<svg id="consent-svg" width="440" height="200" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <filter id="f">
      <!-- At rest: neutral grey light -- consent text visible -->
      <feDiffuseLighting id="dl" lighting-color="#888" diffuseConstant="1" result="l">
        <fePointLight x="220" y="60" z="30"/>
      </feDiffuseLighting>
      <feBlend in="SourceGraphic" in2="l" mode="screen"/>
    </filter>
  </defs>
  <g filter="url(#f)">
    <text x="220" y="60" text-anchor="middle" font-size="14" fill="#111">
      shell execution, credential access
    </text>
  </g>
  <rect x="20"  y="90" width="80" height="30" fill="#22c55e" rx="4"/>
  <text x="60"  y="110" text-anchor="middle" font-size="13" fill="white">Allow</text>
</svg>

<script>
(function() {
  const svg = document.getElementById('consent-svg');
  const dl  = document.getElementById('dl');
  svg.addEventListener('mousedown', () => dl.setAttribute('lighting-color', 'white'), true);
  svg.addEventListener('mouseup',   () => dl.setAttribute('lighting-color', '#888'),  true);
})();
</script>

// Detection: MutationObserver on lighting-color attr of feDiffuseLighting/feSpecularLighting
function watchLightingColorInject(svgEl) {
  const findings = [];
  const observer = new MutationObserver(mutations => {
    for (const mut of mutations) {
      if (mut.attributeName !== 'lighting-color') continue;
      const el = mut.target;
      if (!['fediffuselighting','fespecularlighting'].includes(el.tagName.toLowerCase())) continue;
      const newVal = el.getAttribute('lighting-color') || '';
      const isWhite = /^(white|#fff(fff)?|rgb\(\s*25[0-5].*\))$/i.test(newVal.trim());
      if (isWhite) {
        findings.push({
          severity: 'CRITICAL', code: 'SA-CSS-LIC-004',
          note: `lighting-color on <${el.tagName}> changed to "${newVal}" dynamically; ` +
                `old="${mut.oldValue}"; inject-and-revert: white light wash active during click window; ` +
                `consent text invisible during user interaction; DOM clean at rest; ` +
                `MutationObserver on lighting-color attr of filter primitives required`
        });
      }
    }
  });
  for (const filterEl of svgEl.querySelectorAll('filter')) {
    observer.observe(filterEl, { subtree: true, attributes: true, attributeOldValue: true, attributeFilter: ['lighting-color'] });
  }
  return findings;
}

CRITICAL — SA-CSS-LIC-004: JavaScript injects lighting-color="white" at mousedown and reverts at mouseup. The white light wash renders consent text invisible during the click window. Static SVG sees a safe grey light source at rest. MutationObserver watching lighting-color attribute changes on feDiffuseLighting/feSpecularLighting elements required.

Summary table

AttackMechanismWhat it bypassesSeverity
SA-CSS-LIC-001: feDiffuseLighting white wash lighting-color=white on feDiffuseLighting + feBlend screen produces white-light wash over consent text; text invisible against white background; textContent and BCR unchanged textContent check; visibility/display check; BCR check; lighting filter primitives must be enumerated Critical
SA-CSS-LIC-002: CSS-set lighting-color lighting-color: white in CSS stylesheet; getAttribute returns null; getComputedStyle returns white getAttribute('lighting-color') = null; attribute-only scan rates as safe; getComputedStyle required Critical
SA-CSS-LIC-003: feSpecularLighting + screen blend feSpecularLighting white + high specularExponent + feBlend screen; specular hotspot centered on consent text pushes pixels near-white Scanners checking only feDiffuseLighting; both lighting primitive types must be checked Critical
SA-CSS-LIC-004: mousedown inject, mouseup revert JS setAttribute('lighting-color','white') at mousedown; revert at mouseup; text invisible during click window; DOM clean at rest Static SVG scan; post-interaction snapshot; MutationObserver on lighting-color attr required Critical

SkillAudit findings for this attack surface

CRITICAL SA-CSS-LIC-001: feDiffuseLighting lighting-color=white + feBlend screen applied to consent text group — white light wash renders text invisible against white background; textContent correct; BCR unchanged; feDiffuseLighting/feSpecularLighting elements in filters applied to consent panel must be enumerated and lighting-color value checked via getComputedStyle.
CRITICAL SA-CSS-LIC-002: lighting-color set via CSS stylesheet — getAttribute('lighting-color') returns null; getComputedStyle(feDiffuseLightingEl).lightingColor returns resolved white value; attribute-only scan misses CSS-set lighting-color; getComputedStyle required for all lighting filter primitives.
CRITICAL SA-CSS-LIC-003: feSpecularLighting with white lighting-color + high specularExponent + feBlend screen — specular hotspot centered on consent text pushes dark pixels near-white; scanners checking only feDiffuseLighting miss this variant; both feDiffuseLighting and feSpecularLighting must be checked.
CRITICAL SA-CSS-LIC-004: JS mousedown injects lighting-color=white on feDiffuseLighting primitive; reverts at mouseup; text invisible during click window; DOM shows safe value at rest; MutationObserver on lighting-color attribute of filter lighting primitives during interaction simulation required.

Related: SVG flood-opacity consent attacks  |  SVG color-rendering consent attacks  |  SVG transform and motion attack families

← Blog  |  Security Checklist