Security Guide

MCP server CSS margin-inline-end security — right-side inline collapse, bilateral squeeze with margin-inline-start, dir=rtl side swap, JS mousedown injection

CSS margin-inline-end is the logical right margin in ltr and the logical left margin in rtl. A large positive margin-inline-end on a consent container compresses the space between the dialog's right edge and its containing block, effectively pushing the dialog leftward and clipping its right side — where the approve button often lives. Combined with margin-inline-start it enables bilateral inline collapse, squeezing the text column to near-zero from both sides simultaneously. The dir=rtl attribute swaps which physical side is affected, and JS mousedown injection makes the attack transient and invisible to static analysis.

CSS margin-inline-end — property overview

The margin-inline-end property sets the margin at the inline-end edge of an element — the right side in left-to-right (ltr) writing and the left side in right-to-left (rtl) writing. In a block-formatting context with a fixed-width container, a large margin-inline-end does not shrink the element — the element remains at its declared width — but it compresses the space available for the element by reducing the gap between the element's end edge and the container boundary. If the declared width plus both inline margins exceeds the container width, the element is either clipped (with overflow: hidden) or overflows (with overflow: visible). In a flex or grid container, margin-inline-end eats into the flex track, which can shrink the element if it is not fixed-width. Related properties: margin-inline shorthand, margin-inline-start.

Attack 1: large positive margin-inline-end — right-side clip, approve button off-screen

In a flex row where the consent container is flex: 1, a large margin-inline-end reduces the flex track available to the container. The container shrinks and its approve button — typically positioned at the inline-end side of the dialog — either overflows into hidden space or is pushed to the right past the viewport edge. The consent text column also compresses. The user sees a narrowed dialog with the approval button invisible or unreachable.

/* Flex-row consent layout: sidebar + consent dialog */
.consent-row {
  display: flex;
  width: 600px;
  overflow: hidden;
}

/* Normally: dialog takes remaining space */
.consent-dialog { flex: 1; }

/* Attack: large margin-inline-end eats into the flex track */
.consent-dialog {
  margin-inline-end: 500px !important;
}

/* Effect:
   Available track: 600px
   Consumed by margin-inline-end: 500px
   Remaining for dialog content: 100px (heavily clipped)
   Approve button is at inline-end side → pushed into the clipped 500px margin zone.
   The button may still receive clicks via keyboard / Enter key.

   Audit checking dialog clientWidth: may show 100px (correct for shrunk flex item)
   — but this is an unusual width that should trigger a minimum-width alert.
   Threshold: consent dialog width < 200px is a consent obstruction finding. */

function checkConsentDialogMinWidth(consentEl) {
  const w = consentEl.getBoundingClientRect().width;
  const mie = parseFloat(getComputedStyle(consentEl).getPropertyValue('margin-inline-end')) || 0;
  return {
    width: w,
    marginInlineEnd: mie,
    tooNarrow: w < 200,
    largeEndMargin: mie > 100,
  };
}

In a flex container, margin-inline-end shrinks the element, not just the space after it. Unlike in block formatting context where margin does not affect the element's own width, in flex layout the margin is subtracted from the track — the element itself becomes narrower. A width check alone catches this only if a minimum-width threshold is applied.

Attack 2: bilateral inline collapse — combining margin-inline-start and margin-inline-end

Injecting both margin-inline-start and margin-inline-end on a flex-item consent dialog creates bilateral compression: the dialog is squeezed from both sides simultaneously. The left and right margins each individually appear below common thresholds (e.g., 100px each), but their sum (200px) compresses a 400px dialog to 200px — below the minimum readable width. A scanner that alerts only on individual margins exceeding a threshold misses the combined attack.

/* Bilateral attack: each margin individually looks small, combined effect is large */
.consent-dialog {
  margin-inline-start: 120px !important; /* individually: below a 200px alert threshold */
  margin-inline-end:   120px !important; /* individually: below a 200px alert threshold */
}

/* In a flex container with 400px track:
   Available after margins: 400 - 120 - 120 = 160px
   Dialog is compressed to 160px — consent text wraps or truncates severely.

   Scanner logic that only checks each margin individually:
     120px margin-inline-start → PASS (< 200px threshold)
     120px margin-inline-end   → PASS (< 200px threshold)
   Scanner misses the bilateral collapse.

   Correct detection: sum both inline margins, compare against container width. */

function checkBilateralInlineMargins(consentEl) {
  const cs  = getComputedStyle(consentEl);
  const mis = parseFloat(cs.getPropertyValue('margin-inline-start')) || 0;
  const mie = parseFloat(cs.getPropertyValue('margin-inline-end'))   || 0;

  const parentW = consentEl.parentElement?.clientWidth ?? window.innerWidth;
  const effectiveWidth = parentW - mis - mie;

  return {
    marginInlineStart: mis,
    marginInlineEnd: mie,
    totalInlineMargin: mis + mie,
    effectiveWidth,
    bilateralCollapse: effectiveWidth < 200, // less than 200px is consent-obstruction risk
  };
}

Attack 3: dir="rtl" swap — margin-inline-end becomes the left physical margin

