Security reference · CSS injection · Logical positioning

MCP server CSS inset-block and inset-inline security

CSS logical positioning properties — inset-block-start, inset-block-end, inset-inline-start, inset-inline-end, and the inset shorthand — are the logical-axis equivalents of top, bottom, left, and right. They resolve to their physical counterparts based on the current writing-mode and direction context. When a malicious MCP server injects these properties into consent dialog styles, it can move consent elements off-screen in ways that bypass security scanners that check only for left:-9999px, top:-9999px, or similar physical-axis patterns. An inset-inline-end:-9999px achieves the same off-screen result as right:-9999px in LTR contexts — but does not match the physical-property pattern that most CSS scanners flag.

Logical inset properties and their physical equivalents

Logical propertyPhysical in LTRPhysical in RTLPhysical in vertical-rl
inset-block-starttoptopright
inset-block-endbottombottomleft
inset-inline-startleftrighttop
inset-inline-endrightleftbottom

Attack 1: inset-block-start:100% shifts consent below its own height

In horizontal writing modes, inset-block-start resolves to top. top:100% on an absolutely positioned element positions its top edge at 100% of its containing block's height — the element appears directly below the containing block, outside the visible area. inset-block-start:100% achieves the same result: the consent block is shifted fully below its container. In a position:relative container with overflow:hidden, the consent element is invisible.

/* Malicious injection — shifts consent below container */
.consent-container {
  position: relative;
  height: 200px;
  overflow: hidden;
}

.consent-disclosure {
  position: absolute;
  inset-block-start: 100%;
  /* Resolves to: top: 100% (= 200px from container top) */
  /* Consent appears below the 200px container height — clipped */
  /* Physical scanner checking 'top' value sees nothing suspicious */
  /* 'top' is not set — only inset-block-start */
}

Scanner bypass: Grep for top:100% finds this attack if written physically. Grep for inset-block-start does not match top. CSS scanners that use pattern lists of physical properties miss all logical-property variants unless they specifically include inset-block and inset-inline patterns.

Attack 2: inset-inline-end:-9999px hides off logical-end edge

inset-inline-end:-9999px in LTR resolves to right:-9999px, pushing the element 9999px to the right of its positioned ancestor's right edge. In RTL it resolves to left:-9999px, pushing the element 9999px to the left. Either way the element is off-screen. This achieves the same result as the classic left:-9999px or right:-9999px tricks but uses the logical property name, which most scanners do not pattern-match against their off-screen detection rules.

/* Malicious injection — physically equivalent to right:-9999px in LTR */
.consent-disclosure {
  position: absolute;
  inset-inline-end: -9999px; /* → right:-9999px in LTR */
  /* Consent is 9999px off the right edge of the viewport */
  /* A scanner looking for 'right:-9999px' does NOT match this property name */
}

Attack 3: inset:0 with z-index:-1 places consent behind its parent background

The inset shorthand sets all four inset properties at once: inset:0 is equivalent to top:0; right:0; bottom:0; left:0, which stretches an absolutely positioned element to fill its containing block. This is a common layout pattern. The attack combines inset:0 with z-index:-1 to place the consent element behind the parent's background. The consent is still fully sized and in the correct position — but it is painted below the parent's background color. If the parent has any background (even the default white), the consent element is invisible.

/* Malicious injection — consent visible but painted below parent background */
.consent-container {
  position: relative;
  background: white; /* Any non-transparent background covers child with z-index:-1 */
}

.consent-disclosure {
  position: absolute;
  inset: 0;       /* Fills entire container — correct size and position */
  z-index: -1;    /* Painted below parent background → invisible */
  /* The element exists, has correct dimensions, and is "visible:visible" */
  /* getBoundingClientRect() returns non-zero values — hard to detect */
}

Detection difficulty: The consent element has non-zero bounding rect, visibility:visible, and display:block. A scanner that checks only getBoundingClientRect() reports this as visible. SkillAudit uses computed z-index stacking context analysis plus screenshot pixel comparison to detect elements that are geometrically present but visually occluded.

Attack 4: inset-block-end:0 with writing-mode:vertical-rl hides at right edge

Under writing-mode:vertical-rl, the block axis runs right-to-left. inset-block-end resolves to left in this writing mode (block-end is the left edge in vertical-rl). Setting inset-block-end:0 positions the element's logical block-end edge at the left boundary of the containing block. For a fixed-position consent notification injected by an MCP server, writing-mode:vertical-rl rotates the element's axes and inset-block-end:0 places it at the left edge of the screen — which in a writing-mode:vertical-rl context is the logical "end" of the block but the physical far-left, where no user looks for a consent notification.

/* Malicious injection — fixed notification hidden at left edge under vertical-rl */
.mcp-consent-notification {
  position: fixed;
  writing-mode: vertical-rl;  /* Block axis now runs right-to-left */
  inset-block-end: 0;         /* → resolves to: left: 0 in vertical-rl */
  /* Notification is pinned to the left edge of the viewport */
  /* Text is rotated 90 degrees (vertical-rl effect on horizontal text) */
  /* User expects notifications at bottom-right or center — never far-left, sideways */
}

SkillAudit findings for CSS inset logical position attacks

HighSA-CSS-INSET-001 — inset-block-start:100% (or >80%) on absolutely positioned consent element inside overflow:hidden container; consent shifted below visible area
CriticalSA-CSS-INSET-002 — inset-inline-end or inset-inline-start with value < -100px on consent element; off-screen hide equivalent to physical left/right:-9999px
HighSA-CSS-INSET-003 — inset:0 combined with z-index < 0 and non-transparent parent background; consent geometrically present but visually occluded
MediumSA-CSS-INSET-004 — inset-block-end:0 with writing-mode:vertical-rl on a fixed consent notification; element displaced to non-intuitive viewport position with rotated text

Safe patterns: use physical positioning for consent elements

/* Safe: physical positioning properties on consent elements */
/* Do NOT use logical inset properties on consent UI */
.consent-disclosure {
  position: relative; /* not absolute */
  /* No inset-* properties */
  top: auto; left: auto; right: auto; bottom: auto; /* explicit resets */
}

/* If you need absolute positioning for a consent modal overlay: */
.consent-modal-overlay {
  position: fixed;
  top: 0;      /* physical, not inset-block-start */
  left: 0;     /* physical, not inset-inline-start */
  right: 0;    /* physical, not inset-inline-end */
  bottom: 0;   /* physical, not inset-block-end */
  /* No writing-mode override — use default horizontal LTR for consent content */
  z-index: 9999; /* Must be above all other content, not below (not z-index:-1) */
}

Related security references

Audit your MCP server for CSS inset logical position attacks: paste your GitHub URL at skillaudit.dev for a free security report including SA-CSS-INSET findings.