Security Guide

MCP server CSS inset-inline-start security — off-viewport horizontal push, left-side warning overlap, dir=rtl physical-side swap, JS mousedown repositioning

CSS inset-inline-start is the logical left offset for positioned elements in LTR writing and the logical right offset in RTL. A large positive value pushes a position: absolute or fixed consent dialog rightward, off the right edge of the viewport — the dialog is DOM-present, passes all own-property checks, but the user cannot see or interact with it because it is horizontally off-screen. Negative values shift the dialog leftward, potentially covering navigation bars, risk-disclosure sidebars, or other elements to the left of the dialog's normal position. The dir=rtl axis swap makes physical-side checks (left property) useless, and JS mousedown injection makes the attack transient.

CSS inset-inline-start — property overview

The inset-inline-start property sets the inline-start offset for positioned elements. It is the logical equivalent of left in LTR and right in RTL. Like all inset properties, it only applies when position is absolute, fixed, relative, or sticky. In position: absolute or fixed, a value of 200px places the element's inline-start edge 200px from the inline-start edge of its containing block (the viewport for fixed). Inset properties displace the element without changing its declared width or height. Related properties: inset-block-start, inset-block shorthand.

Attack 1: extreme positive inset-inline-start — consent dialog pushed off the right viewport edge

A consent dialog with position: fixed and a large inset-inline-start is displaced rightward beyond the viewport boundary. The dialog's BCR.left exceeds window.innerWidth — the entire dialog is off-screen to the right. The approve button exists in the DOM and is technically reachable by keyboard (Tab then Enter), but the user cannot see or click it with a pointer device. The MCP tool can auto-click via .click() or dispatch a synthetic click event; the user is unaware the dialog even appeared.

/* position:fixed consent dialog pushed off right edge */
.consent-dialog {
  position: fixed !important;
  inset-inline-start: 2000px !important; /* far right of any reasonable viewport */
  top: 50%;
  transform: translateY(-50%);
}

/* Effect:
   dialog.getBoundingClientRect().left = 2000 (way beyond viewport right edge)
   dialog.getBoundingClientRect().right = 2000 + dialogWidth (also off-screen)

   getComputedStyle checks: position=fixed, visibility=visible, opacity=1, display=block.
   All pass — no DOM-presence issue found.
   Only BCR off-viewport check detects the attack:
     BCR.left >= window.innerWidth → FAIL (dialog entirely off right edge). */

function checkHorizontallyOffScreen(consentEl) {
  const bcr = consentEl.getBoundingClientRect();
  const vw  = window.innerWidth;
  return {
    bcrLeft:   bcr.left,
    bcrRight:  bcr.right,
    offRight:  bcr.left  >= vw,    // entire dialog to the right of viewport
    offLeft:   bcr.right <= 0,     // entire dialog to the left of viewport
    inViewport: bcr.left < vw && bcr.right > 0,
  };
}

A position: fixed dialog pushed off-screen is invisible and unreachable by pointer. It is still reachable via the Tab key and the .click() DOM API. A malicious MCP tool can use a synthetic click() call on the approve button to fire the approval without the user ever seeing the dialog. The only reliable detection is a viewport BCR check: if BCR.right <= 0 or BCR.left >= window.innerWidth, the dialog is off-screen.

Attack 2: negative inset-inline-start — overlapping left-side navigation or risk disclosures

A negative inset-inline-start shifts the consent dialog leftward from its normal position, potentially overlapping a left-side navigation bar, a security disclosure panel, or an advisory element rendered to the left of the consent dialog. The consent dialog visually covers the disclosure. The disclosure's own properties are unchanged. This attack is the horizontal analogue of the block-axis negative inset-block-start attack. In a standard two-panel layout with a risk disclosure on the left and a consent widget on the right, a negative inset-inline-start on the right panel shifts it left into the disclosure area.

/* Layout: left=disclosure panel, right=consent widget */
.page-layout {
  display: grid;
  grid-template-columns: 300px 1fr;
}

.consent-widget {
  position: relative; /* needed for inset to apply */
  grid-column: 2;
}

/* Attack: shift consent widget leftward 280px — covers most of disclosure panel */
.consent-widget {
  inset-inline-start: -280px !important;
  z-index: 10 !important;
}

/* Effect:
   .disclosure-panel BCR: { left: 0, right: 300 } — unchanged
   .consent-widget BCR:   { left: 300-280=20, right: 300-280+widgetWidth }
   The consent widget covers the disclosure panel from left:20 to left:300.

   Disclosure panel own checks: PASS. Consent widget own checks: PASS.
   Only a cross-element BCR overlap check catches the coverage. */

function checkInlineStartOverlap(consentEl) {
  const cs   = getComputedStyle(consentEl);
  const iis  = parseFloat(cs.getPropertyValue('inset-inline-start')) || 0;
  if (iis >= 0) return { attack: false };

  const cBCR = consentEl.getBoundingClientRect();
  const overlapping = [];

  // Check all siblings and layout peers
  const parent = consentEl.parentElement;
  for (const child of (parent?.children ?? [])) {
    if (child === consentEl) continue;
    const r = child.getBoundingClientRect();
    if (Math.min(cBCR.right, r.right) > Math.max(cBCR.left, r.left) &&
        Math.min(cBCR.bottom, r.bottom) > Math.max(cBCR.top, r.top)) {
      overlapping.push(child);
    }
  }

  return { insetInlineStart: iis, attack: iis < -20, overlapping };
}

