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.
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 }; */
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) */
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] }; */
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. */
Summary of SA-CSS-ITF findings
| ID | Severity | Property | Mechanism | Effect |
|---|---|---|---|---|
| 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 |