Security Guide

MCP server CSS inset-block-start security — below-fold consent positioning, above-container pull, z-index overlay combination, JS mousedown repositioning

CSS inset-block-start is the logical top offset for positioned elements (position: absolute, fixed, relative, or sticky). In horizontal-tb writing mode it maps to physical top. A large positive inset-block-start pushes the consent dialog below the visible fold — the dialog is present in the DOM, the approve button passes all own-property checks, but no user can see or interact with the dialog without explicitly scrolling down past the current fold. Negative values pull the dialog above its containing block, off the top of the screen. Combined with z-index layering, a positioned element can be placed to cover the consent dialog while the approve button's click area remains exposed — the user clicks what appears to be the overlay but actually fires the approval.

CSS inset-block-start — property overview

The inset-block-start property sets the offset at the block-start edge of a positioned element — equivalent to top in writing-mode: horizontal-tb. It only applies when the element has position other than static. The property participates in the inset shorthand and the inset-block shorthand. A consent dialog with position: absolute inside its containing block will be displaced by inset-block-start relative to that block's top edge. If the containing block is the viewport (via position: fixed), the displacement is relative to the viewport top. Related properties: inset-block shorthand.

Attack 1: large positive inset-block-start — pushing consent below the viewport fold

A consent dialog positioned with position: absolute inside a tall scrollable containing block can be pushed below the visible viewport fold by setting a large inset-block-start. The containing block's own overflow must be auto or scroll (otherwise the dialog is clipped and hidden, which is an even more severe attack). The user scrolls into the dialog's scroll target but the consent dialog — positioned far below the fold — is only reachable by scrolling much further. If the containing block's height is only as tall as the viewport, the dialog overflows below and the user must scroll the page itself.

/* position:absolute consent dialog inside a containing block */
.consent-wrapper {
  position: relative;
  height: 600px;
  overflow: auto;
}

/* Attack: push dialog 700px below the top of the wrapper */
.consent-dialog {
  position: absolute !important;
  inset-block-start: 700px !important; /* beyond wrapper's 600px → below the fold */
}

/* Effect:
   .consent-wrapper scrollHeight: includes the dialog at 700px offset
   But viewport shows only 0–600px of the wrapper at initial scroll.
   The dialog is at 700–900px (depending on dialog height).
   The approve button is at ~860px within the wrapper — user must scroll 260+ px.

   All DOM-presence checks pass: dialog is in the DOM, not display:none, not hidden.
   Only BCR viewport check fails: dialog.getBoundingClientRect().top >= window.innerHeight. */

function checkConsentDialogBelowFold(consentEl) {
  const bcr = consentEl.getBoundingClientRect();
  const vw  = window.innerHeight;
  const ibs = parseFloat(getComputedStyle(consentEl).getPropertyValue('inset-block-start')) || 0;
  return {
    bcrTop: bcr.top,
    bcrBottom: bcr.bottom,
    belowFold: bcr.top >= vw,
    partiallyBelow: bcr.bottom > vw && bcr.top < vw,
    insetBlockStart: ibs,
  };
}

Below-fold consent dialogs pass every DOM and computed-style check. The element is visible, display is not none, opacity is 1, and all text content is present. Only getBoundingClientRect().top >= window.innerHeight catches the attack — the approve button is literally below the visible screen.

Attack 2: negative inset-block-start — pulling the consent dialog above the top of the screen

A negative inset-block-start on a position: absolute or position: fixed consent dialog moves it upward relative to its containing block. If the containing block's top is at the viewport top, a sufficiently negative value pulls the dialog above the viewport — the dialog is off the top of the screen, unreachable without scrolling in the reverse direction. Negative inset-block-start is especially dangerous with position: fixed because the dialog is removed from scroll flow; scrolling cannot bring it back into view.

/* position:fixed consent dialog: negative inset-block-start pulls above viewport */
.consent-dialog {
  position: fixed !important;
  inset-block-start: -400px !important; /* 400px above the top of the viewport */
  left: 50%;
  transform: translateX(-50%);
}

/* Effect:
   dialog.getBoundingClientRect().top = -400 (above viewport)
   dialog.getBoundingClientRect().bottom = -400 + dialogHeight (still negative if dialog < 400px tall)

   With position:fixed, scrolling does NOT bring the dialog into view.
   The dialog exists in the DOM and the approve button is reachable via Tab key or
   direct .click() programmatically, but the user cannot see or point to it.

   Detection: BCR.bottom <= 0 (entire dialog above viewport top edge). */

// Combine above-top and below-fold into one viewport check
function isConsentInViewport(el) {
  const { top, bottom, left, right } = el.getBoundingClientRect();
  const vw = window.innerWidth;
  const vh = window.innerHeight;
  return top < vh && bottom > 0 && left < vw && right > 0;
}

Attack 3: inset-block-start + z-index overlay — covering consent text with a click-through overlay

A positioned element injected into the DOM at a high z-index is placed over the consent dialog using absolute or fixed positioning. The overlay's inset-block-start and inset-inline-start are set to match the consent dialog's position. The overlay covers the consent text visually but is sized to leave the approve button's click target exposed underneath — the overlay uses pointer-events: none on the button region, or the overlay itself is a transparent div sized to match the dialog minus the button area. The user reads the overlay's text (which may say something different from the consent text), then clicks the approve button that is visible below the overlay's bottom edge.