Attack 3: dir="rtl"inset-inline-start maps to physical right

In an RTL context, inset-inline-start controls the right physical offset, not the left. A security scanner that reads getComputedStyle(el).left to detect start-edge positioning attacks reads zero — the displacement is on the right physical property. A large positive inset-inline-start in RTL pushes the dialog leftward (toward the RTL start edge, which is the physical right side in RTL). The physical direction of the off-screen displacement is reversed, but the attack result — dialog not in the visible viewport — is the same.

/* RTL: inset-inline-start controls physical right */
.consent-dialog {
  position: fixed !important;
  direction: rtl !important;
  inset-inline-start: 2000px !important; /* → physical right: 2000px in RTL */
}

/* In RTL layout:
   physical right = 2000px pushes dialog 2000px from the right edge of the viewport.
   This places the dialog far to the left of the viewport (off-screen on the left side in RTL).

   Scanner reading: getComputedStyle(el).left → "auto" or initial value — no alert.
   Scanner must read: getComputedStyle(el).getPropertyValue('inset-inline-start') → "2000px". */

// Correct: always use logical property name
function getInlineStartOffset(el) {
  const cs  = getComputedStyle(el);
  const iis = cs.getPropertyValue('inset-inline-start');
  const dir = cs.direction;
  return { insetInlineStart: iis, direction: dir };
}

Read inset-inline-start via getPropertyValue('inset-inline-start'), not style.left or getComputedStyle(el).left. In RTL, the physical left and right properties swap meaning relative to inline-start. The logical property name is stable regardless of direction context.

Attack 4: JS mousedown injection — transient horizontal repositioning at click time

At page load, the consent dialog is correctly centered in the viewport. A mousedown listener on the approve button injects a large inset-inline-start (and ensures position: fixed is set), moving the dialog off-screen horizontally for the duration of the button press. The user's pointer was pointed at the approve button before the dialog moved; the click fires on whatever element now occupies that viewport coordinate. At mouseup the positioning is restored.

/* Mousedown: shift consent dialog off-screen horizontally */
(function () {
  const DIALOG  = '.consent-dialog';
  const APPROVE = '.approve-btn, [data-action="allow"]';

  function shiftOffScreen() {
    document.querySelectorAll(DIALOG).forEach(el => {
      el.style.setProperty('position', 'fixed', 'important');
      el.style.setProperty('inset-inline-start', `${window.innerWidth + 200}px`, 'important');
    });
  }

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

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

Mousedown inset-inline-start injection requires a MutationObserver. Watch style attribute mutations on the consent container. When inset-inline-start is set dynamically, immediately check BCR — if BCR.left >= window.innerWidth or BCR.right <= 0, the dialog has been horizontally evicted from the viewport.

Detection summary

HIGH Consent dialog BCR.left >= window.innerWidth — dialog is entirely off the right edge of the viewport; not visible or pointer-reachable.
HIGH Consent dialog BCR.right <= 0 — dialog is entirely off the left edge of the viewport.
HIGH Negative inset-inline-start on positioned consent container — dialog overlaps elements to its left (navigation, disclosure panel); cross-element BCR overlap confirms coverage.
MEDIUM dir="rtl" detected on consent container or parent — inset-inline-start maps to physical right; physical-left-based position checks audit the wrong property.
MEDIUM Mousedown listener on approve button injects inset-inline-start — transient horizontal off-screen repositioning not detectable at page load.
/* Complete inset-inline-start audit */
function auditInsetInlineStart(consentEl) {
  const cs  = getComputedStyle(consentEl);
  const iis = parseFloat(cs.getPropertyValue('inset-inline-start')) || 0;
  const dir = cs.direction;
  const pos = cs.position;
  const bcr = consentEl.getBoundingClientRect();
  const vw  = window.innerWidth;

  const offRight   = bcr.left >= vw;
  const offLeft    = bcr.right <= 0;
  const inViewport = bcr.left < vw && bcr.right > 0;

  let siblingOverlap = false;
  if (iis < -20) {
    const parent = consentEl.parentElement;
    for (const child of (parent?.children ?? [])) {
      if (child === consentEl) continue;
      const r = child.getBoundingClientRect();
      if (Math.min(bcr.right,r.right) > Math.max(bcr.left,r.left) &&
          Math.min(bcr.bottom,r.bottom) > Math.max(bcr.top,r.top)) {
        siblingOverlap = true; break;
      }
    }
  }

  return { insetInlineStart: iis, direction: dir, position: pos, offRight, offLeft, inViewport, siblingOverlap };
}

SkillAudit checks horizontal off-screen positioning for consent dialogs — BCR viewport edge checks in both inline directions, cross-element overlap detection for negative inset-inline-start, RTL-aware logical property reading, and mousedown-injected style mutation monitoring. Run a free audit →