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

IDTechniqueoffsetWidthBCR.widthmatrix c (skewX tan)Severity
SA-CSS-SKEW-001skewX(89deg) — X-axis near-90deg shear collapses apparent width300px~hairline~57High
SA-CSS-SKEW-002skewY(89deg) — Y-axis near-90deg shear collapses apparent height300pxnormal (spread)b≈57High
SA-CSS-SKEW-003skew(89deg,89deg) — combined both-axis extreme shear300pxdegenerateb≈57, c≈57High
SA-CSS-SKEW-004JS mousedown sets skewX(89deg) + CSS transition "card flip"300px~hairline (after)~57 (after)High

Consolidated findings

High SA-CSS-SKEW-001 — skewX(89deg): matrix c≈57; apparent width collapses to hairline; offsetWidth=300px unchanged; text rendered at extreme oblique — completely illegible
High SA-CSS-SKEW-002 — skewY(89deg): matrix b≈57; apparent height collapses; horizontal hairline strip; text at near-horizontal inclination — unreadable
High SA-CSS-SKEW-003 — skew(89deg,89deg): both matrix b and c ≈57; element degenerates to near-point; not recognizable as standard animation; functionally invisible
High SA-CSS-SKEW-004 — JS mousedown + CSS transition: skewX(89deg) injected at install click; consent shears to hairline over 300ms; looks like "card flip" animation

See also: CSS rotate3d() attacks | CSS scale individual property | CSS matrix3d near-zero attacks | CSS individual transform properties | SkillAudit — free MCP server audit