MCP server CSS skewX/skewY security: near-90deg X-axis shear collapse, Y-axis shear height collapse, combined skew() two-axis collapse, and JS mousedown skew injection
Published 2026-08-07 — SkillAudit Research
The CSS skewX(angle) and skewY(angle) transform functions apply a 2D shearing transformation to an element along the X or Y axis. Unlike rotate() — which rotates the element as a rigid body, preserving its apparent dimensions — skew applies a progressive shear that, at angles approaching 90 degrees, compresses the element's apparent rendered width (skewX) or height (skewY) to near zero. Unlike scale(0) — which collapses the dimension directly — skew produces a distinctive parallelogram shape at moderate angles that can be disguised as a CSS animation frame (a "playing card flip" effect). At 89 degrees, the apparent compression approaches a hairline, and the element is visually unreadable.
The key distinction: skewX(89deg) changes the apparent rendered width (as measured by getBoundingClientRect()) without changing the CSS layout width (offsetWidth). A scanner that checks only offsetWidth will see the normal 300px layout width and report no issue. The geometric check via getBoundingClientRect() is the correct detection. See also CSS rotate3d() attacks and CSS scale individual property attacks for related dimensional collapse patterns.
Skew vs rotate vs scale — key distinctions: rotate() rotates the element as a rigid shape — apparent width changes but the element remains fully visible at oblique angle. scale(0) collapses the dimension to zero while preserving angles (makes it an invisible point). skewX(89deg) shears the X axis — the element is compressed into a near-vertical hairline strip while remaining at its full CSS layout height. The shear specifically makes the rendered glyphs completely illegible without fully hiding the element's layout box.
Attack 1: skewX(89deg) — X-axis shear collapses apparent width to near-zero (SA-CSS-SKEW-001)
The consent element applies transform: skewX(89deg). The X-axis shear at 89 degrees applies a horizontal shear factor of tan(89°) ≈ 57 — meaning for every 1px of height, the top edge is displaced 57px horizontally relative to the bottom edge. The resulting shape is a nearly-vertical parallelogram. The getBoundingClientRect() width encompasses the horizontal extent of the parallelogram: for a 60px tall element, the shear displacement is 57 × 60 ≈ 3420px — the element's horizontal extent is enormous (mostly displaced out of view), but the element itself as a content region appears as a hairline strip of near-zero readable width. The text in the strip is rendered at extreme oblique angles — completely illegible.
/* MCP attack: */
.consent-disclosure {
width: 300px;
height: 60px;
transform: skewX(89deg); /* near-90deg X shear — element compressed to hairline */
/* offsetWidth: 300px (CSS layout) ← passes
offsetHeight: 60px (CSS layout) ← passes
Rendered appearance: near-vertical hairline strip
Text rendered at extreme oblique — completely illegible
getBoundingClientRect(): large left/right spread (due to shear offset)
but content region width near-zero
getComputedStyle().transform: 'matrix(...)' — tan values near-infinity */
}
// Detection — parse skewX angle from transform matrix:
function detectSkewXCollapse(el) {
const cs = window.getComputedStyle(el);
const t = cs.transform;
if (!t || t === 'none') return;
// Extract matrix — skewX encodes as matrix(1, 0, tan(angle), 1, 0, 0)
const m = t.match(/matrix\(([^)]+)\)/);
if (m) {
const vals = m[1].split(',').map(parseFloat);
const skewXTan = vals[2]; // matrix(a,b,c,d,e,f) — c = tan(skewX)
if (Math.abs(skewXTan) > 5) { // tan(80deg) ≈ 5.67
const angle = Math.atan(Math.abs(skewXTan)) * 180 / Math.PI;
console.error('SA-CSS-SKEW-001: skewX near-90deg collapses apparent width', {
el, skewXTan, effectiveAngle: angle.toFixed(1) + 'deg'
});
}
}
}
// Alternative: geometric detection:
function detectSkewGeometric(el) {
const bcr = el.getBoundingClientRect();
const content = el.textContent.trim();
// If element has text content but appears as a very thin strip
if (content.length > 0 && bcr.height > 20) {
const cs = window.getComputedStyle(el);
const t = cs.transform;
if (t && t !== 'none' && bcr.height > bcr.width * 10) {
// Height much greater than width — shear collapse signature
console.error('SA-CSS-SKEW-001: extreme height/width ratio suggests skew collapse', {
el, bcrWidth: bcr.width, bcrHeight: bcr.height, ratio: bcr.height / bcr.width
});
}
}
}
Attack 2: skewY(89deg) — Y-axis shear collapses apparent height to near-zero (SA-CSS-SKEW-002)
skewY(89deg) applies the shear along the Y axis — each unit of width displaces the top/bottom edges vertically by tan(89°) ≈ 57 units. The result is a near-horizontal parallelogram whose apparent vertical height is a hairline. For a 60px tall consent element, the horizontal width of the element causes a vertical shear offset of 57 × 300px ≈ 17,100px total height spread, but the rendered height at any given horizontal position is near-zero. The text is rendered at near-horizontal inclination — completely illegible. offsetHeight remains 60px; getBoundingClientRect().height is extremely large (due to vertical spread) but the readable content region is a hairline.
/* MCP attack: */
.consent-disclosure {
transform: skewY(89deg); /* near-90deg Y shear — collapses to horizontal hairline */
/* offsetHeight: 60px (CSS layout) ← passes
Rendered: near-horizontal parallelogram strip
Text at near-horizontal inclination — unreadable
A 'card sweep from above' animation appearance */
}
/* Moderate-angle variant — still unreadable: */
.consent-disclosure {
transform: skewY(80deg); /* tan(80°) ≈ 5.67 — extreme shear, text illegible */
}
// Detection — parse skewY from matrix:
function detectSkewYCollapse(el) {
const cs = window.getComputedStyle(el);
const t = cs.transform;
if (!t || t === 'none') return;
const m = t.match(/matrix\(([^)]+)\)/);
if (m) {
const vals = m[1].split(',').map(parseFloat);
const skewYTan = vals[1]; // matrix(a,b,c,d,e,f) — b = tan(skewY)
if (Math.abs(skewYTan) > 5) {
const angle = Math.atan(Math.abs(skewYTan)) * 180 / Math.PI;
console.error('SA-CSS-SKEW-002: skewY near-90deg collapses apparent height', {
el, skewYTan, effectiveAngle: angle.toFixed(1) + 'deg'
});
}
}
}
Attack 3: skew(89deg, 89deg) — two-axis combined shear (SA-CSS-SKEW-003)
The skew(ax, ay) shorthand applies shear on both axes simultaneously. skew(89deg, 89deg) combines the X and Y shear at near-90 degrees. The resulting matrix is matrix(1, tan(89), tan(89), 1, 0, 0) — the off-diagonal matrix elements are each approximately 57. The element is sheared into a shape that is both horizontally and vertically compressed, rendering as a near-point in the viewport. The resulting shape does not cleanly appear as a "playing card" animation — but disguised with a low alpha or very small intended size, it is functionally invisible. The getBoundingClientRect() will show a degenerate rectangle.
/* MCP attack: */
.consent-disclosure {
transform: skew(89deg, 89deg); /* both axes near-90deg */
/* Resulting matrix: matrix(1, 57, 57, 1, 0, 0)
Element is a severely distorted parallelogram in both dimensions
Content region effectively collapses to a near-point
Not obviously recognizable as any standard animation pattern */
}
// Detection — check both skew components:
function detectCombinedSkew(el) {
const cs = window.getComputedStyle(el);
const t = cs.transform;
if (!t || t === 'none') return;
const m = t.match(/matrix\(([^)]+)\)/);
if (m) {
const vals = m[1].split(',').map(parseFloat);
const skewX = Math.abs(vals[2]); // tan(skewX)
const skewY = Math.abs(vals[1]); // tan(skewY)
if (skewX > 2 || skewY > 2) { // tan(63deg) ≈ 2
console.error('SA-CSS-SKEW-003: combined skew() severely distorts consent', {
el, skewXTan: skewX, skewYTan: skewY, transform: t
});
}
}
}
Attack 4: JS mousedown sets skewX(89deg) + CSS transition — "card flip" animation cover (SA-CSS-SKEW-004)
At page load, the consent element renders normally. The element has a CSS transition set on its transform: transition: transform 0.3s ease-in. At mousedown on the install button, JS sets el.style.transform = 'skewX(89deg)'. The consent element shears into a near-invisible strip over 300ms — visually identical to a "playing card flip" UI animation common in MCP installation dialogs. During this animation, the user's mouse button is being pressed on the install button. By the time the animation completes (consent now invisible), the browser registers the mouseup event and the install is confirmed. MutationObserver on the consent element style attribute + geometric check detects this attack.
/* Baseline CSS: */
.consent-disclosure {
width: 300px;
height: 60px;
transition: transform 0.3s ease-in; /* smooth "card flip" animation baseline */
}
// MCP JS — triggers at install click:
document.querySelector('#install-btn').addEventListener('mousedown', () => {
const consent = document.querySelector('.consent-disclosure');
if (consent) {
consent.style.transform = 'skewX(89deg)';
/* Consent shears to near-invisible strip over 300ms
Looks like a "step confirmed" animation
User's mouse is pressed during the entire 300ms animation */
}
}, { capture: true });
// Detection:
function detectDynamicSkew(consentEl) {
new MutationObserver(() => {
const cs = window.getComputedStyle(consentEl);
const t = cs.transform;
if (t && t !== 'none') {
const m = t.match(/matrix\(([^)]+)\)/);
if (m) {
const vals = m[1].split(',').map(parseFloat);
if (Math.abs(vals[1]) > 2 || Math.abs(vals[2]) > 2) {
console.error('SA-CSS-SKEW-004: JS injected extreme skew transform at install click', {
consentEl, skewYTan: vals[1], skewXTan: vals[2], transform: t
});
}
}
}
}).observe(consentEl, { attributes: true, attributeFilter: ['style'] });
document.querySelector('#install-btn, [data-action="install"]')
?.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
}
Root detection method: Parse the computed transform matrix for high skew tangent values. getComputedStyle(el).transform returns the matrix form matrix(a, b, c, d, e, f). A skewX angle is encoded as c = tan(skewX); a skewY angle is encoded as b = tan(skewY). Any value of |b| or |c| above 5.67 (corresponding to 80 degrees) indicates a near-extreme shear. Additionally check getBoundingClientRect().width / offsetWidth < 0.05 or getBoundingClientRect().height / offsetHeight < 0.05 as a geometry-level fallback. SkillAudit checks both matrix parsing and geometry on every consent element.
Attack summary
| ID | Technique | offsetWidth | BCR.width | matrix c (skewX tan) | Severity |
|---|---|---|---|---|---|
| SA-CSS-SKEW-001 | skewX(89deg) — X-axis near-90deg shear collapses apparent width | 300px | ~hairline | ~57 | High |
| SA-CSS-SKEW-002 | skewY(89deg) — Y-axis near-90deg shear collapses apparent height | 300px | normal (spread) | b≈57 | High |
| SA-CSS-SKEW-003 | skew(89deg,89deg) — combined both-axis extreme shear | 300px | degenerate | b≈57, c≈57 | High |
| SA-CSS-SKEW-004 | JS mousedown sets skewX(89deg) + CSS transition "card flip" | 300px | ~hairline (after) | ~57 (after) | High |
Consolidated findings
See also: CSS rotate3d() attacks | CSS scale individual property | CSS matrix3d near-zero attacks | CSS individual transform properties | SkillAudit — free MCP server audit