Security Guide

MCP server SVG surfaceScale consent security — feSpecularLighting flat-surface white-out, extreme value glyph-edge normal collapse, feDiffuseLighting dark-out, and JS mousedown injection

The surfaceScale attribute of <feSpecularLighting> and <feDiffuseLighting> scales the height of the bump-map derived from the alpha channel of the input image. The SVG spec formula uses surfaceScale as a multiplier when computing surface normals from the alpha-channel gradient. With surfaceScale="0", all height differences are zero — the surface is perfectly flat with normals uniformly pointing in the (0,0,1) direction. With very large values (e.g., 10000), glyph-edge alpha gradients produce near-horizontal normals, causing extreme shading variation at text boundaries. Both extremes create consent-text invisibility attack surfaces when combined with appropriate light sources.

Attack 1: surfaceScale="0" + overhead feDistantLight + high specularConstant — flat-surface white-out (SA-CSS-SSC-001)

When surfaceScale="0", the surface normal is uniformly (0,0,1) for every pixel (no bump mapping). For feSpecularLighting with a feDistantLight at elevation="90" (overhead), the light direction is also (0,0,1). The viewer direction is (0,0,1) (looking straight down). The half-vector H = normalize(L + V) = normalize((0,0,1)+(0,0,1)) = (0,0,1). The dot product dot(H, N) = 1. The specular output: I = specularConstant × 1^specularExponent × lighting-color.

With specularConstant="20" and lighting-color="white": I = 20 × 1 × 1 = 20, clamped to 1.0. Every pixel produces maximum white. When this is blended over consent text via feBlend mode="screen", the formula is C_out = 1 - (1-C_spec)(1-C_src) = 1 - 0 × (1-C_src) = 1. All pixels become white regardless of the consent text content. Consent text is invisible against white backgrounds.

<!-- SA-CSS-SSC-001: surfaceScale=0 + feDistantLight elevation=90 + specularConstant=20
     Normal = (0,0,1) everywhere; L = (0,0,1); V = (0,0,1); H = (0,0,1)
     dot(H,N) = 1; specular = 20 * 1^1 * white = 20 → clamped to 1.0 (all-white)
     feBlend screen: C_out = 1 for all pixels; consent text invisible on white bg -->
<filter id="ssc-white">
  <feSpecularLighting in="SourceGraphic" specularConstant="20" specularExponent="1"
                      lighting-color="white" surfaceScale="0" result="spec">
    <feDistantLight azimuth="0" elevation="90"/>
  </feSpecularLighting>
  <feBlend in="SourceGraphic" in2="spec" mode="screen"/>
</filter>

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

// Detection: check surfaceScale=0 + specularLighting with high specularConstant
function auditSurfaceScale(lightingEl) {
  const ss = parseFloat(lightingEl.getAttribute('surfaceScale') ?? '1');
  if (ss === 0) {
    const tag = lightingEl.tagName.toLowerCase();
    if (tag === 'fespecularlighting') {
      const sc = parseFloat(lightingEl.getAttribute('specularConstant') ?? '1');
      const lc = lightingEl.getAttribute('lighting-color') ?? 'white';
      if (sc > 5 && isWhiteOrNearWhite(lc)) {
        return { sev: 'HIGH', note: `feSpecularLighting surfaceScale=0: flat surface; specularConstant=${sc} white lighting-color → all-white output via overhead-light specular max` };
      }
    }
    if (tag === 'fediffuselighting') {
      return { sev: 'HIGH', note: `feDiffuseLighting surfaceScale=0: flat surface; illumination depends entirely on feDistantLight elevation — check for zero-illumination combination` };
    }
  }
  return null;
}

HIGH — SA-CSS-SSC-001: surfaceScale="0" on feSpecularLighting with an overhead light and high specularConstant produces all-white output because the flat surface produces maximum specular response at the overhead-light geometry. Consent text invisible on white backgrounds. Detection: flag surfaceScale=0 on feSpecularLighting with specularConstant > 5 and white lighting-color.

Attack 2: Very large surfaceScale + horizontal feDistantLight — glyph-edge normal collapse (SA-CSS-SSC-002)

The SVG spec computes surface normals from the height field derived from the input's alpha channel. The normal at each pixel is proportional to the gradient of the alpha channel multiplied by surfaceScale. At glyph edges — where the alpha channel transitions from 0 (background) to 1 (text fill) over a few pixels — the alpha gradient is large. With surfaceScale="10000", this gradient is scaled by 10000, producing a near-horizontal normal (pointing almost sideways, in the XY plane rather than the Z direction).

For feDiffuseLighting with a horizontal feDistantLight (elevation=0), the diffuse formula for the interior of a glyph is: diffuse = max(0, dot(L_horizontal, N_vertical)) = max(0, 0) = 0 — zero illumination for flat interior. But with extreme surfaceScale, even the interior pixels may be affected by the large normal artifacts. The glyph edges get normals nearly parallel to the light direction (if the glyph is oriented correctly), producing high illumination at edges but zero illumination inside, rendering text as very dark, barely-visible outlines against a dark background. The net visual effect: consent text effectively disappears against dark backgrounds.