In an RTL context, margin-inline-end controls the physical left margin. A security scanner that reads marginRight (physical) to detect end-margin attacks will find zero in RTL — the attack is on marginLeft (physical), which is what the logical margin-inline-end resolves to. The attack can push the consent dialog leftward (from the right edge in RTL layout), effectively the same collapse but in the opposite physical direction. RTL consent dialogs are common in Arabic and Hebrew language settings.

/* RTL: margin-inline-end controls physical left margin */
.consent-dialog {
  direction: rtl !important;
  margin-inline-end: 350px !important; /* → physical margin-left: 350px in RTL */
}

/* In RTL flex layout from the right:
   The dialog is pushed 350px from the start of the flex track (right side in RTL).
   Physical effect: dialog shifted leftward by 350px.

   Scanner reading: getComputedStyle(el).marginRight → "0px" — no alert.
   Scanner must read: getComputedStyle(el).getPropertyValue('margin-inline-end') → "350px" */

// Always use logical property names
function getInlineMargins(el) {
  const cs = getComputedStyle(el);
  return {
    start: parseFloat(cs.getPropertyValue('margin-inline-start')) || 0,
    end:   parseFloat(cs.getPropertyValue('margin-inline-end'))   || 0,
    dir:   cs.direction,
  };
}

Physical vs logical property names diverge under dir=rtl. Read margin-inline-end via getPropertyValue('margin-inline-end') — never rely on marginRight or marginLeft as proxies for inline-start or inline-end. The logical properties are stable across direction changes; the physical properties are not.

Attack 4: JS mousedown injection — bilateral inline collapse at click time

At page load the consent dialog is normal. A mousedown listener on the approve button injects both margin-inline-start and margin-inline-end simultaneously, creating a bilateral squeeze for the duration of the press. The consent text is compressed from both sides at the moment the user clicks; the approve button itself may still be within the now-narrow dialog and receive the click. At mouseup both margins are removed and the layout restores.

/* Mousedown: bilateral inline collapse at click time */
(function () {
  const DIALOG  = '.consent-dialog';
  const APPROVE = '.approve-btn, [data-action="allow"]';

  function bilateralCollapse() {
    document.querySelectorAll(DIALOG).forEach(el => {
      const parentW = el.parentElement?.clientWidth ?? window.innerWidth;
      const squeeze = Math.floor((parentW - 60) / 2); // leave only 60px visible
      el.style.setProperty('margin-inline-start', `${squeeze}px`, 'important');
      el.style.setProperty('margin-inline-end',   `${squeeze}px`, 'important');
    });
  }

  function restore() {
    document.querySelectorAll(DIALOG).forEach(el => {
      el.style.removeProperty('margin-inline-start');
      el.style.removeProperty('margin-inline-end');
    });
  }

  document.querySelectorAll(APPROVE).forEach(btn => {
    btn.addEventListener('mousedown', bilateralCollapse, { passive: true });
    btn.addEventListener('mouseup',   restore,           { passive: true });
    btn.addEventListener('mouseleave',restore,           { passive: true });
  });
})();

Bilateral mousedown attacks require checking both inline margins simultaneously. A MutationObserver on the consent element's style attribute detects the simultaneous injection. On detection, check that effectiveWidth = parentWidth - mis - mie >= 200px to confirm no bilateral collapse is in progress.

Detection summary

HIGH Sum of computed margin-inline-start + margin-inline-end causes effective consent dialog width below 200px — bilateral inline collapse that hides consent text while the approve button may remain reachable.
HIGH Computed margin-inline-end pushes consent dialog right edge (BCR.right) beyond the container width — approve button is clipped or off-screen in a fixed-width container.
MEDIUM dir="rtl" or direction: rtl detected on consent container — margin-inline-end controls physical left margin; physical-side audits read wrong axis.
MEDIUM Mousedown listener on approve button injects margin-inline-end (or bilateral inline margins) on consent container — transient inline collapse at click time, not visible at page load.
/* Complete inline-end margin audit */
function auditMarginInlineEnd(consentEl) {
  const cs      = getComputedStyle(consentEl);
  const mis     = parseFloat(cs.getPropertyValue('margin-inline-start')) || 0;
  const mie     = parseFloat(cs.getPropertyValue('margin-inline-end'))   || 0;
  const dir     = cs.direction;
  const bcr     = consentEl.getBoundingClientRect();
  const parentW = consentEl.parentElement?.clientWidth ?? window.innerWidth;

  const effectiveW    = parentW - mis - mie;
  const rightEdgeClip = bcr.right > window.innerWidth;

  return {
    marginInlineStart: mis,
    marginInlineEnd:   mie,
    totalInlineMargin: mis + mie,
    effectiveWidth:    effectiveW,
    bilateralCollapse: effectiveW < 200,
    rightEdgeClipped:  rightEdgeClip,
    direction:         dir,
  };
}

SkillAudit checks both margin-inline-end and margin-inline-start together — computing the effective dialog width after bilateral inline margins, checking BCR edge clipping, and monitoring mousedown-injected style mutations that create transient inline collapse. Run a free audit →