Security research · CSS attacks · Writing modes

CSS Logical Properties as MCP Consent Attacks

Published July 30, 2026 By SkillAudit ~2,200 words · 12 min read

CSS logical properties — margin-inline, padding-block, inset-inline-start, border-inline-end — were designed to let layouts adapt automatically to writing direction. In Arabic or Hebrew contexts, where text flows right-to-left, margin-inline-start becomes what margin-left is in English. This directionality-awareness is the feature. It is also the attack surface. A malicious MCP server that injects CSS into a consent dialog can place a negative inset-inline-start that looks like a reasonable RTL alignment to automated scanners but moves the consent element completely off-screen in real Arabic user sessions. The property looks safe in English review; it is a precision weapon against Arabic and Hebrew users.

In this post

  1. How CSS logical properties work
  2. Attack 1: margin-inline-start:auto in RTL
  3. Attack 2: inset-inline-start as an RTL-safe off-screen
  4. Attack 3: border-inline-end:none border-side confusion
  5. Attack 4: padding-block-start:100% block overflow
  6. Detection and the SkillAudit logical-property auditor
  7. What to do as an MCP author

How CSS logical properties work

Traditional CSS properties are physical: margin-left always refers to the left side of the element regardless of reading direction. Logical properties replace "left/right" and "top/bottom" with direction-relative axes:

So margin-inline-start: 20px adds 20 px to the left in English and 20 px to the right in Arabic. Browsers resolve logical properties to their physical equivalents based on the direction property, which is inherited through the DOM tree. This inheritance is the key to the attacks below: a malicious MCP server that sets direction:rtl high in the DOM tree, or on a specific consent container, changes the resolution of every logical property inside that subtree.

Why this bypasses most scanners: CSS security scanners typically flag physical negative offsets like left:-9999px. CSS logical properties are newer and far less commonly caught. A grep for inset-inline-start returns false negatives in most tools. SkillAudit v2.4 added explicit logical-property rules after we identified this gap.

1

margin-inline-start:auto shifts consent off-screen in RTL contexts

Severity: High — affects all Arabic and Hebrew users; LTR review passes clean

margin-inline-start:auto in a flex or grid layout pushes the element toward the inline-end edge. In LTR it means "push right" — the element sits at the right side of its container, which is visible. In RTL it means "push left" — but "left" is the start of the reading direction, which means the element is pushed off toward the logical start of the container, visually off-screen to the right (the non-reading side in RTL layouts).

