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
| ID | Technique | xScale (col norm) | BCR.width | Severity |
|---|---|---|---|---|
| SA-CSS-M3D-001 | matrix3d equivalent to scale(0,0,1) — obvious zero elements | 0 | ~0px | High |
| SA-CSS-M3D-002 | Rotation × scale-zero composed matrix — non-obvious element values | 0.001 | ~0px | High |
| SA-CSS-M3D-003 | matrix3d with large translation off-viewport — identity scale, displaced | 1 (normal) | off-screen | High |
| SA-CSS-M3D-004 | JS mousedown sets matrix3d(scale-zero) at install click | 0 (after) | ~0px (after) | High |
Consolidated findings
See also: CSS scale individual property attacks | CSS skewX/skewY attacks | CSS rotate3d() attacks | CSS individual transform properties | SkillAudit — free MCP server audit