MCP server CSS matrix3d() security: near-zero X/Y scale collapse, rotation-obfuscated scale-zero, combined scale+translate out of viewport, and JS mousedown matrix3d injection

Published 2026-08-07 — SkillAudit Research

The CSS transform: matrix3d() function specifies a complete 4×4 3D transformation matrix in column-major order, taking 16 numeric values. It is the most general form of CSS transform — any combination of translation, rotation, scale, skew, and perspective can be expressed as a single matrix3d() call. The security relevance: a matrix3d() that is mathematically equivalent to scale(0, 0, 1) (which collapses the element to an invisible point) is not identifiable by reading the 16 matrix values without performing matrix decomposition. The obfuscation is inherent — 16 numbers without a rotation-cancel walkthrough do not obviously indicate a zero-scale collapse.

This attack is distinct from CSS scale individual property attacks (where getComputedStyle().scale directly reveals the value), from rotate3d() attacks (where the rotation angle is the signal), and from skewX/Y attacks (where the shear tangent is the signal). When the browser serializes a matrix3d() transform, getComputedStyle(el).transform returns the same matrix3d(...) string — no decomposition into named functions is performed. The canonical detection is the geometric fallback: getBoundingClientRect().

Detection gap: Scanners that look for transform function names ('scale', 'rotate', 'translate') in the computed style string will see only 'matrix3d' — no named component functions. A matrix3d equivalent to scale(0, 0, 1) cannot be identified from the function name alone. The correct detection path: (1) geometric check via getBoundingClientRect(), (2) matrix decomposition to extract scale components, (3) check the 4×4 matrix's upper-left 3×3 submatrix for near-zero row norms.

Attack 1: matrix3d equivalent to scale(0,0,1) — 16-element matrix obfuscates zero-scale collapse (SA-CSS-M3D-001)

The purest form of the attack: a matrix3d() that is exactly equivalent to scale(0, 0, 1). The identity 4×4 matrix has 1s on the diagonal and 0s elsewhere. Setting the first and fifth elements (the X and Y scale terms in the column-major layout) to near-zero produces a matrix that collapses the rendered element. The full 16-element representation matrix3d(0,0,0,0, 0,0,0,0, 0,0,1,0, 0,0,0,1) is a scale(0,0,1) expressed as a 4×4 matrix — which to a human reader of the computed style string appears as an arbitrary matrix, not obviously equivalent to scale(0).

/* MCP attack — matrix3d equivalent to scale(0,0,1): */
.consent-disclosure {
  transform: matrix3d(
    0, 0, 0, 0,    /* column 1: X basis vector — near-zero X scale */
    0, 0, 0, 0,    /* column 2: Y basis vector — near-zero Y scale */
    0, 0, 1, 0,    /* column 3: Z basis vector — Z preserved */
    0, 0, 0, 1     /* column 4: homogeneous — translation zero */
  );
  /* getComputedStyle().transform: 'matrix3d(0,0,0,0, 0,0,0,0, 0,0,1,0, 0,0,0,1)'
     No 'scale' keyword in the string — function-name scanners miss this
     getBoundingClientRect().width:  ~0px ← DETECTS
     getBoundingClientRect().height: ~0px ← DETECTS
     offsetWidth:  300px (layout unchanged) ← passes
     offsetHeight: 60px (layout unchanged) ← passes */
}

// Detection via matrix decomposition:
function extractMatrix3dScale(transformStr) {
  const m = transformStr.match(/matrix3d\(([^)]+)\)/);
  if (!m) return null;
  const vals = m[1].split(',').map(parseFloat);
  // Column-major 4x4: indices [0-3]=col1, [4-7]=col2, [8-11]=col3, [12-15]=col4
  // X scale: magnitude of first column vector (vals[0..2])
  // Y scale: magnitude of second column vector (vals[4..6])
  const xScale = Math.sqrt(vals[0]**2 + vals[1]**2 + vals[2]**2);
  const yScale = Math.sqrt(vals[4]**2 + vals[5]**2 + vals[6]**2);
  return { xScale, yScale };
}

function detectMatrix3dZeroScale(el) {
  const t = window.getComputedStyle(el).transform;
  if (!t || !t.startsWith('matrix3d')) return;
  const scales = extractMatrix3dScale(t);
  if (scales && (scales.xScale < 0.05 || scales.yScale < 0.05)) {
    console.error('SA-CSS-M3D-001: matrix3d has near-zero scale component', {
      el, xScale: scales.xScale, yScale: scales.yScale, transform: t
    });
  }
}

Attack 2: matrix3d with rotation pre-multiplied to obfuscate scale-zero — column norms appear non-zero (SA-CSS-M3D-002)

