Security Guide

MCP server CSS inset-block-end security — above-container pull, below-fold extension, bilateral block collapse, JS mousedown repositioning

CSS inset-block-end is the logical bottom offset for positioned elements. In horizontal-tb writing mode it maps to physical bottom. It only has effect when the element has position other than static. A large positive inset-block-end anchors the element's block-end (bottom) edge to the containing block's block-end edge and pulls it upward — when the offset exceeds the containing block's available block size minus the element's own block size, the element moves above the containing block's block-start edge and exits the viewport. Combined with inset-block-start, bilateral large values collapse the dialog's computed block size to zero, making it physically unclickable despite appearing in the DOM.

CSS inset-block-end — property overview

The inset-block-end property sets the offset at the block-end edge of a positioned element. In writing-mode: horizontal-tb (the default), this maps to physical bottom. In writing-mode: vertical-rl, it maps to physical left; in vertical-lr, to physical right. The property participates in both the inset-block shorthand and the inset shorthand. An audit reading getComputedStyle(el).bottom will correctly reflect inset-block-end after cascade resolution — but an audit reading el.style.bottom (the inline style attribute) will find nothing if the attack was injected via inset-block-end rather than bottom directly. Related properties: inset-block shorthand, inset-block-start.

Attack 1: large positive inset-block-end — pulling consent above the container

For a position: absolute element inside a containing block, a large positive inset-block-end anchors the element's bottom edge against the container's bottom and displaces it upward. When the offset exceeds the container's block size minus the dialog's own block size, the dialog's top edge moves above the containing block's top edge. If the containing block's top edge is near the viewport top, the dialog exits the visible area above the screen. Unlike a large negative top, which reads as a negative number, a large positive bottom (or inset-block-end) reads as a positive number — audits checking for negative offsets will not flag it.

/* Attack: position:absolute consent dialog — large inset-block-end pulls it above container */
.consent-wrapper {
  position: relative;
  height: 400px;    /* containing block */
}

/* Attack injection */
.consent-dialog {
  position: absolute !important;
  inset-block-end: 500px !important; /* 500px > 400px container — dialog pushed 100px above */
  /* BCR.top < 0 relative to container; if container is at viewport top → above viewport */
}

/* Effect:
   If container's top = 60px from viewport top:
   dialog.getBoundingClientRect().top ≈ 60 - 100 = -40px
   dialog.getBoundingClientRect().bottom ≈ -40 + dialogHeight
   If dialogHeight < 40px → BCR.bottom < 0 → entirely above viewport

   All DOM presence checks pass. getComputedStyle(dialog).bottom reads "500px" (positive).
   An audit checking for negative bottom values finds nothing. */

function checkInsetBlockEndPull(consentEl) {
  const cs  = getComputedStyle(consentEl);
  const ibe = parseFloat(cs.getPropertyValue('inset-block-end')) || 0;
  const bcr = consentEl.getBoundingClientRect();
  const vh  = window.innerHeight;
  return {
    aboveTop:   bcr.bottom <= 0,
    belowFold:  bcr.top >= vh,
    inViewport: bcr.top < vh && bcr.bottom > 0,
    insetBlockEnd: ibe,
    suspiciousPositive: ibe > 80, /* large positive → above-container risk */
  };
}

A large positive inset-block-end is not caught by negative-offset audits. The value is positive, but the computed effect moves the dialog above its container. Only a BCR viewport intersection check — BCR.bottom <= 0 — reliably catches the above-viewport case.

Attack 2: negative inset-block-end — extending the consent dialog below the fold

A negative inset-block-end allows the element to extend beyond the block-end of its containing block. For a position: absolute dialog in a containing block whose block-end aligns with the viewport bottom, a sufficiently negative inset-block-end pushes the dialog's bottom edge below the viewport fold. Unlike a large top value (which pushes the entire dialog below the fold), a negative bottom extends the dialog downward while keeping the top edge in place — the dialog straddles the fold, with the approve button at the bottom of the dialog pushed below the visible area.

/* Attack: negative inset-block-end extends dialog below fold */
.consent-dialog {
  position: absolute !important;
  inset-block-end: -300px !important; /* extend 300px below container's block-end */
  /* dialog's top is in view; the approve button at the dialog's bottom is below fold */
}

/* Effect:
   BCR.top is in the visible range (dialog top in viewport).
   BCR.bottom = BCR.top + (dialog height + 300px) — approve button is off-screen bottom.

   An audit checking only BCR.top >= viewport.height finds nothing — top IS in viewport.
   Must also check if the APPROVE BUTTON's BCR is in viewport, not just the dialog. */

function checkApproveButtonInViewport(consentEl) {
  const approveBtn = consentEl.querySelector('[data-action="allow"], .approve-btn, button[type="submit"]');
  if (!approveBtn) return { found: false };
  const bcr = approveBtn.getBoundingClientRect();
  const vh  = window.innerHeight;
  return {
    found:      true,
    inViewport: bcr.top < vh && bcr.bottom > 0,
    belowFold:  bcr.top >= vh,
    aboveTop:   bcr.bottom <= 0,
  };
}

Attack 3: bilateral block collapse — inset-block-start + inset-block-end sum exceeds container height

