Security Guide

MCP server CSS inset-inline-end security — off-left-edge push (LTR), RTL physical-axis swap, bilateral inline collapse, JS mousedown repositioning

CSS inset-inline-end sets the offset at the inline-end edge of a positioned element. In LTR horizontal-tb writing mode this maps to physical right. In RTL it maps to physical left. A large positive inset-inline-end in LTR anchors the element's right edge far to the right of its containing block, pulling the element's left edge off the left of the viewport. In RTL, the same attack moves the dialog off the right edge — and an audit that reads style.right instead of the logical property misses the attack entirely because the logical property maps to physical left in RTL. Combined with inset-inline-start, bilateral large values collapse the dialog's inline dimension to zero.

CSS inset-inline-end — property overview

The inset-inline-end property sets the offset at the inline-end edge of a positioned element — equivalent to right in LTR horizontal-tb mode, left in RTL, bottom in vertical-rl, and bottom in vertical-lr. Like all inset properties, it only has effect when position is not static. It participates in the inset shorthand (second value in two-value form). An audit reading getComputedStyle(el).right will reflect the resolved value in LTR, but in RTL the same property maps to the physical left — reading getComputedStyle(el).right in RTL mode gives auto when only inset-inline-end is set. Related: inset-inline-start, CSS inset family synthesis.

Attack 1: large positive inset-inline-end (LTR) — pulling consent off the left edge

In LTR mode, inset-inline-end maps to physical right. A large positive value anchors the dialog's right edge far to the right of the containing block — pulling the dialog's left edge off the left of the viewport. If the value exceeds the containing block width plus the dialog's own width, BCR.right <= 0 — the dialog is entirely off the left edge. This is counterintuitive: setting a large right (or inset-inline-end) value moves the element to the left. Audits looking for large left values or negative right values will miss this attack.

/* Attack: LTR — large inset-inline-end pulls dialog off left edge */
.consent-dialog {
  position: fixed !important;
  inset-inline-end: 200vw !important; /* anchors right edge at 200vw right → dialog left = -100vw */
  /* BCR.right = viewport_width - 200vw = -100vw → entirely off left */
  /* BCR.left  ≈ -100vw - dialogWidth → well off screen */
}

/* The value "200vw" is POSITIVE and LARGE — audits looking for negatives miss it.
   getComputedStyle(dialog).right reads "200vw" (positive) in LTR.
   getComputedStyle(dialog).left reads "auto" — left-edge audit finds nothing. */

function checkInsetInlineEndLTR(consentEl) {
  const cs  = getComputedStyle(consentEl);
  const iie = parseFloat(cs.getPropertyValue('inset-inline-end')) || 0;
  const dir = getComputedStyle(consentEl.closest('[dir]') || document.documentElement).direction;
  const bcr = consentEl.getBoundingClientRect();
  const vw  = window.innerWidth;
  return {
    direction:  dir,
    offLeft:    bcr.right  <= 0,   /* in LTR: large inset-inline-end pushes off left */
    offRight:   bcr.left   >= vw,  /* in RTL: large inset-inline-end pushes off right */
    rawOffset:  iie,
  };
}

Large positive inset-inline-end in LTR pushes the dialog off the left edge. The value is positive and the computed right property is also positive — nothing in the numbers looks like an off-screen attack. Only BCR.right <= 0 reliably detects the dialog is off-screen to the left.

Attack 2: RTL axis swap — inset-inline-end maps to physical left

In RTL mode (dir="rtl" on an ancestor element), the inline-end direction is the physical left. A large positive inset-inline-end in RTL anchors the dialog's physical left edge far to the left, which in practice pushes the dialog off the right edge of the viewport. An audit reading getComputedStyle(dialog).right in an RTL context returns auto — because inset-inline-end resolves to physical left, not right, in RTL. The audit sees no large right value. Only reading the logical property getPropertyValue('inset-inline-end') or checking BCR catches the attack.

/* Attack: RTL ancestor — inset-inline-end maps to physical left */
/* Ancestor has dir="rtl" */
.consent-dialog {
  position: fixed !important;
  inset-inline-end: 150vw !important; /* in RTL: maps to physical left → left:150vw */
  /* dialog's physical left is at 150vw → off right edge of viewport */
  /* BCR.left >= window.innerWidth */
}

/* Audit reading getComputedStyle(dialog).right finds "auto" — the right property is not set.
   getComputedStyle(dialog).left reads "150vw" — revealing the true offset.
   But if the audit only checks .right (because it's LTR-assumption baked in), it misses everything.

   Correct detection: always read the logical property regardless of dir/writing-mode */
function checkInsetInlineEndRTL(consentEl) {
  const cs  = getComputedStyle(consentEl);
  const iie = parseFloat(cs.getPropertyValue('inset-inline-end')) || 0;
  const bcr = consentEl.getBoundingClientRect();
  return {
    insetInlineEnd: iie,
    offScreen: bcr.right <= 0 || bcr.left >= window.innerWidth,
    /* BCR is axis-agnostic — catches both LTR and RTL off-screen cases */
  };
}

Attack 3: bilateral inline collapse — inset-inline-start + inset-inline-end exceeds container width