A more sophisticated obfuscation: the matrix3d() is composed as a scale-zero matrix pre-multiplied by a rotation matrix. The mathematical result is still a zero-scale collapse, but the individual matrix elements are no longer simply 0 and 1 — they are combinations of rotation angles and zero-scale values that produce non-obvious element values. For example, a 45-degree Z rotation combined with X/Y scale of 0.001 produces a matrix where most elements are small non-zero values (0.0007, 0.0007, etc.) rather than exactly zero. Reading the matrix, no single element is obviously zero — the zero-scale effect emerges only from computing the column vector norms.

/* MCP attack — rotation × scale-zero (obfuscated): */
/*
  Composition: rotateZ(45deg) × scale(0.001, 0.001, 1)
  = matrix3d(
      0.000707, 0.000707, 0, 0,    // col1: Rx × Sx
     -0.000707, 0.000707, 0, 0,    // col2: Ry × Sy
      0,        0,        1, 0,    // col3: Z unchanged
      0,        0,        0, 1     // col4: translation
    )
  Individual values look like "floating point noise" — not obviously suspicious
  Column norms: col1 = sqrt(0.000707² + 0.000707²) ≈ 0.001 — near-zero X scale
*/
.consent-disclosure {
  transform: matrix3d(
    0.000707, 0.000707, 0, 0,
   -0.000707, 0.000707, 0, 0,
    0,        0,        1, 0,
    0,        0,        0, 1
  );
  /* No obvious zeros — human reader sees "floating point values"
     Column norm decomposition reveals xScale = yScale = 0.001
     getBoundingClientRect() remains canonical detection */
}

// Detection — column norm decomposition catches this case:
function detectObfuscatedMatrix3d(el) {
  const t = window.getComputedStyle(el).transform;
  if (!t || !t.startsWith('matrix3d')) return;
  const scales = extractMatrix3dScale(t);
  if (scales && (scales.xScale < 0.05 || scales.yScale < 0.05)) {
    console.error('SA-CSS-M3D-002: obfuscated matrix3d (rotation×scale) has near-zero column norms', {
      el, xScale: scales.xScale.toFixed(4), yScale: scales.yScale.toFixed(4)
    });
  }
}

Attack 3: matrix3d with large translation off-viewport + identity scale (SA-CSS-M3D-003)

A matrix3d() that is equivalent to a large translation (e.g., translateX(100vw)) moves the element completely off-screen. Unlike the scale attacks, this uses the translation components (elements 12 and 13 in column-major order, corresponding to the X and Y translation). The element is rendered at its normal size but positioned outside the viewport. offsetLeft and offsetTop remain at their CSS layout values (within the viewport); getBoundingClientRect().left reveals the actual rendered position. The 16-element matrix encoding of a translation obscures the translateX() equivalent for function-name scanners.

/* MCP attack — matrix3d equivalent to translateX(1500px): */
.consent-disclosure {
  transform: matrix3d(
    1, 0, 0, 0,     /* col1: identity X */
    0, 1, 0, 0,     /* col2: identity Y */
    0, 0, 1, 0,     /* col3: identity Z */
    1500, 0, 0, 1   /* col4: X translation = 1500px (off-screen right) */
  );
  /* getBoundingClientRect().left ≈ 1500px + CSS layout left — off viewport right
     offsetLeft: normal CSS value (unchanged by transform)
     getComputedStyle().transform: 'matrix3d(...)' — no 'translate' keyword */
}

// Detection:
function detectMatrix3dOffscreenTranslation(el) {
  const t = window.getComputedStyle(el).transform;
  if (!t || !t.startsWith('matrix3d')) return;
  const m = t.match(/matrix3d\(([^)]+)\)/);
  if (!m) return;
  const vals = m[1].split(',').map(parseFloat);
  // Translation components: vals[12] = X, vals[13] = Y (column-major 4x4)
  const tx = vals[12] || 0;
  const ty = vals[13] || 0;
  const vw = window.innerWidth;
  const vh = window.innerHeight;
  if (Math.abs(tx) > vw * 0.5 || Math.abs(ty) > vh * 0.5) {
    console.error('SA-CSS-M3D-003: matrix3d large translation off-viewport', {
      el, translateX: tx, translateY: ty, viewportW: vw, viewportH: vh
    });
  }
  // Also: BCR geometric check as fallback
  const bcr = el.getBoundingClientRect();
  if (bcr.right < 0 || bcr.left > vw || bcr.bottom < 0 || bcr.top > vh) {
    console.error('SA-CSS-M3D-003: matrix3d renders consent outside viewport (BCR)', {
      el, bcr
    });
  }
}

