← Blog

MCP server CSS individual transform properties consent security

Four SA-CSS-ITF consent attack patterns using CSS Transforms Level 2’s individual transform properties — scale, rotate, and translate. These properties are applied independently of the transform shorthand and evade detectors that only inspect getComputedStyle(el).transform. scale: 0.001 collapses the consent panel to sub-pixel size while preserving the click target at its original position.

By SkillAudit  |  29 Oct 2026

CSS Transforms Level 2 introduced three individual CSS properties — translate, rotate, and scale — that apply their respective transform operations independently of the transform shorthand. Crucially, getComputedStyle(el).transform resolves the combined matrix including all three individual properties, but el.style.transform and a static CSS property check for the transform rule will miss them if only the individual properties are set. A detector that checks el.style.transform.includes('scale') or cs.transform.startsWith('matrix') may still catch them via the matrix representation, but a pattern-match on the raw CSS source declaration misses scale: 0.001 because the property name is scale, not transform.

Composed matrix: getComputedStyle(el).transform resolves the fully-composed matrix from translate + rotate + scale + the transform shorthand. Reading the matrix values is the most reliable detection method: matrix(a, b, c, d, e, f) where the scale factor is Math.sqrt(a² + b²). However, many detectors read the CSS source declaration rather than the computed matrix, and therefore miss individual transform properties.

SA-CSS-ITF-001: scale=0.001 — sub-pixel collapse with preserved click area

Setting scale: 0.001 on a consent panel collapses its visual rendering to 0.1% of its original dimensions: a 300×120px dialog becomes a 0.3×0.12px speck, invisible at any practical screen resolution. However, CSS transforms do not affect the element’s layout box. getBoundingClientRect() returns the post-transform dimensions (0.3×0.12px), but the click target area in many browser implementations corresponds to the pre-transform layout box, meaning pointer events still reach the element at its original screen coordinates. The “I Agree” button, also scaled to a speck, may still fire click events at the original position.

/* SA-CSS-ITF-001: consent panel collapsed to sub-pixel via scale property */
.consent-dialog {
  scale: 0.001;           /* CSS Transforms L2 individual property */
  transform-origin: top left;
}

/* Visual: 300×120px dialog → 0.3×0.12px speck. Invisible.
   Layout: el.offsetWidth = 300, el.offsetHeight = 120 (layout unchanged)
   getBoundingClientRect():
     width: 0.3, height: 0.12 (post-transform visual bounds)

   Click area (pointer-events):
   Some browsers honor the post-transform bounding box for hit testing.
   Others use the pre-transform layout box.
   Result: in pre-transform hit-testing browsers, the entire 300×120px original
   area is clickable even though content is invisible.

   getComputedStyle(el).scale  === "0.001"       ← individual property
   getComputedStyle(el).transform === "matrix(0.001, 0, 0, 0.001, 0, 0)"
   el.style.transform === ""  (not set via transform shorthand)

   Detection:
   const matrix = getComputedStyle(el).transform;
   const m = matrix.match(/^matrix\(([^,]+),([^,]+),([^,]+),([^,]+)/);
   if (m) {
     const sx = Math.sqrt(m[1]**2 + m[2]**2);  /* scale X */
     const sy = Math.sqrt(m[3]**2 + m[4]**2);  /* scale Y */
     if (Math.min(sx, sy) < 0.1)
       return { risk: 'CRITICAL', id: 'SA-CSS-ITF-001', sx, sy };
   }
   /* Also check individual property directly: */
   const scaleProp = getComputedStyle(el).scale;  /* "0.001" */
   if (scaleProp && scaleProp !== 'none' && parseFloat(scaleProp) < 0.1)
     return { risk: 'CRITICAL', id: 'SA-CSS-ITF-001', scale: scaleProp }; */
CRITICAL
SA-CSS-ITF-001 — scale:0.001 collapses consent panel to 0.3×0.12px speck. Invisible but click-active at original position in some browsers. Evades transform: scale() source declaration checks. Detection: getComputedStyle().scale < 0.1 OR matrix() decomposition scale < 0.1.

SA-CSS-ITF-002: rotate=90deg — sideways text, parent overflow:hidden clips

Setting rotate: 90deg on a consent text block rotates it 90 degrees. The text remains accessible (if the dialog container is large enough), but the user must read it sideways. Combined with overflow: hidden on the parent, the rotated text is clipped and may disappear entirely if the rotation moves it outside the visible area of the parent.

/* SA-CSS-ITF-002: consent text rotated 90 degrees */
.consent-text {
  rotate: 90deg;          /* CSS Transforms L2 individual property */
  /* transform-origin default: center */
}

.consent-dialog {
  overflow: hidden;       /* rotated text may exit bounds and be clipped */
  width: 300px;
  height: 50px;           /* short dialog: most rotated text exits bounds */
}

/* With rotate:90deg on text inside a short (50px height) dialog:
   A 300px-wide text block rotated 90° becomes 300px tall × text-height wide.
   The dialog is only 50px tall with overflow:hidden.
   Most of the rotated text block is clipped; only a sliver remains visible.

   Evades: transform:rotate() source declaration checks.
   Detection: getComputedStyle(el).rotate !== 'none' && rotate !== '0deg'
   → HIGH SA-CSS-ITF-002 (requires content accessibility analysis) */
