Security Guide
MCP server CSS trig functions consent security — sin() width collapse at mousedown, cos() height animation zero-crossing, atan2() obfuscated dimension bypass, translate sin() off-screen consent positioning
CSS Level 4 trigonometric functions — sin(), cos(), atan2() — are fully supported in Chrome 111+, Firefox 108+, and Safari 15.4+. When used inside calc() for element dimensions, these functions produce consent-element widths and heights that equal exactly zero at specific angle values: sin(0deg)=0, sin(180deg)=0, cos(90deg)=0. An MCP server that sets a custom property angle to these zero-crossing values at install mousedown collapses consent width or height to zero for the precise click window — visible at load, invisible at commit, restored after. No CSS transition is declared on the width or height property, so transition-property auditors find nothing. The element remains in the DOM with correct color, opacity, and visibility.
How CSS trigonometric functions introduce consent dimension attacks
Before CSS Level 4 math functions, achieving a dimension of zero required explicit height: 0, width: 0, max-height: 0, or transform: scale(0) — all of which are well-known consent-hiding patterns that static auditors flag. Trigonometric functions introduce an entirely new attack surface: a dimension expressed as calc(1px * sin(var(--phase) * 1deg) * 100) evaluates to a non-zero pixel value when --phase is 90, and evaluates to exactly zero when --phase is 0. No explicit zero appears anywhere in the CSS source.
Because the CSS engine resolves these expressions at layout time, getComputedStyle().width and getComputedStyle().height return the actual computed pixel value — not the custom property angle token. This means trig-based attacks are in principle detectable via computed style inspection, but only if the auditor reads the computed style at the exact moment the dimension is zero. One-shot auditors that snapshot computed styles at page load will see the correct full-size value and will not encounter the collapse that happens exclusively at install mousedown.
The four attacks documented here represent the four main trig-function exploitation paths: direct width collapse via sin() at mousedown, animated height collapse via cos() zero-crossing, off-screen positioning via translate sin(), and geometric obfuscation via atan2().
Attack 1: sin() width collapse at install mousedown (SA-CSS-TRIG-001)
This attack uses the mathematical identity sin(0°) = 0 to collapse a consent container's width to exactly zero at the moment the install button is pressed. The custom property --install-phase is initialised to 90, causing sin(90°) = 1 and thus a full-size width of 100px. A mousedown event listener on the install button, registered with capture: true to fire before any other handlers, changes --install-phase to 0. At that angle, sin(0°) = 0, collapsing width to zero. The handler also queues a restoration to 90 on the next click event, so the element regains its full size immediately after commit.
/* Attack CSS: sin() width collapse */
.consent-wrapper {
width: calc(1px * sin(var(--install-phase, 90) * 1deg) * 100);
overflow: hidden;
}
/* Default: --install-phase = 90
sin(90deg) = 1 → width = 1px * 1 * 100 = 100px (full consent visible)
At mousedown: --install-phase = 0
sin(0deg) = 0 → width = 1px * 0 * 100 = 0px (consent collapsed)
No transition property declared on width — transition auditors find nothing.
No animation property — animation auditors find nothing.
getComputedStyle().width at page load → "100px" (PASS)
getComputedStyle().width during mousedown → "0px" (only caught with interaction sim)
*/
// Attack JS: mousedown capture handler flips --install-phase to zero
document.querySelector('#install-btn').addEventListener('mousedown', (e) => {
document.documentElement.style.setProperty('--install-phase', '0');
// Restore after click so consent appears valid post-install
document.querySelector('#install-btn').addEventListener('click', () => {
document.documentElement.style.setProperty('--install-phase', '90');
}, { once: true });
}, { capture: true });
// At page load: width=100px — static audit reads correct dimension
// At mousedown (capture): --install-phase set to 0 → width collapses to 0px
// User's click completes — consent was "shown" (at load) but invisible at commit
// Restore: width snaps back to 100px before success overlay appears
No transition, no animation — auditors miss the collapse: Because no transition property is set on width, and no @keyframes or animation property is involved, automated auditors that scan for animated or transitioning consent dimensions will find nothing to flag. The collapse is instantaneous, driven purely by a custom property change at mousedown.
Attack 2: Registered @property angle animated through cos() zero-crossing (SA-CSS-TRIG-002)
This attack registers a custom property --phase-rad with the CSS Typed OM (@property syntax: <number>). Because the property is registered with a numeric type, the CSS animation engine can interpolate it smoothly from 0 to 90 over three seconds. The consent container's height is expressed as calc(cos(var(--phase-rad) * 1deg) * 80px). At the start of the animation (--phase-rad = 0), cos(0°) = 1 and height is 80px — consent is fully visible. At the end of the animation (--phase-rad = 90), cos(90°) = 0 and height is 0px — consent has collapsed. The install button becomes enabled only at t=2.9s, when height has shrunk to approximately 1.4px.
/* Attack CSS: registered @property + @keyframes cos() zero-crossing */
@property --phase-rad {
syntax: '<number>';
initial-value: 0;
inherits: false;
}
@keyframes collapse-consent {
0% { --phase-rad: 0 } /* cos(0deg) = 1 → height = 80px */
100% { --phase-rad: 90 } /* cos(90deg) = 0 → height = 0px */
}
.consent-box {
height: calc(cos(var(--phase-rad) * 1deg) * 80px);
overflow: hidden;
animation: collapse-consent 3s linear forwards;
}
/* Timeline:
t=0s: height = cos(0°) × 80px = 80px (full consent visible)
t=1.5s: height = cos(45°) × 80px ≈ 56.6px
t=2.9s: height = cos(87°) × 80px ≈ 4.2px ← install button enabled here
t=2.95s: height = cos(88.5°) × 80px ≈ 2.1px
t=3s: height = cos(90°) × 80px = 0px (consent collapsed)
Auditor re-check required at t=0s AND t=3s to catch the collapse.
An auditor measuring height at page load (t=0) sees 80px — PASS.
The animation-name 'collapse-consent' is the only visible signal in CSS.
*/
// Attack JS: enable install button only when consent height is near-zero
const btn = document.querySelector('#install-btn');
btn.disabled = true;
// Enable install button at t=2.9s — consent is ~4px tall at this point
setTimeout(() => {
btn.disabled = false;
// By the time the user reads and clicks (human reaction time ~150-300ms),
// the animation has completed and consent height is ≈ 0px.
}, 2900);
/* Registered @property allows smooth numeric interpolation of --phase-rad.
An unregistered custom property cannot be animated this way — the engine
would flip from 0 to 90 instantaneously at 50%.
getComputedStyle(el).height returns the live computed px value at any moment.
Auditor must re-measure at t=3s to catch height=0 state. */
Trig-function expressions in CSS are resolved at layout time — getComputedStyle().width returns the actual computed pixel value, not the angle variable. This is different from unregistered custom property animation where getComputedStyle returns the variable token. Trig attacks are correctly reflected in computed styles when read — but only at the exact moment the property is read. One-shot auditors reading at page load miss the collapse that happens at install mousedown.
Attack 3: translate sin() off-screen consent positioning (SA-CSS-TRIG-003)
Rather than collapsing dimensions, this attack exploits the sin() function inside a CSS translate value to move the consent block off-screen to the left by 400px while the install button remains in its normal on-screen position. The custom property --slide defaults to 90, causing sin(90°) = 1 and an x-translation of -400px. At install click, JavaScript sets --slide to 0, causing sin(0°) = 0 and bringing the consent block to its natural in-place position for exactly one layout frame — just long enough for the click to register over the now-positioned element. Immediately after, a success overlay covers the page, preventing the user from seeing the consent block flicker into view.
/* Attack CSS: translate sin() off-screen positioning */
.consent-block {
/* default --slide = 90 → sin(90deg) = 1 → translateX(-400px) = off-screen left */
translate: calc(sin(var(--slide, 90) * 1deg) * -400px) 0;
}
/* At page load:
getBoundingClientRect().left ≈ (natural position - 400px)
The element is off-screen — consent is not visible to the user.
At install click (after JS sets --slide = 0):
sin(0deg) = 0 → translate(0px, 0) → element snaps to natural position.
One layout frame later, success overlay is shown.
The element is NOT hidden (visibility: visible, opacity: 1, display: block)
Static auditors checking visibility/opacity/display find no issues.
getBoundingClientRect() at load shows left < 0 — may be classified as
"benign hidden element" rather than a consent bypass by naive auditors. */
// Attack JS: snap consent block into place at install click, then show overlay
document.querySelector('#install-btn').addEventListener('click', () => {
// Move consent block to natural position for one frame
document.documentElement.style.setProperty('--slide', '0');
// Show success overlay one frame later — before user can see consent
requestAnimationFrame(() => {
document.querySelector('.success-overlay').style.display = 'block';
// Restore consent block off-screen position for future audits
document.documentElement.style.setProperty('--slide', '90');
});
});
/* The attack relies on the timing window between:
1. The click handler setting --slide=0 (consent snaps in)
2. The rAF callback showing the overlay (consent covered again)
Within this window, the click already registered — install proceeds.
Detection: check getBoundingClientRect().left < -50px at load.
Then re-check after dispatching a synthetic click on the install button.
Any consent element that was off-screen at load but moves on-screen at
click is a high-confidence consent positioning attack. */
Off-screen elements misclassified as benign: Auditors that detect off-screen elements via getBoundingClientRect().left < 0 may classify them as "benign hidden element" (e.g., a slide-out menu panel or a tooltip) rather than a consent bypass. The off-screen consent element in SA-CSS-TRIG-003 has correct visibility, opacity, color, and display values — only its translation places it out of view. Classification logic must account for off-screen positioning of consent-labelled elements specifically.
Attack 4: atan2() geometric obfuscation of consent collapse (SA-CSS-TRIG-004)
The atan2(y, x) function returns the angle (in radians) of the point (x, y) from the origin. When the point is at (1px, 0px), atan2(0px, 1px) = 0rad = 0° and cos(0°) = 1, yielding a consent height of 100px. When the point is at (0px, 1px) — "vertical" — atan2(1px, 0px) = π/2 rad = 90° and cos(90°) = 0, collapsing consent height to 0px. The custom properties are named --geom-x and --geom-y, making the code read as a geometric layout parameter rather than a consent-collapse trigger. Code review auditors are unlikely to identify this as a security issue without knowing the mathematical identity cos(atan2(1,0)) = 0.
/* Attack CSS: atan2() geometric obfuscation */
.consent-block {
/*
* atan2(y, x) gives the angle of the point (x,y) from origin.
* cos(atan2(0px, 1px)) = cos(0rad) = 1 → height = 100px (horizontal point = full height)
* cos(atan2(1px, 0px)) = cos(π/2rad) = 0 → height = 0px (vertical point = zero height)
*
* Default: --geom-y=0px, --geom-x=1px → height = 100px (consent visible)
*/
height: calc(cos(atan2(var(--geom-y, 0px), var(--geom-x, 1px))) * 100px);
overflow: hidden;
}
/* Code reads as: "height is determined by the cosine of the geometry angle"
A code reviewer sees coordinate parameters, not a consent collapse trigger.
The variable names --geom-y and --geom-x suggest layout/positioning intent.
Nothing in the property names reveals the collapse mechanism. */
// Attack JS: rotate geometry to vertical at install mousedown
document.querySelector('#install-btn').addEventListener('mousedown', () => {
// "Rotate geometry to vertical" — sets atan2(1px, 0px) = 90° → cos(90°) = 0
document.documentElement.style.setProperty('--geom-y', '1px');
document.documentElement.style.setProperty('--geom-x', '0px');
// Restore geometry to horizontal after click (consent reappears)
document.querySelector('#install-btn').addEventListener('click', () => {
document.documentElement.style.setProperty('--geom-y', '0px');
document.documentElement.style.setProperty('--geom-x', '1px');
}, { once: true });
}, { capture: true });
/* At page load:
--geom-y=0px, --geom-x=1px → atan2(0,1)=0° → cos(0°)=1 → height=100px
getComputedStyle(el).height → "100px" (static audit PASS)
At mousedown (capture):
--geom-y=1px, --geom-x=0px → atan2(1,0)=90° → cos(90°)=0 → height=0px
Consent is collapsed at click commit.
A code review sees: "setProperty('--geom-y','1px')" — ambiguous intent.
Only interaction simulation with post-mousedown re-measurement catches this.
*/
Detection: SkillAudit's Phase 2 interaction simulation dispatches a synthetic mousedown (capture: true) on the install button and re-measures all consent element dimensions after one requestAnimationFrame. This catches SA-CSS-TRIG-001 and SA-CSS-TRIG-004 reliably. SA-CSS-TRIG-002 requires Phase 3 timed re-measurement at 0.5s, 1s, 2s, 3s, 5s. SA-CSS-TRIG-003 requires checking getBoundingClientRect().left < -50px at load and re-checking after install click.
Why standard auditors miss trig-function consent attacks
Conventional CSS consent auditors operate on a list of known-bad property-value pairs: height: 0, opacity: 0, visibility: hidden, display: none, transform: scale(0), clip-path: inset(100%). Trig-function attacks produce none of these patterns. At page load, the computed width or height is a normal positive pixel value. There is no transition or animation on the dimension property itself in SA-CSS-TRIG-001 and SA-CSS-TRIG-004. There is no explicit zero anywhere in the CSS source for any of the four attacks.
The only approach that reliably detects all four attacks is behavioral simulation: trigger the install flow with a synthetic mousedown and measure consent dimensions throughout the interaction timeline. Static property inspection alone will always miss the zero-crossing window.
Findings summary
sin() width collapse at install mousedown via --install-phase custom property set to 0 at mousedown capture — consent width = 0px at click commit, no transition or animation property visible to auditors.@property angle animated from 0 to 90 through cos() zero-crossing — height collapses to 0px at t=3s; install button enabled at t=2.9s when height ≈ 4px; timed re-measurement required at 3s.translate sin() off-screen consent positioning — consent block at translateX(-400px) by default; snaps to natural position at install click, covered by success overlay one frame later; off-screen detection may misclassify as benign.atan2() geometric obfuscation — coordinate-named custom properties rotate from horizontal to vertical at mousedown, producing cos(90°)=0 height collapse; code review auditors unlikely to identify geometric variable names as consent-collapse triggers.Summary table
| Attack | Severity | Zero-crossing | Browser support | Detection |
|---|---|---|---|---|
SA-CSS-TRIG-001: sin() width collapse at mousedown |
Critical | sin(0deg) = 0 — angle driven to 0 at mousedown capture |
Chrome 111+, Firefox 108+, Safari 15.4+ | Phase 2 interaction simulation: synthetic mousedown + rAF re-measure |
SA-CSS-TRIG-002: cos() height animation zero-crossing |
High | cos(90deg) = 0 — animated from 0 to 90 over 3s via registered @property |
Chrome 111+, Firefox 108+, Safari 15.4+; @property Chrome 85+, Firefox 128+, Safari 16.4+ |
Phase 3 timed re-measurement at 0.5s, 1s, 2s, 3s, 5s post-load |
SA-CSS-TRIG-003: translate sin() off-screen positioning |
High | sin(90deg) = 1 → translateX(-400px); sin(0deg) = 0 → translateX(0) |
Chrome 111+, Firefox 108+, Safari 15.4+ | Check getBoundingClientRect().left < -50px at load; re-check after install click |
SA-CSS-TRIG-004: atan2() geometric obfuscation |
Medium | cos(atan2(1px, 0px)) = cos(90°) = 0 — coordinate rotation at mousedown |
Chrome 111+, Firefox 108+, Safari 15.4+ | Phase 2 interaction simulation: synthetic mousedown + rAF re-measure; atan2 pattern flag in static analysis |
Defences and detection recommendations
Phase 2 interaction simulation (SA-CSS-TRIG-001, SA-CSS-TRIG-004): Dispatch a synthetic mousedown event with capture: true on the install button, then wait one requestAnimationFrame before re-measuring all consent element dimensions via getBoundingClientRect(). Any consent element whose width or height drops by more than 20% between the initial measurement and the post-mousedown measurement should be flagged as a critical finding.
Phase 3 timed re-measurement (SA-CSS-TRIG-002): Measure consent element computed dimensions at 500ms intervals from page load up to 5s. A consent element whose computed height or width crosses below 20px at any point during this window should trigger a high finding, even if it was correctly sized at the initial page-load measurement.
Off-screen consent positioning (SA-CSS-TRIG-003): At page load, check getBoundingClientRect().left and .top for all consent-labelled elements. Elements where left < -50px or top < -50px should be flagged as off-screen, and their classification should account for the possibility that they are consent blocks displaced via translate — not merely decorative or interactive overlay elements.
Static trig-expression pattern matching: Scan computed styles and inline style attributes for calc( expressions containing sin(, cos(, or atan2( used in dimension properties (width, height, translate, transform). Flag these for deeper behavioral analysis regardless of the current computed value, since the zero-crossing may not yet have been triggered.
Registered @property animation monitoring: CSS @property registrations that declare a numeric or angle type should be cross-referenced against any @keyframes that reference the same property name. If the animated range includes a trig zero-crossing angle (0°, 90°, 180°, 270°, 360°) and the property is used in a dimension expression, flag the animation for timed re-measurement.