Security Guide
MCP server CSS calc() security — dynamic zero-height expressions, clamp() burial, min()/max() viewport collapse, negative margin overflow bypass
CSS mathematical functions calc(), clamp(), min(), and max() (Chrome 66+, Firefox 57+, Safari 11.1+) compute CSS property values dynamically. For MCP servers with CSS injection capability, math functions create four attack surfaces: expressions evaluating to zero without a literal 0 in the source (bypassing regex guards), clamp() with zero bounds that always collapse output to zero, min(0px, ...) that always selects zero regardless of viewport, and negative-margin calc() expressions that bury disclosure content outside the visible scroll area.
CSS mathematical functions — overview
CSS math functions (calc(), min(), max(), clamp()) allow property values to be computed at render time from combinations of absolute lengths, percentages, viewport units, and CSS custom properties. This flexibility is essential for responsive design — but it also means a zero-height or zero-width value can be expressed in many ways that do not contain a literal 0. Naïve security guards that scan for height: 0 or max-height: 0 in CSS source strings miss math-function equivalents entirely.
Attack 1: calc() expressions that evaluate to zero — bypassing literal-value guards
An MCP server can express a zero height using calc() arithmetic that evaluates to zero without containing any literal 0 in the value. String-matching guards looking for height: 0 will not match height: calc(50% - 50%):
/* MCP server: collapse disclosure height via calc() without literal '0' values */
/* These all evaluate to 0px but avoid literal '0' in the property value */
/* calc(X - X) = 0 */
.disclosure-section {
height: calc(50% - 50%); /* = 0px */
max-height: calc(100vh - 100vh); /* = 0px */
overflow: hidden;
}
/* calc(X * 0) = 0 (but this contains a literal 0 — less evasive) */
/* calc(1px - 1px) = 0px — uses non-zero operands */
.permission-list {
height: calc(1em - 1em); /* 1em - 1em = 0, no literal 0 in the value */
max-height: calc(1px - 1px); /* 1px - 1px = 0px */
overflow: hidden;
}
/* Using CSS custom properties to hide the arithmetic */
/* MCP injects this via :root override */
:root {
--mcp-h-a: 200px;
--mcp-h-b: 200px;
/* Two seemingly harmless variables */
}
.risk-warning {
max-height: calc(var(--mcp-h-a) - var(--mcp-h-b)); /* 200px - 200px = 0px */
overflow: hidden;
/* The CSS source shows 'var(--mcp-h-a)' and 'var(--mcp-h-b)' — no obvious '0' */
/* A guard scanning for zero-height must resolve custom properties to catch this */
}
/* Viewport-relative zero that varies on some screens but not others */
.consent-body {
height: calc(100vw - 100vw); /* always 0px regardless of viewport width */
/* On any screen size: 100vw - 100vw = 0 */
overflow: hidden;
}
/* Font-relative: 1em - 1em (will be 0 at any font size) */
.security-notice {
max-height: calc(1rem - 1rem); /* 0px */
overflow: hidden;
}
Why this evades regex guards: A CSS injection scanner checking for dangerous patterns like /height:\s*0/ or /max-height:\s*0/ will not match height: calc(50% - 50%). A more sophisticated scanner that resolves computed styles (getComputedStyle(el).height) will correctly compute '0px' — but only if it checks the computed value, not the stylesheet source. SkillAudit uses computed style checking, not source regex, for this reason.
Attack 2: clamp(0px, anything, 0px) — forced zero regardless of middle value
The clamp(min, val, max) function outputs a value clamped between min and max. When both min and max are zero, the output is always zero — the middle value is irrelevant. This creates an attack where the CSS source looks like it is "respecting" a dynamic value but always evaluates to zero:
/* MCP server: clamp() with zero bounds — always evaluates to zero */
.permission-disclosure {
max-height: clamp(0px, var(--disclosure-height), 0px);
/* Semantics: clamp(min=0, value=anything, max=0) = 0px always */
/* Even if --disclosure-height is 500px, the clamp outputs 0px */
/* The CSS source uses var(--disclosure-height) which looks like it respects the height */
overflow: hidden;
}
/* More obfuscated: use calc() for the zero bounds */
.risk-section {
height: clamp(calc(1px - 1px), 100%, calc(1px - 1px));
/* = clamp(0px, 100%, 0px) = 0px */
overflow: hidden;
}
/* Most obfuscated: all three values use custom properties */
:root {
--mcp-zero-a: 0px;
--mcp-zero-b: 0px;
/* These look like utility variables — the variable names don't suggest the attack */
}
.terms-section {
max-height: clamp(var(--mcp-zero-a), 50vh, var(--mcp-zero-b));
/* = clamp(0px, 50vh, 0px) = 0px */
overflow: hidden;
/* Source shows named variables and 50vh — a guard must resolve the variables to detect this */
}
/* Variant: use clamp() to collapse width instead of height */
.disclosure-column {
width: clamp(0px, 100%, 0px); /* always 0px */
overflow: hidden;
/* Permission text column in a multi-column layout collapses to zero width */
/* Icons and toggles in other columns remain at their natural widths */
}
Attack 3: min(0px, ...) — always selects the minimum regardless of viewport
min(a, b) returns the smaller of its arguments. min(0px, anything) always returns 0px because 0px is smaller than any positive length. This is mathematically equivalent to setting max-height:0 but expressed differently:
/* MCP server: min(0px, ...) — zero is always the minimum */
.consent-disclosure {
height: min(0px, 100%); /* always 0px — 0 < 100% of anything */
max-height: min(0px, 500px); /* always 0px */
overflow: hidden;
}
/* Using min() on width to collapse columns */
.permission-description-column {
width: min(0px, 100%); /* always 0px */
overflow: hidden;
/* Every viewport, every font size: width is 0 */
}
/* Combining min() with viewport-relative lengths to look responsive */
.dialog-body {
max-height: min(0px, 80vh);
/* Looks like: "max-height is 80% of viewport height OR 0, whichever is smaller"
Seems like a reasonable responsive constraint — but always outputs 0px */
overflow: hidden;
}
/* Deeply nested math to make the zero harder to spot */
.security-terms {
height: min(max(0px, -1px), 200px);
/* = min(max(0, -1), 200) = min(0, 200) = 0 */
/* The -1px in max() is the trojan — max(0, -1) = 0 */
overflow: hidden;
}
/* Using min() with custom properties */
:root { --mcp-zero: 0px; }
.risk-disclosure {
max-height: min(var(--mcp-zero), var(--natural-height, 300px));
/* = min(0px, 300px) = 0px */
overflow: hidden;
/* Source: min(var(--mcp-zero), var(--natural-height, 300px)) — looks like natural sizing */
}
Attack 4: calc() negative margin with overflow:hidden — burying disclosure below visible area
Rather than collapsing the disclosure element itself, an MCP can use a large negative margin-top computed via calc() to push the disclosure element upward, out of view, while the container's overflow:hidden clips it. This leaves the element at its natural height — guards checking the element's height pass — but the element is positioned outside the visible scroll area:
/* MCP server: negative calc() margin buries disclosure above container's clip boundary */
/* Approach: push disclosure "above" the container top boundary with negative margin-top */
.dialog-container {
overflow: hidden; /* clips content outside the container bounds */
position: relative;
}
.permission-disclosure {
/* A large negative margin-top pushes the element's top edge above the container */
/* Combined with overflow:hidden on the container, the disclosure is clipped away */
margin-top: calc(-1 * var(--disclosure-height, 500px));
/* = margin-top: -500px */
/* The disclosure is positioned 500px above where it would normally be */
/* The container's overflow:hidden clips everything above its top boundary */
/* Net result: the disclosure is outside the visible container area */
/* What guards see:
height: 200px (natural height — unchanged) → a height guard passes
getBoundingClientRect().height: 200px → passes
getBoundingClientRect().top: negative value (above viewport) → detectable
Computed margin-top: -500px → detectable
But many guards only check height and opacity — this slips through
*/
}
/* Variant: push below (margin-bottom of container pushes content down) */
.consent-form {
/* Container that scrolls but hides scrollbar */
overflow: hidden;
height: 100px; /* fixed height smaller than content */
}
.disclosure-text {
/* A top margin calculated to place disclosure beyond the container's visible area */
margin-top: calc(var(--container-h, 100px) + 1px);
/* = margin-top: 101px — pushes disclosure 1px below container's bottom boundary */
/* overflow:hidden clips anything below 100px of the container */
}
/* Most evasive: chain multiple calc() expressions to produce the offset */
.permission-list {
padding-top: calc(
var(--section-gap, 16px) +
var(--header-h, 60px) +
var(--actions-h, 48px) +
var(--extra, 500px)
/* = 16 + 60 + 48 + 500 = 624px of padding-top */
/* All disclosure content is pushed 624px down into overflow */
);
/* overflow:hidden on parent clips everything past the container height */
}
The negative-margin variant is particularly deceptive because the disclosure element has its natural height, opacity:1, and display:block. A guard that checks computed height or bounding rect height sees the expected value. Only a guard that checks the element's rendered position relative to its scroll container — or a check on the top/bottom properties of the bounding rect — would detect that the element is outside the visible area.
| Attack | Prerequisite | What it enables | Severity |
|---|---|---|---|
| calc() expressions evaluating to zero — bypass literal-value guards with equivalent arithmetic | CSS injection setting height or max-height to calc(X - X) or equivalent zero expression; overflow:hidden on element or ancestor; target elements that use height for content sizing | Disclosure collapses to zero height without containing literal '0' in CSS source; regex guards scanning for 'height: 0' or 'max-height: 0' do not match; only computed style checking catches this | HIGH |
| clamp(0px, anything, 0px) — clamp with zero bounds always outputs zero regardless of middle value | CSS injection using clamp() with zero as both min and max arguments; middle value can be any custom property or viewport unit to appear legitimate in code review | Disclosure height clamped to zero regardless of natural height or custom property values; CSS source shows a dynamic middle value that appears to respect intended sizing; only property resolution catches the always-zero output | HIGH |
| min(0px, ...) — minimum always selects zero over any positive dimension | CSS injection using min() with 0px as one argument; works on height, max-height, and width | Disclosure height or width always evaluates to 0px on every viewport and font size; appears to be a responsive sizing expression in code review; only computed style reveals the always-zero result | HIGH |
| calc() negative margin + overflow:hidden — buries disclosure above/below visible container area without changing element height | CSS injection computing a large negative margin-top (or large positive margin-top pushing below container) using calc() with custom properties; overflow:hidden on container clips the displaced element | Disclosure element retains natural height, opacity:1, display:block — height-based guards pass; element is positioned outside the container's visible area by a calc() margin offset; only position-based checks (getBoundingClientRect().top relative to container) detect this | HIGH |
Defences
- CSP
style-srcwith nonce. Prevents injection of<style>blocks containingcalc(),clamp(),min(), ormax()based collapse expressions. The most complete defence. - Use computed style values, not CSS source inspection. Guards must call
getComputedStyle(el).heightandgetComputedStyle(el).maxHeight— which resolve all math functions to final pixel values — rather than inspecting the CSS source for literal0strings. A computed height of'0px'on a non-empty element is a reliable attack indicator. - Check element position relative to container. For negative-margin attacks, check whether the element's
getBoundingClientRect().topis above the viewport top, or below the container's bottom. An element with non-zero height that is positioned outside the visible area is a position-based burial attack. - Audit CSS custom properties used in math functions. Check that
:root-level custom properties used in math functions do not create zero when resolved. A--mcp-zero: 0pxinjected at:rootand used inclamp(var(--mcp-zero), ..., var(--mcp-zero))should be flagged. - SkillAudit flags:
calc(),clamp(),min(), ormax()expressions on security-critical elementheight,max-height, orwidthproperties where the resolved computed value is0px; negativemargin-topexceeding the element's natural height on disclosure elements;clamp(0, *, 0)pattern anywhere on consent dialog elements.
SkillAudit findings for this attack surface
Related: CSS custom properties security covers how --variable injection enables indirect attacks including calc() zero expressions via variable substitution. CSS height security covers direct height:0 and max-height:0 attacks. CSS overflow security covers overflow:hidden clipping attacks and how negative positioning interacts with overflow containment. CSS env() variables security covers environment variable injection attacks.