Attack 4: JS mousedown sets matrix3d scale-zero at install click (SA-CSS-M3D-004)

At page load, the consent element has no transform. Load-time audit passes. At mousedown on the install button, JS sets a matrix3d() equivalent to scale(0, 0, 1) on the consent element. The element collapses to an invisible point instantaneously. Because the transform is expressed as a matrix3d() rather than as scale(0), static CSS analyzers that look for the 'scale(0)' pattern in the stylesheet will not find it. MutationObserver on the consent element's style attribute detects the matrix3d injection; the geometric check confirms the collapse.

/* Baseline CSS — no transform at load time: */
.consent-disclosure {
  /* No transform — renders normally at load time */
}

// MCP JS — triggers at install click:
document.querySelector('#install-btn').addEventListener('mousedown', () => {
  const consent = document.querySelector('.consent-disclosure');
  if (consent) {
    // Set matrix3d equivalent to scale(0,0,1) — obfuscated zero scale
    consent.style.transform = 'matrix3d(0,0,0,0, 0,0,0,0, 0,0,1,0, 0,0,0,1)';
    /* Element collapses instantaneously — no transition
       getComputedStyle().transform: 'matrix3d(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1)'
       Static scanner looking for 'scale(0)' does not match
       getBoundingClientRect().width === 0, .height === 0 */
  }
}, { capture: true });

// Detection:
function detectDynamicMatrix3d(consentEl) {
  new MutationObserver(() => {
    requestAnimationFrame(() => {
      const t = window.getComputedStyle(consentEl).transform;
      if (t && t.startsWith('matrix3d')) {
        const scales = extractMatrix3dScale(t);
        if (scales && (scales.xScale < 0.05 || scales.yScale < 0.05)) {
          console.error('SA-CSS-M3D-004: JS injected matrix3d scale-zero at install click', {
            consentEl, xScale: scales.xScale, yScale: scales.yScale
          });
        }
        // Geometric fallback
        const bcr = consentEl.getBoundingClientRect();
        if (bcr.width < 2 && bcr.height < 2 && consentEl.offsetWidth > 10) {
          console.error('SA-CSS-M3D-004: matrix3d collapses consent to near-zero BCR (geometric)', {
            consentEl, bcrWidth: bcr.width, bcrHeight: bcr.height
          });
        }
      }
    });
  }).observe(consentEl, { attributes: true, attributeFilter: ['style'] });

  document.querySelector('#install-btn, [data-action="install"]')
    ?.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
}

Root detection method for matrix3d attacks: Two complementary checks are required. (1) Geometric: getBoundingClientRect().width < 2 && offsetWidth > 10 or similarly for height — this catches all collapse attacks regardless of how the matrix is expressed. (2) Matrix decomposition: For matrix3d() transforms, extract column vector norms: xScale = sqrt(m[0]²+m[1]²+m[2]²), yScale = sqrt(m[4]²+m[5]²+m[6]²); flag if either is below 0.05. Also check translation components m[12]/m[13] for large off-viewport values. SkillAudit runs both checks on every consent element in the rendered DOM.

Attack summary

IDTechniquexScale (col norm)BCR.widthSeverity
SA-CSS-M3D-001matrix3d equivalent to scale(0,0,1) — obvious zero elements0~0pxHigh
SA-CSS-M3D-002Rotation × scale-zero composed matrix — non-obvious element values0.001~0pxHigh
SA-CSS-M3D-003matrix3d with large translation off-viewport — identity scale, displaced1 (normal)off-screenHigh
SA-CSS-M3D-004JS mousedown sets matrix3d(scale-zero) at install click0 (after)~0px (after)High

Consolidated findings

High SA-CSS-M3D-001 — matrix3d equivalent to scale(0,0,1): 16-element matrix obfuscates zero-scale; function-name scanners see 'matrix3d' not 'scale'; column norm = 0; BCR collapses to 0px
High SA-CSS-M3D-002 — rotation × scale-zero matrix: individual elements appear as floating-point values; column norms reveal xScale=yScale=0.001; BCR confirms collapse; requires decomposition
High SA-CSS-M3D-003 — matrix3d with large tx/ty translation: consent displaced off-viewport; offsetLeft unchanged; BCR.left > innerWidth; translation components m[12]/m[13] reveal attack
High SA-CSS-M3D-004 — JS mousedown injects matrix3d(scale-zero); instantaneous collapse; no transition; MutationObserver + rAF + column-norm decomposition detects

See also: CSS scale individual property attacks | CSS skewX/skewY attacks | CSS rotate3d() attacks | CSS individual transform properties | SkillAudit — free MCP server audit