Security Guide
MCP server CSS inset shorthand consent security — four-edge positional displacement attacks
The CSS inset property is a shorthand for top, right, bottom, and left, setting all four positional offsets in a single declaration. On positioned consent elements (position: fixed, position: absolute), extreme inset values displace the element off all four viewport edges simultaneously. inset: 0 on a higher-z-index overlay positions a click-capture layer precisely over the consent dialog. Both attacks leave DOM text content intact, evading text-presence guards while making the consent invisible or non-interactive.
How inset works
The CSS inset property (CSS Logical Properties Level 1, widely supported) is shorthand for top right bottom left using the same multi-value syntax as margin. inset: 1px sets all four offsets to 1px. inset: 10px 20px sets top/bottom to 10px and right/left to 20px. inset: 10px 20px 30px 40px sets each individually. The values participate in CSS constraint layout: for a position: fixed element, setting both top and bottom (or using inset equivalents) simultaneously constrains the element's vertical extent. If both offsets exceed the viewport, the element is pushed entirely off-screen. Security guards that check individual axis values (top, left) but do not compute the composite effect of inset shorthand may miss the compound displacement.
/* inset shorthand values */
inset: 10px; /* top:10px right:10px bottom:10px left:10px */
inset: 10px 20px; /* top:10px right:20px bottom:10px left:20px */
inset: 10px 20px 30px; /* top:10px right:20px bottom:30px left:20px */
inset: 10px 20px 30px 40px; /* top:10px right:20px bottom:30px left:40px */
/* For position:fixed — expands to: */
.element { top: 10px; right: 10px; bottom: 10px; left: 10px; }
/* getComputedStyle expansion: */
/* inset is expanded to individual top/right/bottom/left properties */
/* getComputedStyle(.element).top → "10px" */
/* getComputedStyle(.element).inset → "10px" (shorthand not always supported) */
/* Key attack insight: a guard that only checks getBoundingClientRect()
will catch the off-screen position, but a guard that checks
getComputedStyle().top individually may not compute the combined effect
of inset when the shorthand sets opposing edges simultaneously */
Compound displacement gap: Security guards that check getComputedStyle().top for extreme values may miss inset when both top and bottom are set simultaneously (the constraint interaction is only visible via layout, not the individual property values). The reliable detection is getBoundingClientRect() — which returns the rendered position regardless of how it was achieved.
Attack 1 (CRITICAL): consent displaced off all viewport edges with inset: 100vh
On a position: fixed consent element, inset: 100vh sets all four edges (top, right, bottom, left) to 100 viewport-height units. The top offset of 100vh pushes the top edge to the bottom of the viewport; the bottom offset of 100vh pushes the bottom edge to the top. The element is simultaneously pushed below the bottom and above the top — it collapses to a zero-height rectangle between two opposing out-of-viewport constraints. The DOM textContent is fully intact; offsetHeight may return 0 due to constraint collapse.
/* Attack 1: consent displaced off all edges via inset:100vh */
.consent-dialog {
position: fixed;
inset: 100vh; /* top:100vh right:100vh bottom:100vh left:100vh */
}
/* Layout result:
top: 100vh = pushed below viewport bottom
bottom: 100vh = pushed above viewport top
left: 100vh = pushed right of viewport right edge
right: 100vh = pushed left of viewport left edge
The four constraints conflict: browser resolves via over-constrained rules
Result: element is at a position entirely off all four edges
offsetWidth/offsetHeight: 0 (collapsed)
getBoundingClientRect(): {x: vh, y: vh, width: 0, height: 0}
textContent: "You agree to the following terms..." (fully intact)
Guard checking textContent only → finds consent → reports present
Guard checking getBoundingClientRect → finds off-screen → reports attack
*/
/* Variant with deliberate unit mixing */
.consent-dialog {
position: fixed;
inset: 100vh 100vw; /* top/bottom: 100vh, right/left: 100vw */
/* Multi-unit variant; same displacement result */
}
Attack 2 (HIGH): inset: 0 click-capture overlay above consent
The attack creates an invisible overlay element positioned with inset: 0 (covering all four edges of the viewport or containing block), with a z-index higher than the consent dialog, and pointer-events: all. This overlay captures all mouse clicks, touch events, and keyboard focus traversal above the consent dialog. The user sees the consent dialog (it is visible), can read it, but cannot click the Accept or Decline buttons — all pointer events land on the transparent overlay. The consent dialog's DOM text is intact and visible; only interaction is blocked.
/* Attack 2: invisible overlay captures all clicks above consent dialog */
/* The consent dialog is visible but unreachable */
.consent-dialog {
position: fixed;
z-index: 1000;
inset: 20vh 10vw; /* centered consent dialog */
}
/* Overlay: covers entire viewport, above consent dialog */
.mcp-overlay {
position: fixed;
inset: 0; /* top:0 right:0 bottom:0 left:0 = full viewport */
z-index: 1001; /* above consent dialog */
pointer-events: all; /* captures all events */
background: transparent; /* invisible */
cursor: default; /* no pointer change indication */
}
/* Or as injected pseudo-element on body */
body::after {
content: '';
position: fixed;
inset: 0;
z-index: 9999;
pointer-events: all;
}
/* User sees consent dialog (z-index 1000)
User clicks Accept button
Click lands on overlay (z-index 1001 above consent)
Consent button never receives click event
Overlay click handler fires instead (may trigger consent acceptance)
OR consent remains unclicked while user believes they clicked it */
Attack 3 (HIGH): over-constrained inset: calc(100% + 1px) geometry
Using calc() inside inset creates over-constrained layout geometry. inset: calc(100% + 1px) on a percentage-positioned element within a containing block: both top and bottom are set to the full height of the containing block plus one pixel. This creates a geometry impossibility: the element has no valid vertical position within its containing block. The browser resolves the constraint according to spec, resulting in a zero-height element at an off-screen position. The calc() expression makes the extreme value harder to identify at a glance.
/* Attack 3: calc()-obscured over-constrained inset */
.consent-container {
position: relative;
height: 400px;
}
.consent-dialog {
position: absolute;
/* calc() obscures the extreme value */
inset: calc(100% + 1px);
/* = top:401px right:401px bottom:401px left:401px (for 400px container) */
/* Over-constrained: all four edges set beyond full container size */
/* Browser: resolves to zero-size element at computed position */
/* offsetHeight: 0 */
}
/* More obscured variant */
.consent-dialog {
position: fixed;
inset: calc(var(--consent-offset, 50%) + 51%);
/* var(--consent-offset) set to 0% by default → total = 51% */
/* But if --consent-offset = 50% → total = 101% → off-screen */
/* Custom property override can switch between visible and off-screen */
}
Attack 4 (MEDIUM): negative inset — viewport-expanding invisible overlay
Negative inset values expand a positioned element beyond its containing block boundaries. inset: -200vh -200vw on a position: fixed; opacity: 0 element creates an invisible element that extends 200 viewport heights above and below the viewport and 200 viewport widths left and right. This creates a very large transparent overlay covering a massive area. With pointer-events: all, this captures all pointer events across a much larger area than the viewport itself, preventing any element beneath it from receiving events — including consent dialogs.
/* Attack 4: negative inset expands overlay far beyond viewport */
.mcp-wide-overlay {
position: fixed;
inset: -200vh -200vw; /* extends far beyond all viewport edges */
opacity: 0;
pointer-events: all;
z-index: 9998;
/* Creates a 600vh × 600vw invisible overlay */
/* Any scroll, pan, or resize cannot escape this overlay */
/* All pointer events captured — no element beneath receives events */
}
/* With position:fixed, the negative inset extends the element
200vh above the top of the viewport
200vh below the bottom of the viewport
200vw to the left of the viewport
200vw to the right of the viewport
Total size: (200+100+200) = 500vw × 500vh
getBoundingClientRect(): very large negative x/y coordinates
Guard checking off-screen: may detect large negative position
Guard only checking positive displacement: may miss negative expansion */
Detection
/* Detect inset shorthand attacks on consent elements */
function auditInsetOnConsentElements() {
const consentSelectors = '.consent-dialog, [class*="consent"], [class*="disclosure"]';
const attacks = [];
document.querySelectorAll(consentSelectors).forEach(el => {
const style = getComputedStyle(el);
const rect = el.getBoundingClientRect();
const top = parseFloat(style.top);
const right = parseFloat(style.right);
const bottom = parseFloat(style.bottom);
const left = parseFloat(style.left);
/* Check 1: element off-screen */
const offScreen = rect.bottom < 0 || rect.top > window.innerHeight ||
rect.right < 0 || rect.left > window.innerWidth;
/* Check 2: zero or collapsed dimensions */
const collapsed = rect.width === 0 || rect.height === 0;
/* Check 3: extreme position values (absolute) */
const extremeValues = [top, right, bottom, left].some(v =>
Math.abs(v) > window.innerHeight * 1.5
);
/* Check 4: high z-index with inset:0 (overlay attack) */
const zIndex = parseInt(style.zIndex);
const isOverlay = zIndex > 100 &&
top === 0 && right === 0 && bottom === 0 && left === 0 &&
style.pointerEvents !== 'none';
if (offScreen || collapsed || extremeValues || isOverlay) {
attacks.push({
element: el,
position: style.position,
inset: `${style.top} ${style.right} ${style.bottom} ${style.left}`,
rect,
offScreen, collapsed, extremeValues, isOverlay
});
}
});
/* Also check for high-z overlay elements above consent */
document.querySelectorAll('[style*="inset: 0"], [style*="inset:0"]').forEach(el => {
const zIndex = parseInt(getComputedStyle(el).zIndex);
if (zIndex > 500 && getComputedStyle(el).pointerEvents !== 'none') {
attacks.push({ element: el, type: 'click-capture-overlay', zIndex });
}
});
return attacks;
}
| Attack | Severity | DOM textContent intact? | Detection method |
|---|---|---|---|
| inset:100vh on position:fixed — off all four edges | CRITICAL | Yes | getBoundingClientRect() off-screen check; offsetHeight === 0 |
| inset:0 click-capture overlay with high z-index | HIGH | N/A (overlay) | Enumerate position:fixed/absolute elements with inset:0 and z-index > consent; check pointer-events |
| inset:calc(100% + 1px) over-constrained geometry | HIGH | Yes | getBoundingClientRect() collapsed/off-screen; parse calc() inset values for out-of-bounds results |
| inset:-200vh/-200vw invisible overlay expansion | MEDIUM | N/A (overlay) | getBoundingClientRect() very-large-negative coordinates; flag pointer-events:all on very large elements |