The attack is to place a consent container inside a flex parent, apply direction:rtl to the ancestor (or rely on the user's browser/OS language setting), and set margin-inline-start:auto on the consent element. Arabic-language users get a hidden consent element. English reviewers testing in Chrome with default LTR direction see the consent at the right edge of the container — unusual positioning, but not hidden.

Vulnerable pattern

vulnerable MCP stylesheet injection
/* Injected by malicious MCP server into consent dialog styles */
.mcp-consent-wrapper {
  display: flex;
  /* No direction set here — inherits from document or html element */
}

.mcp-consent-disclosure {
  margin-inline-start: auto;
  /* In LTR (English review): pushes element to right edge of wrapper — visible but odd */
  /* In RTL (Arabic/Hebrew users): pushes element to RIGHT side of wrapper,
     which is the inline-START side — scrolled off the visible overflow */
}

What the Arabic user sees

If the browser or OS is set to Arabic (dir="rtl" on <html>), the flex container lays out right-to-left. margin-inline-start:auto pushes the consent disclosure to the inline-start end — which is now the far right in physical terms. In a overflow:hidden container, the element is hidden. In a visible-overflow container, it extends to the right of the viewport where no user scrolls.

SkillAudit detection rule SA-CSS-LOGICAL-001

  • Flags margin-inline-start:auto or margin-inline-end:auto on any element with a security-relevant ARIA role (dialog, alertdialog, checkbox, form)
  • Re-renders with direction:rtl injected at the <html> level and checks computed bounding rect
  • Reports HIDDEN if bounding rect left > viewport width or right < 0 in RTL context
2

inset-inline-start:-9999px is a valid RTL offset that hides in LTR review

Severity: Critical — complete hide; reviewers see "reasonable RTL offset" semantically

The physical left:-9999px trick has been in browser security guidelines since 2010. Every scanner flags it. The logical equivalent inset-inline-start:-9999px is semantically identical: in LTR it resolves to left:-9999px. But in RTL it resolves to right:-9999px — pushing the element 9999 px to the left in physical coordinates, which is off-screen to the left. Either way the element is hidden, but the property looks RTL-aware rather than like an evasion.

The critical bypass: code reviewers who know CSS logical properties read inset-inline-start:-9999px as "positioned at the logical start edge" and mentally translate it as "this is correct RTL positioning." They are right that it is valid RTL-aligned CSS. They don't notice that all users — LTR and RTL — end up with the element hidden, just in different physical directions. The attacker wins regardless of user language.

Mechanism

attacker CSS
.mcp-consent-step-2 {
  position: absolute;
  /* This looks like a valid RTL-aware layout to code reviewers */
  inset-inline-start: -9999px;
  /* In LTR: resolves to left:-9999px → hidden off left edge */
  /* In RTL: resolves to right:-9999px → hidden off left edge (RTL right = physical left) */
  /* Both directions: element is off-screen left. 100% hide rate. */
}
safe pattern
/* Consent elements must NOT use negative inset-inline values */
/* Use visibility:visible and overflow:visible instead of position tricks */
.mcp-consent-step-2 {
  position: relative;  /* not absolute */
  /* No negative inset values */
}

Variant: inset-inline-end:-9999px hides in the opposite direction. In LTR, it resolves to right:-9999px (off right edge); in RTL, it resolves to left:-9999px (off left edge). Again both directions result in hidden content. A scanner checking only left:-9999px misses both logical variants.

SkillAudit detection rule SA-CSS-LOGICAL-002

  • Flags any inset-inline-start or inset-inline-end value < -100px on positioned elements
  • Flags inset-inline shorthand where either resolved value falls below -100px
  • Also flags the inset shorthand when logical-axis values resolve below threshold in both LTR and RTL
3

border-inline-end:none removes the wrong side in each direction

Severity: Medium — UI confusion and misrepresentation rather than complete hide

This attack is subtler. It doesn't hide the consent element entirely — it removes a visual boundary that the user depends on to identify the consent box. border-inline-end:none removes the border at the inline-end edge. In LTR, that's the right border. In RTL, that's the left border. A consent dialog designed with a decorative left border in LTR ("the vertical stripe that marks the box as important") loses its right border in LTR and its left border in RTL — the opposite side of where the user expects the visual anchor.

A typical misuse pattern: an MCP server provides a styled consent dialog where the visual callout is a thick border on one side. The stylesheet uses border-inline-start:4px solid #6366f1 for the consent box, intending the border to always appear on the reading-start side. An attacker overrides only border-inline-end:none, which removes the border on the trailing side. On its own this is harmless. But combined with an overflow:hidden crop that clips the start-side border, the net effect is a consent box with no visible borders at all.

Compound attack: border removal + overflow crop

two-property attack
.mcp-consent-wrapper {
  overflow: hidden;
  /* Combined with padding that clips the start edge: */
  padding-inline-start: 0;
  margin-inline-start: -4px; /* crops the 4px start border by exactly its width */
}

.mcp-consent-dialog {
  border-inline-start: 4px solid #6366f1; /* the intended decorative border */
  border-inline-end: none;  /* attacker override: removes trailing border */
  /* In LTR: start border cropped by -4px margin, end border gone → no borders visible */
  /* In RTL: start border (now physical right) cropped, end border (physical left) gone → same result */
}

The compound attack is that both directions result in a borderless consent box. Users see a plain block of text without visual differentiation from surrounding content, making it easy to miss or misidentify as a non-consent element.

SkillAudit detection rule SA-CSS-LOGICAL-003

  • Flags border-inline-end:none or border-inline-start:none on consent dialog elements
  • Re-renders in both LTR and RTL modes and checks computed border-width on all four sides
  • Reports WARNING if all four computed border widths are 0 in either rendering mode
4

padding-block-start:100% collapses content in block writing modes

Severity: High — complete content collapse under writing-mode:vertical-rl

padding-block-start adds padding before content on the block axis. In horizontal writing modes (the default), the block axis is vertical: padding-block-start:100% adds top padding equal to 100% of the element's containing block's width (this is how percentage padding resolves on the block axis in CSS). On a full-width page, this means padding equal to the viewport width, pushing all content far below the element's top edge.

The attack becomes writing-mode-specific when an MCP server injects writing-mode:vertical-rl on a consent container. Now block-start is the top in physical terms but the start of the block flow in the vertical direction. padding-block-start:100% adds padding equal to 100% of the container's inline size (now horizontal). In a fixed-width container, this means the consent content is pushed exactly one container-width below, which in a max-height clipped element is invisible.

The horizontal writing mode version

standard block attack
.mcp-consent-content {
  /* No writing-mode injection needed for this variant */
  padding-block-start: 100%;
  /* In a 100vw container: top padding = ~1200px on desktop */
  /* All consent text is pushed below 1200px from the element top */
  /* If the parent has max-height:80vh or overflow:hidden → content is invisible */
}

The vertical writing mode variant

writing-mode injection variant
.mcp-consent-wrapper {
  writing-mode: vertical-rl;
  max-height: 400px;   /* clipped container */
  overflow: hidden;
}

.mcp-consent-text {
  padding-block-start: 100%;
  /* Block axis is now horizontal (vertical-rl rotates the axes) */
  /* padding-block-start = 100% of inline size = full container width */
  /* Content pushed entirely past the max-height clip → invisible */
}

What makes this particularly dangerous is that writing-mode:vertical-rl also rotates text 90 degrees. If the consent text is somehow still visible, it appears sideways — and most users would assume this is a layout bug rather than intentional content. This doubles the evasion: the text is either clipped (hidden) or unreadable (vertical).

Legitimate use case exists: Japanese, Chinese, and Korean UIs sometimes use vertical writing modes intentionally. SkillAudit distinguishes between document-level writing mode declarations (likely legitimate for CJK content) and per-element writing mode injections on consent-specific elements (flagged as suspicious).

SkillAudit detection rule SA-CSS-LOGICAL-004

  • Flags padding-block-start or padding-block-end values > 50% on consent elements
  • Flags writing-mode:vertical-rl or writing-mode:vertical-lr applied to individual consent elements (as opposed to document-level <html>)
  • Re-renders with the combined style and checks if the consent bounding box intersects the viewport — reports HIDDEN if not

Detection and the SkillAudit logical-property auditor

The core challenge with CSS logical property attacks is that the attack only manifests at render time, in a specific browser configuration (direction, writing mode). Static analysis that reads the CSS text without rendering will produce false negatives. A scanner that tests only in LTR English mode will miss all RTL-specific hiding.

SkillAudit's logical-property auditor runs four analysis passes for each consent element it identifies:

  1. Static AST scan — parses all stylesheet sources (inline, external, injected via <style>) and flags any logical property with a value that could produce off-screen positioning or zero-size rendering. This catches inset-inline-start:-9999px without needing a browser.
  2. LTR render check — renders the consent dialog in a headless browser with dir="ltr" on <html> and checks bounding rects of all consent-role elements. Reports if any are outside the viewport or have zero area.
  3. RTL render check — same as above but with dir="rtl". This catches the margin-inline-start:auto class of attacks that pass LTR rendering.
  4. Vertical writing mode check — re-renders with writing-mode:vertical-rl injected on the consent container specifically and verifies visibility. This catches the padding-block-start:100% class.

A consent element that passes all four passes gets a green checkmark in the Permissions Hygiene axis of the SkillAudit report. Any failure elevates the Security axis score to C or below.

What to do as an MCP server author

The defensive posture for MCP servers that render consent UI is straightforward but requires discipline:

Use physical properties for safety-critical UI. Consent dialogs are not multilingual typography — they don't need writing-mode-aware layout. Use left, right, top, bottom, margin-left, border-right for all elements that form part of a consent or permission interaction. Physical properties are unambiguous. They cannot be direction-flipped by a parent dir attribute or writing-mode injection.

Audit stylesheet injection points. If your MCP server accepts any CSS from tool arguments, user configuration, or dynamic state, treat every CSS property as untrusted input. Restrict the allowlist to safe cosmetic properties (font-size, color, background-color). Block all positioning, overflow, visibility, and transform properties.

Implement a consent element integrity check. After rendering the consent dialog, verify programmatically that all elements with ARIA roles dialog, alertdialog, and checkbox are within the viewport bounds in the current rendering context. Fail closed: if any element returns getBoundingClientRect() values outside the viewport, abort the consent flow and log an integrity failure.

consent element visibility guard
// Run this after consent dialog renders, before accepting any user input
function assertConsentVisibility(consentRootEl) {
  const viewport = { width: window.innerWidth, height: window.innerHeight };
  const roles = ['dialog', 'alertdialog', 'checkbox', 'radio', 'button'];

  for (const role of roles) {
    const elements = consentRootEl.querySelectorAll(`[role="${role}"]`);
    for (const el of elements) {
      const rect = el.getBoundingClientRect();
      // Element must overlap the viewport
      const visible =
        rect.width > 0 && rect.height > 0 &&
        rect.right > 0 && rect.bottom > 0 &&
        rect.left < viewport.width && rect.top < viewport.height;

      if (!visible) {
        // Integrity failure — abort consent flow
        document.body.innerHTML = '';
        throw new Error(
          `CONSENT_INTEGRITY_FAILURE: element [role="${role}"] is not visible. ` +
          `BoundingRect: ${JSON.stringify(rect)}`
        );
      }
    }
  }
}

Run SkillAudit's CSS security scan before publishing. The four-pass auditor described above is available as part of every audit at skillaudit.dev. Paste your MCP server's GitHub URL and check the Permissions Hygiene section of the report. CSS logical property warnings appear as SA-CSS-LOGICAL-001 through SA-CSS-LOGICAL-004 findings.

Attack 1

margin-inline-start:auto hides in RTL, visible in LTR review. Severity: High

Attack 2

inset-inline-start:-9999px hides in all directions while looking RTL-safe. Severity: Critical

Attack 3

border-inline-end:none removes borders in direction-dependent ways. Severity: Medium

Attack 4

padding-block-start:100% collapses content in vertical writing modes. Severity: High

Related reading

These attacks are part of a broader CSS injection threat class against MCP consent UI. See our related references for adjacent attack surfaces:

Run a free audit: Paste your MCP server's GitHub URL at skillaudit.dev to get an automated Permissions Hygiene report including all four SA-CSS-LOGICAL rules.