HIGH
SA-CSS-ITF-002 — rotate:90deg + parent overflow:hidden. Rotated consent text clips against short parent bounds. Readable only sideways (if not clipped). Evades transform:rotate() source checks. Detection: getComputedStyle().rotate !== 'none' and not '0deg' on consent element.

SA-CSS-ITF-003: translate=400px 0 — off-screen shift with original click target

Setting translate: 400px 0 shifts the rendered consent text 400px to the right, beyond the viewport edge for a standard 360–480px wide mobile dialog. The element’s layout position is unchanged: getBoundingClientRect().x returns the post-transform offset (400px+ rightward) and pointer events follow the transformed position. The text is visually absent but the “I Agree” button, rendered at its own untransformed position, still fires correctly.

/* SA-CSS-ITF-003: consent text shifted off-screen right */
.consent-text {
  translate: 400px 0;     /* CSS Transforms L2: shift 400px right */
}

/* Dialog width: 320px (mobile). Screen width: 375px.
   After translate: consent text at x_screen + 400px → off right edge.
   Clipped by viewport (CSS overflow:hidden on body/html) or simply off-screen.
   The text element still exists in DOM; click events on the text's translated
   position arrive off-screen (no user interaction possible there).

   Allow button: separate element, no translate applied → still visible, clickable.

   Comparison with feOffset (SA-CSS-FOS): feOffset is an SVG filter primitive that
   shifts the rendered filter output; translate is a CSS transform applied before
   compositing. Different rendering stage, different detection path.

   Detection:
   const tx = getComputedStyle(el).translate;   /* "400px 0" */
   /* Or via matrix: matrix(1, 0, 0, 1, 400, 0) — tx=400 */
   const m = getComputedStyle(el).transform.match(/matrix\(1, 0, 0, 1, ([^,]+), ([^)]+)\)/);
   if (m && (Math.abs(parseFloat(m[1])) > 200 || Math.abs(parseFloat(m[2])) > 200))
     return { risk: 'CRITICAL', id: 'SA-CSS-ITF-003', tx: m[1], ty: m[2] }; */
CRITICAL
SA-CSS-ITF-003 — translate:400px 0 shifts consent text off right edge of viewport. Allow button at original position remains hittable. Evades feOffset (different rendering stage) and transform:translate() source checks. Detection: getComputedStyle().translate or matrix() translation component > 200px.

SA-CSS-ITF-004: JS mousedown injects scale=0 — collapse at click time

The consent text is initially displayed at scale: 1 (normal). At mousedown on the confirm button, JavaScript sets scale: 0 on the consent text, collapsing it to a point. The text is invisible by the time the user lifts the mouse button to complete the click. No animation or transition is needed; the collapse is instantaneous at scale: 0.

/* Initial state: consent text fully visible */
.consent-text { /* no individual transform properties — scale:1 by default */ }

document.getElementById('confirm-btn').addEventListener('mousedown', () => {
  document.querySelector('.consent-text').style.scale = '0';
  /* Alternatively: el.style.setProperty('scale', '0') */
});

/* At mousedown: consent text collapses to a single point.
   transform-origin (default: 50% 50%): collapses to center of element.
   The consent text area is instantly invisible.
   The confirm button is a separate element; its own click event fires at mouseup.

   el.style.scale === "0"  (set via inline style)
   getComputedStyle(el).scale === "0"
   getComputedStyle(el).transform === "matrix(0, 0, 0, 0, 0, 0)"

   MutationObserver on style attribute detects scale=0 injection at mousedown.

   Note: scale=0 differs from scale=0.001 (SA-CSS-ITF-001):
   scale=0 collapses to a literal point (degenerate matrix; cannot display anything).
   scale=0.001 collapses to sub-pixel but is theoretically non-zero.
   Both produce invisible rendering; scale=0 is more absolute.

   Detection:
   MutationObserver on consent element attributes for style changes;
   read getComputedStyle(el).scale or matrix decomposition scale at mutation time. */
CRITICAL
SA-CSS-ITF-004 — JS mousedown injects scale:0. Consent collapses to point instantly. Allow button unaffected. TOCTOU: legible at rest, invisible at confirmation. MutationObserver on consent element style attribute for scale injection.

Summary of SA-CSS-ITF findings

IDSeverityPropertyMechanismEffect
SA-CSS-ITF-001 CRITICAL scale: 0.001 Sub-pixel collapse; layout box preserved; click area may persist Visual: invisible; click: active
SA-CSS-ITF-002 HIGH rotate: 90deg Sideways rotation; clipped by overflow:hidden parent Visual: clipped or sideways
SA-CSS-ITF-003 CRITICAL translate: 400px 0 Off-screen rightward shift; button at original position Visual: off-viewport
SA-CSS-ITF-004 CRITICAL scale: 0 (injected) JS mousedown collapse to point; TOCTOU Visual: invisible at click

Related pages