When both inset-block-start and inset-block-end are set to values that together exceed the containing block's available block size, the browser resolves the element's height as negative (or effectively zero). For a 400px container: inset-block-start: 250px plus inset-block-end: 250px leaves a resolved height of −100px (clamped to 0). The dialog's BCR will have height: 0 — the approve button's pointer target area collapses to zero, making it impossible to click with a pointing device. Unlike height: 0 (which audits look for explicitly), no height property is involved in this attack.

/* Attack: bilateral block-axis collapse */
.consent-dialog {
  position: absolute !important;
  inset-block-start: 250px !important; /* from top of 400px container */
  inset-block-end:   250px !important; /* from bottom of 400px container */
  /* browser resolves height as: 400 - 250 - 250 = -100px → clamped to 0 */
}

/* Effect:
   getBoundingClientRect().height === 0
   All child elements including the approve button have BCR.height === 0
   Pointer events can never land on a zero-height target
   el.style.height is unset — no height property to audit
   Only checking inset-block-start + inset-block-end sum reveals the attack */

function checkBilateralBlockCollapse(consentEl) {
  const cs  = getComputedStyle(consentEl);
  const ibs = parseFloat(cs.getPropertyValue('inset-block-start')) || 0;
  const ibe = parseFloat(cs.getPropertyValue('inset-block-end'))   || 0;
  const bcr = consentEl.getBoundingClientRect();
  const parentH = consentEl.offsetParent?.getBoundingClientRect().height || window.innerHeight;
  return {
    collapsed:   bcr.height < 4,
    blockStart:  ibs,
    blockEnd:    ibe,
    sumExceedsContainer: (ibs + ibe) > parentH * 0.85,
  };
}

Bilateral inset-block collapse leaves no height property to audit. The dialog's height, min-height, and max-height properties are unset or at their defaults. Only checking getBoundingClientRect().height < 4 or summing inset-block-start + inset-block-end against the parent's dimension detects this attack.

Attack 4: JS mousedown injection — repositioning at click time

The consent dialog is correctly positioned at page load. A mousedown event listener on the approve button injects a large inset-block-end value, pulling the dialog above the viewport during the button press. The user's pointer is still positioned at the old dialog location, which may now be occupied by different content. At mouseup, the positioning is restored. Because the attack is transient, a page-load audit finds the dialog correctly positioned and raises no flags.

/* Mousedown: pull consent dialog above viewport via inset-block-end */
(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');
      }
      /* With position:fixed and no top set, inset-block-end:9999px anchors
         bottom to 9999px above the viewport bottom → top edge at -(9999 - vh) → above screen */
      dialog.style.setProperty('inset-block-end', '9999px', 'important');
    }, { passive: true });

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

Mousedown inset-block-end injection requires a MutationObserver on the consent container's style attribute. Watch for changes to inset-block-end or bottom. When either changes during an active pointer event, immediately re-check BCR. A transient above-viewport result during a mousedown is a HIGH-severity finding even if the dialog returns to a valid position at mouseup.

Detection summary

HIGH Consent dialog BCR.bottom <= 0 — dialog is above the visible viewport; pointer cannot reach the approve button. Caused by large positive inset-block-end pulling the dialog upward.
HIGH Approve button BCR.top >= window.innerHeight — button is below the fold even when the dialog's top is in view. Caused by negative inset-block-end extending the dialog downward.
HIGH Consent dialog BCR.height < 4 — bilateral inset-block-start + inset-block-end collapse has reduced the dialog's block dimension to zero; approve button click target has zero area.
MEDIUM Large positive inset-block-end (>80px) on a positioned consent element — above-container pull risk on smaller viewports or when the containing block is repositioned.
MEDIUM Mousedown listener on approve button injects inset-block-end style on the consent container — transient repositioning attack not visible at page-load audit time.
/* Complete inset-block-end consent audit */
function auditInsetBlockEnd(consentEl) {
  const cs  = getComputedStyle(consentEl);
  const ibe = parseFloat(cs.getPropertyValue('inset-block-end')) || 0;
  const ibs = parseFloat(cs.getPropertyValue('inset-block-start')) || 0;
  const bcr = consentEl.getBoundingClientRect();
  const vh  = window.innerHeight;
  const parentH = consentEl.offsetParent?.getBoundingClientRect().height || vh;

  const approveBtn = consentEl.querySelector('[data-action="allow"], .approve-btn, button[type="submit"]');
  const btnBCR     = approveBtn ? approveBtn.getBoundingClientRect() : null;

  return {
    insetBlockEnd:          ibe,
    insetBlockStart:        ibs,
    aboveTop:               bcr.bottom <= 0,
    belowFold:              bcr.top >= vh,
    collapsed:              bcr.height < 4,
    bilateralCollapse:      (ibs + ibe) > parentH * 0.85,
    approveButtonBelowFold: btnBCR ? btnBCR.top >= vh : null,
  };
}

SkillAudit checks inset-block-end as part of a full inset-family audit — reading each sub-property via getPropertyValue, testing bilateral block-axis collapse by summing start and end offsets against the parent height, and checking the approve button's own BCR independently of the dialog container. Run a free audit →