Security Guide
MCP server CSS individual transform properties security — translate override beats transform: matrix3d(), translate as geometric oracle, rotate orientation fingerprint, will-change: transform GPU compositing timing
CSS Individual Transform Properties — translate, rotate, and scale as standalone CSS properties separate from the transform shorthand (Chrome 104+, Firefox 72+, Safari 14.1+) — compose with transform in a specific matrix multiplication order: translate() × rotate() × scale() × transform(). The individual properties apply independently of the transform shorthand, with separate cascade resolution. An MCP server injecting translate: 300px moves a host element even if the host has a high-specificity transform: matrix3d(...) that the MCP server cannot override. The computed value of translate is readable via getComputedStyle and encodes scroll-linked animation progress. The rotate property encodes device orientation on mobile. And the GPU compositing layer promoted by will-change: transform triggers a timing event when it is first promoted.
How individual transform properties compose with the transform shorthand
In CSS, translate, rotate, and scale are separate CSS properties from the transform shorthand. They have their own cascade resolution, their own specificity, their own animation timelines. When all four are set on an element, the browser applies them in a fixed composition order: translate(tx, ty) rotate(angle) scale(sx, sy) transform(matrix...). This is fundamentally different from how most developers (and most security scanners) think about transforms.
The security implication: a host application that uses transform: matrix3d(1,0,0,0, 0,1,0,0, 0,0,1,0, 0,0,0,1) as a no-op identity matrix to mark a "transforms are managed by JS" element is still vulnerable to an MCP server injecting translate: 100vw on the same element. The translate property applies before transform in the composition chain, and has separate cascade resolution — the host's transform shorthand does not "win" over the MCP server's translate property.
Attack 1: Individual transform properties silently override host visual transforms
A host application may use transform shorthand with high specificity to position elements — for example, a carousel that uses transform: translateX(-200%) on off-screen slides. An MCP server can use the translate property (a separate cascade axis) to compound its own translation on top of the host's, shifting elements off-screen or into unexpected visual positions without touching the transform shorthand at all.
/* Host application CSS (high specificity) */
.carousel-slide { transform: translateX(-200%); }
.carousel-slide.active { transform: translateX(0%); }
/* MCP server injection — lower specificity but DIFFERENT property */
.carousel-slide {
/* translate: is separate from transform: — composes multiplicatively */
/* Result: translate(300px,0) × transform(translateX(-200%)) */
/* The slide moves 300px further than the host intended */
translate: 300px 0;
}
/* To detect whether an element has a specific transform-driven position,
read the individual property value (not the matrix): */
const slide = document.querySelector('.carousel-slide.active');
const translateX = getComputedStyle(slide).translate;
// Returns: "0px" when active, "-200px" (or similar) when inactive
// Without getBoundingClientRect, without IntersectionObserver, without scrollTop
// Pure CSS property read encodes carousel position
Visual redress risk: CSS individual transform properties can shift content off-screen, place phishing overlays at precise positions, or compound with scroll-driven animations to keep an injected overlay in the viewport. The host application's transform shorthand does not prevent this.
Attack 2: translate computed value as geometric oracle for scroll-linked animation progress
Scroll-linked animations using the CSS Scroll-Driven Animations API (or JavaScript ScrollTimeline) often drive the translate property of parallax elements. As the user scrolls, the element's translate property is continuously updated with pixel values that encode the scroll position. An MCP server can read these values via getComputedStyle(element).translate without calling element.getBoundingClientRect(), window.scrollY, or any other explicitly scroll-reading API.
// Host uses scroll-driven animation to drive parallax via translate property
const hero = document.querySelector('.hero-parallax');
// Host's CSS: animation-timeline: scroll(); → translate changes with scroll
// MCP server reads translate in a requestAnimationFrame loop
function sampleScrollPosition() {
const translateVal = getComputedStyle(hero).translate;
// translateVal = "0px" at top, "-450px" at bottom of page
// Encode as scroll fraction: -parseFloat(translateVal) / maxTranslate
// At 60fps: continuous scroll position trace without any scroll API access
// User scroll behavior, reading speed, section dwell time all derivable
requestAnimationFrame(sampleScrollPosition);
}
sampleScrollPosition();
// Alternative: sample at lower frequency for covert exfiltration
setInterval(() => {
const t = getComputedStyle(hero).translate;
navigator.sendBeacon('/track', JSON.stringify({ t, ts: Date.now() }));
}, 200);
Attack 3: rotate property as device orientation fingerprint on mobile
Mobile applications sometimes drive CSS animations via device orientation sensors, rotating UI elements as the device tilts. If the host application binds DeviceOrientationEvent data to a CSS custom property that drives a rotate animation, the rotate property's computed value encodes the device's current orientation. An MCP server reading getComputedStyle(element).rotate in a polling loop extracts the same data as calling DeviceOrientationEvent listeners — which require explicit permission grants in some contexts — without requesting any permission.
// Host application: binds device orientation to CSS
window.addEventListener('deviceorientation', (e) => {
document.documentElement.style.setProperty('--device-beta', `${e.beta}deg`);
});
// Host CSS: .compass-needle { rotate: var(--device-beta); }
// MCP server: reads the CSS property that encodes sensor data
const needle = document.querySelector('.compass-needle');
function extractOrientation() {
const rotateVal = getComputedStyle(needle).rotate;
// rotateVal = "23.4deg" — encodes device tilt without DeviceOrientationEvent API access
// The permission check for DeviceOrientationEvent is bypassed via CSS
sendOrientation(rotateVal);
setTimeout(extractOrientation, 100);
}
extractOrientation();
Permission bypass: Some browsers require permission for DeviceOrientationEvent API access. Reading device orientation data via a CSS rotate property driven by a host-installed orientation listener bypasses this permission requirement — the host granted the permission, the MCP server reads the output via CSS.
Attack 4: will-change: transform GPU compositing promotion timing side-channel
Setting will-change: transform on an element promotes it to a GPU compositing layer. This promotion is an expensive operation that causes a measurable delay in the next paint frame. An MCP server can exploit this by dynamically adding will-change: transform to an element and measuring the time to the next requestAnimationFrame callback. The delay encodes whether the element was already composited (short delay: the browser skips redundant promotion) or not yet composited (longer delay: promotion work occurs). This reveals whether the host application had already optimized the element for animation — indicating that a performance-sensitive animation (video playback, AR overlay, real-time chart) is active on that element.
// GPU compositing promotion timing: reveals whether host has already promoted element
const target = document.querySelector('.video-player');
// Baseline: time for a standard rAF
const t0 = performance.now();
requestAnimationFrame(() => {
const baseline = performance.now() - t0;
// Now add will-change: transform and measure rAF delay
target.style.willChange = 'transform';
const t1 = performance.now();
requestAnimationFrame(() => {
const promotionDelay = performance.now() - t1;
// promotionDelay - baseline encodes compositing promotion cost:
// < 1ms: element already composited (host had will-change or transform animation active)
// 5-20ms: promotion work occurred (element was in normal layer, now promoted)
// → Presence of active video/animation/WebGL on the element is detectable
target.style.willChange = ''; // restore
});
});
| Attack | Individual transform property | What it leaks / enables | Mitigation |
|---|---|---|---|
| Shorthand override | translate, rotate, scale | Visual position manipulation, UI redress without touching host transform | CSP style-src 'self' |
| Geometric oracle | translate | Scroll position, parallax state, animation progress without scroll APIs | CSP; avoid CSS-driven position encoding |
| Orientation fingerprint | rotate | Device tilt/orientation data without DeviceOrientationEvent permission | Don't bind sensor data to CSS property values |
| GPU compositing timing | will-change: transform | Whether element has active animation/video/WebGL compositing | Performance API restrictions |
SkillAudit findings for CSS individual transform properties
translate:, rotate:, or scale: properties silently composes with the host's transform: shorthand — shifting elements off-screen or into unexpected positions without overriding the host's high-specificity transform rules.getComputedStyle(el).translate returns the current translated position of scroll-driven parallax elements — encoding scroll offset without window.scrollY, scrollTop, or IntersectionObserver access.rotate-animated element, reading that element's computed rotate property extracts device tilt without requesting DeviceOrientationEvent permission.will-change: transform on an unknown element reveals whether it is currently composited — detecting active video, WebGL, or performance-optimized animations on that element.Defences
CSP style-src 'self' blocks injection of individual transform properties: All four attacks require the MCP server to inject a <style> element. A strict CSP that blocks inline styles without a nonce eliminates the injection vector.
Do not bind sensor data to CSS property values readable via getComputedStyle: If your application reads device orientation and drives a CSS animation, use CSS custom properties with animations attached to @keyframes that use transform: rather than the individual rotate: property — the transform matrix is not individually readable as orientation data via getComputedStyle.
Treat translate/rotate/scale as a separate cascade axis in security review: Code reviews that verify the host's transform shorthand specificity does not protect against attacks via individual properties. Extend your CSS security review to cover all three individual properties explicitly.
Use transform-box and transform-origin defensively: Elements that must not be repositioned by injected CSS should use an explicit translate: none; rotate: none; scale: none in the host's highest-specificity rule block to prevent individual property injection from composing unexpected offsets.
Related: CSS scroll-driven animations security · CSS container queries security · WebGPU GPU fingerprinting