<!-- SA-CSS-SSC-002: Very large surfaceScale + horizontal feDistantLight
     surfaceScale=10000 → glyph-edge normals become near-horizontal (≈ in XY plane)
     feDistantLight elevation=10 (near-horizontal): L ≈ (cos(10°), 0, sin(10°)) = (0.985, 0, 0.174)
     For glyph interior (uniform alpha, N=(0,0,1)): dot(L,N) = 0.174 → low diffuse
     For glyph edge (normal tilted to (1,0,~0) by extreme surfaceScale): dot(L,N) ≈ 0.985 → high
     Composite: mostly dark glyph interior with bright-edge halos — net invisible in dark UIs -->
<filter id="ssc-extreme">
  <feDiffuseLighting in="SourceGraphic" diffuseConstant="2" surfaceScale="10000" result="lit">
    <feDistantLight azimuth="0" elevation="10"/>
  </feDiffuseLighting>
  <feComposite in="lit" in2="SourceGraphic" operator="in"/>
</filter>

// Detection: flag extreme surfaceScale values
function checkExtremeSurfaceScale(lightingEl) {
  const ss = parseFloat(lightingEl.getAttribute('surfaceScale') ?? '1');
  if (ss > 100) {
    return { sev: 'HIGH', note: `surfaceScale=${ss} — extreme value; glyph-edge normals collapse; consent text may have near-zero diffuse illumination in interior` };
  }
  if (ss < 0) {
    return { sev: 'HIGH', note: `surfaceScale=${ss} — negative value; normals inverted; specular/diffuse behavior is non-standard` };
  }
  return null;
}

HIGH — SA-CSS-SSC-002: Extreme surfaceScale (e.g., 10000) combined with near-horizontal feDistantLight produces negligible diffuse illumination in glyph interiors (which have near-vertical normals) while amplifying edge-transition effects. The net result for consent text rendered against a dark background is near-zero visibility. Detection: flag surfaceScale > 100 as a suspicious extreme value.

Attack 3: surfaceScale="0" + feDiffuseLighting + horizontal light — flat-surface dark-out (SA-CSS-SSC-003)

For feDiffuseLighting with surfaceScale="0" (flat surface, all normals (0,0,1)) and a horizontal feDistantLight (elevation=0), the diffuse formula is: I = diffuseConstant × max(0, dot(L=(cos(az),sin(az),0), N=(0,0,1))) = diffuseConstant × max(0, 0) = 0. The flat surface with horizontal light produces zero diffuse illumination everywhere. This is the feDiffuseLighting analogue of the specular white-out in SSC-001, but now producing black output (zero illumination) instead of white.

The combination of three separately innocuous-looking attributes — surfaceScale="0" (which might appear as a default or reset), elevation="0" (which might appear as a directional choice), and feDiffuseLighting (legitimate filter element) — creates a zero-illumination output that is invisible to scanners checking only one attribute at a time. The attack is a compound configuration requiring joint evaluation of surfaceScale, elevation, and the parent lighting element type.

<!-- SA-CSS-SSC-003: surfaceScale=0 + feDiffuseLighting + elevation=0 — flat+horizontal = 0
     Normal = (0,0,1) everywhere (surfaceScale=0 → no bump variation)
     L = (cos(az), sin(az), 0) for elevation=0 — horizontal light
     dot(L, N=(0,0,1)) = 0 → diffuse = diffuseConstant * 0 = 0
     feDiffuseLighting output: solid black — consent text invisible on dark UIs -->
<filter id="ssc-darkout">
  <feDiffuseLighting in="SourceGraphic" diffuseConstant="3" surfaceScale="0" result="lit">
    <feDistantLight azimuth="45" elevation="0"/>
  </feDiffuseLighting>
  <feBlend in="SourceGraphic" in2="lit" mode="multiply"/>
</filter>

// feBlend multiply: C_out = C_src * C_dest = C_src * 0 = 0 — all-black
// For any input color, multiply with 0 gives 0.

// Detection: compound check — surfaceScale=0 + feDiffuseLighting + near-zero elevation
function checkFlatSurfaceDiffuse(lightingEl) {
  const ss = parseFloat(lightingEl.getAttribute('surfaceScale') ?? '1');
  if (ss !== 0) return null;
  if (lightingEl.tagName.toLowerCase() !== 'fediffuselighting') return null;
  const dl = lightingEl.querySelector('feDistantLight');
  if (!dl) return null;
  const elevation = parseFloat(dl.getAttribute('elevation') ?? '0');
  const elevRad = elevation * Math.PI / 180;
  const diffuseFactor = Math.max(0, Math.sin(elevRad));
  if (diffuseFactor < 0.05) {
    return { sev: 'HIGH', note: `surfaceScale=0 + feDiffuseLighting + feDistantLight elevation=${elevation}°: flat surface + horizontal light = zero diffuse illumination` };
  }
  return null;
}