When both inset-inline-start and inset-inline-end are set to values that sum beyond the containing block's inline size, the browser resolves a zero (or negative) width for the element. For a 500px-wide container: inset-inline-start: 300px plus inset-inline-end: 300px leaves resolved width of −100px (clamped to 0). The dialog's BCR width approaches zero; the approve button has zero inline dimension, making pointer interaction impossible. No width property is set in the attack; width audits find nothing.

/* Attack: bilateral inline-axis collapse */
.consent-dialog {
  position: absolute !important;
  inset-inline-start: 300px !important;
  inset-inline-end:   300px !important;
  /* container width 500px → resolved width = 500 - 300 - 300 = -100 → clamped to 0 */
}

/* BCR.width === 0 → approve button has zero width → pointer cannot land on it
   el.style.width is unset → width audit finds nothing */

function checkBilateralInlineCollapse(consentEl) {
  const cs  = getComputedStyle(consentEl);
  const iis = parseFloat(cs.getPropertyValue('inset-inline-start')) || 0;
  const iie = parseFloat(cs.getPropertyValue('inset-inline-end'))   || 0;
  const bcr = consentEl.getBoundingClientRect();
  const parentW = consentEl.offsetParent?.getBoundingClientRect().width || window.innerWidth;
  return {
    collapsed:          bcr.width < 4,
    inlineStart:        iis,
    inlineEnd:          iie,
    sumExceedsParent:   (iis + iie) > parentW * 0.85,
  };
}

Bilateral inline collapse leaves no width property to audit. The dialog's width, min-width, and max-width are unset. Only getBoundingClientRect().width < 4 or summing inset-inline-start + inset-inline-end against the parent width reveals the attack.

Attack 4: JS mousedown injection — repositioning at click time

At page load the consent dialog is visible and correctly positioned. A mousedown listener on the approve button injects a large inset-inline-end, pushing the dialog off the left edge during the button press. The user's cursor was aimed at the approve button's original location, which now contains whatever element was behind the dialog. The click event fires on that underlying element instead. At mouseup, positioning is restored so a subsequent BCR check finds the dialog back in a valid position.

/* Mousedown: push dialog off left edge via inset-inline-end */
(function () {
  document.querySelectorAll('.approve-btn, [data-action="allow"]').forEach(btn => {
    const dialog = btn.closest('.consent-dialog');
    if (!dialog) return;

    btn.addEventListener('mousedown', () => {
      if (!['absolute','fixed','relative','sticky'].includes(
            getComputedStyle(dialog).position)) {
        dialog.style.setProperty('position', 'fixed', 'important');
      }
      dialog.style.setProperty('inset-inline-end', '200vw', 'important');
    }, { passive: true });

    btn.addEventListener('mouseup',    () => { dialog.style.removeProperty('inset-inline-end'); dialog.style.removeProperty('position'); }, { passive: true });
    btn.addEventListener('mouseleave', () => { dialog.style.removeProperty('inset-inline-end'); dialog.style.removeProperty('position'); }, { passive: true });
  });
})();

Use a MutationObserver on the consent container's style attribute to catch mousedown injection. When inset-inline-end or right (or left in RTL) changes during an active pointer event, immediately re-run a BCR check. A transient off-screen value during a mousedown is HIGH severity even if the dialog returns to a valid position on mouseup.

Detection summary

HIGH Consent dialog BCR.right <= 0 — dialog is off the left edge of the viewport (large positive inset-inline-end in LTR). Pointer cannot reach the approve button.
HIGH Consent dialog BCR.left >= window.innerWidth — dialog is off the right edge (large inset-inline-end in RTL; inset-inline-end maps to physical left in RTL).
HIGH Consent dialog BCR.width < 4 — bilateral inset-inline-start + inset-inline-end collapse; approve button inline dimension is zero and unreachable by pointer.
MEDIUM Large positive inset-inline-end (>200px) on a positioned consent element — off-left risk in LTR or off-right risk in RTL depending on the direction context.
MEDIUM Mousedown listener on approve button injects inset-inline-end style on the consent container — transient repositioning not detectable at page-load audit time.
/* Complete inset-inline-end consent audit */
function auditInsetInlineEnd(consentEl) {
  const cs  = getComputedStyle(consentEl);
  const iie = parseFloat(cs.getPropertyValue('inset-inline-end'))   || 0;
  const iis = parseFloat(cs.getPropertyValue('inset-inline-start')) || 0;
  const bcr = consentEl.getBoundingClientRect();
  const vw  = window.innerWidth;
  const parentW = consentEl.offsetParent?.getBoundingClientRect().width || vw;

  return {
    insetInlineEnd:    iie,
    insetInlineStart:  iis,
    offLeft:           bcr.right  <= 0,
    offRight:          bcr.left   >= vw,
    collapsed:         bcr.width  < 4,
    bilateralCollapse: (iis + iie) > parentW * 0.85,
  };
}

SkillAudit reads inset-inline-end via getPropertyValue rather than physical right or left, ensuring RTL and vertical writing-mode attacks are detected regardless of which physical edge the property resolves to. BCR-based viewport intersection catches all off-screen cases axis-agnostically. Run a free audit →