/* Overlay injection: covers consent text, expose approve button below */
const overlay = document.createElement('div');
overlay.style.cssText = `
  position: fixed;
  inset-block-start: 200px;    /* matches consent dialog top */
  inset-inline-start: 50%;
  transform: translateX(-50%);
  width: 480px;
  height: 160px;               /* covers text region but not the 40px button row */
  background: white;
  z-index: 9999;               /* above consent dialog */
  pointer-events: all;
`;
overlay.textContent = 'This action will only read your file preferences.';
document.body.appendChild(overlay);

/* Effect:
   The real consent dialog text (which says "This grants full file-system access") is hidden.
   The overlay shows a softer message.
   The approve button is at inset-block-start + 160px (below the overlay's bottom).
   The user clicks the approve button seeing only the overlay's reassuring text.

   Detection: check for high-z elements overlapping the consent dialog's BCR
   that cover the text region but not the button region. */

function checkConsentOverlap(consentEl) {
  const bcr = consentEl.getBoundingClientRect();
  const overlays = [];

  document.querySelectorAll('*').forEach(el => {
    if (el === consentEl || consentEl.contains(el)) return;
    const cs = getComputedStyle(el);
    const z  = parseInt(cs.zIndex) || 0;
    if (z < 1) return;

    const r = el.getBoundingClientRect();
    const hOverlap = Math.min(bcr.right, r.right) > Math.max(bcr.left, r.left);
    const vOverlap = Math.min(bcr.bottom, r.bottom) > Math.max(bcr.top, r.top);
    if (hOverlap && vOverlap) {
      overlays.push({ el, z, opacity: parseFloat(cs.opacity) });
    }
  });

  return overlays; // non-empty → something is on top of the consent dialog
}

Overlay attacks require cross-element z-index and BCR scanning. The consent dialog itself is unmodified — the attack is an injected sibling at a higher z-index. An audit that checks only the consent element's own properties will find no issue. Scan all elements with z-index >= 1 for BCR overlap with the consent dialog region.

Attack 4: JS mousedown injection — repositioning the consent dialog at click time

At page load the consent dialog is correctly positioned. A mousedown listener on the approve button changes inset-block-start to move the dialog off-screen at the moment of the click. The user's cursor is still pointed at the (now-moved) approve button's original position, which may now contain different content or empty space. The click fires on whatever element occupies that position after the dialog moves. At mouseup, the positioning is restored.

/* Mousedown: shift consent dialog upward during button press */
(function () {
  const APPROVE = '.approve-btn, [data-action="allow"]';

  document.querySelectorAll(APPROVE).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-block-start', '-9999px', 'important');
    }, { passive: true });

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

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

Mousedown position injection requires a MutationObserver. Watch style attribute changes on the consent container. When inset-block-start changes, immediately check the dialog's BCR — if BCR.bottom <= 0 or BCR.top >= window.innerHeight, flag it as a position-based consent evasion attempt.

Detection summary

HIGH Consent dialog getBoundingClientRect().top >= window.innerHeight — dialog is below the visible viewport fold; approve button is unreachable without scrolling.
HIGH Consent dialog BCR.bottom <= 0 — dialog is above the visible viewport top edge; unreachable with position: fixed (scroll cannot bring it into view).
HIGH High-z-index element with non-zero BCR overlap with the consent dialog region — overlay is covering consent text while potentially leaving the approve button exposed.
MEDIUM Large positive inset-block-start detected on a positioned consent element — below-fold positioning risk even if currently above fold (may push below fold on smaller viewports).
MEDIUM Mousedown listener on approve button injects inset-block-start style on consent container — transient repositioning attack not detectable at page load.
/* Complete inset-block-start consent audit */
function auditInsetBlockStart(consentEl) {
  const cs  = getComputedStyle(consentEl);
  const ibs = parseFloat(cs.getPropertyValue('inset-block-start')) || 0;
  const pos = cs.position;
  const bcr = consentEl.getBoundingClientRect();
  const vh  = window.innerHeight;

  const belowFold  = bcr.top  >= vh;
  const aboveTop   = bcr.bottom <= 0;
  const inViewport = bcr.top < vh && bcr.bottom > 0 && bcr.left < window.innerWidth && bcr.right > 0;

  const overlappingHighZ = Array.from(document.querySelectorAll('*')).filter(el => {
    if (el === consentEl || consentEl.contains(el)) return false;
    const z = parseInt(getComputedStyle(el).zIndex) || 0;
    if (z < 1) return false;
    const r = el.getBoundingClientRect();
    return Math.min(bcr.right,r.right) > Math.max(bcr.left,r.left) &&
           Math.min(bcr.bottom,r.bottom) > Math.max(bcr.top,r.top);
  });

  return { insetBlockStart: ibs, position: pos, belowFold, aboveTop, inViewport, overlappingHighZCount: overlappingHighZ.length };
}

SkillAudit checks consent dialog viewport positioning — flagging below-fold and above-top placement, scanning for high-z overlay elements, and monitoring mousedown-injected inset-block-start changes that reposition the dialog at click time. Run a free audit →