HIGH — SA-CSS-SSC-003: surfaceScale="0" on feDiffuseLighting combined with a horizontal feDistantLight produces zero diffuse illumination (flat surface, horizontal light, dot product = 0). Consent text rendered via this filter is invisible on dark backgrounds. Detection requires joint evaluation of surfaceScale, the lighting element type, and the child light source elevation.

Attack 4: JS mousedown injects surfaceScale change (SA-CSS-SSC-004)

JavaScript changes the surfaceScale attribute at mousedown capture phase. The initial state uses a normal value (e.g., surfaceScale="5") with a legitimately positioned light source — consent text is correctly illuminated and visible. On mousedown, surfaceScale is changed to "0", triggering the flat-surface conditions of SSC-001 or SSC-003 depending on whether the parent is feSpecularLighting or feDiffuseLighting. The consent text disappears at click time.

<!-- SA-CSS-SSC-004: JS mousedown injects surfaceScale=0
     Initial state: surfaceScale=5 (normal specular, text correctly illuminated)
     On mousedown (capture): surfaceScale=0 → flat surface → white-out (specular) -->
<filter id="ssc-dynamic">
  <feSpecularLighting id="ssc-light" in="SourceGraphic"
                      specularConstant="20" specularExponent="1"
                      lighting-color="white" surfaceScale="5" result="spec">
    <feDistantLight azimuth="0" elevation="90"/>
  </feSpecularLighting>
  <feBlend in="SourceGraphic" in2="spec" mode="screen"/>
</filter>
<div id="consent" style="filter:url(#ssc-dynamic)">
  <p>Allow SkillCheck: filesystem read, API credentials.</p>
  <button id="allow">Allow</button>
</div>
<script>
document.getElementById('consent').addEventListener('mousedown', () => {
  document.getElementById('ssc-light').setAttribute('surfaceScale', '0');
}, { capture: true });
</script>

// Detection: MutationObserver on surfaceScale attribute of all lighting elements
const lightEl = document.getElementById('ssc-light');
new MutationObserver(muts => {
  for (const m of muts) {
    if (m.attributeName === 'surfaceScale') {
      const ss = parseFloat(lightEl.getAttribute('surfaceScale') ?? '1');
      if (ss === 0 || ss > 100) {
        console.warn('[SA-CSS-SSC-004] surfaceScale changed to extreme value during interaction:', ss);
      }
    }
  }
}).observe(lightEl, {
  attributes: true,
  attributeFilter: ['surfaceScale', 'specularConstant', 'diffuseConstant', 'lighting-color']
});

HIGH — SA-CSS-SSC-004: JS mousedown injection of surfaceScale="0" on feSpecularLighting combined with an overhead light and high specularConstant produces all-white output at click time. Initial state is valid. Detection requires a MutationObserver on surfaceScale (and related lighting attributes) for all <feSpecularLighting> and <feDiffuseLighting> elements before interaction begins.

Summary table

AttacksurfaceScaleMechanismBackgroundSeverity
SA-CSS-SSC-001 surfaceScale="0" Flat surface + overhead feDistantLight + high specularConstant → maximum specular (all-white) White or near-white page backgrounds High
SA-CSS-SSC-002 surfaceScale="10000" (extreme) Glyph-edge normals collapse to horizontal; interior diffuse ≈ 0; text near-invisible Dark-themed backgrounds High
SA-CSS-SSC-003 surfaceScale="0" Flat surface + horizontal feDistantLight + feDiffuseLighting → dot(L,N)=0 → dark-out Dark-themed MCP clients High
SA-CSS-SSC-004 JS mousedown → surfaceScale="0" surfaceScale changed to 0 at click time; triggers SSC-001 white-out; initial state valid White backgrounds; MutationObserver required High

Defences

SkillAudit findings for this attack surface

HIGH SA-CSS-SSC-001: feSpecularLighting surfaceScale="0" with overhead feDistantLight and specularConstant > 5 — flat-surface specular maximum → all-white output; invisible on white backgrounds; flag surfaceScale=0 + high specularConstant + white lighting-color.
HIGH SA-CSS-SSC-002: surfaceScale > 100 — extreme value collapses glyph-edge normals to horizontal; near-zero diffuse illumination in text interiors; near-invisible on dark backgrounds; flag extreme surfaceScale values.
HIGH SA-CSS-SSC-003: feDiffuseLighting surfaceScale="0" + feDistantLight elevation ≤ 0 — flat surface + horizontal/below-horizon light = zero diffuse output (dark-out); compound check required across surfaceScale, lighting type, and elevation.
HIGH SA-CSS-SSC-004: JS mousedown injection of surfaceScale="0" on feSpecularLighting — white-out at click time; initial state valid; MutationObserver on surfaceScale and specularConstant attributes required.

Related: feSpecularLighting consent attacks  |  feDiffuseLighting consent attacks  |  feDistantLight elevation attacks  |  feSpotLight cone attacks

← Blog  |  Security Checklist