Security Guide

MCP server CSS padding-block-end security — below-fold content push (border-box), bilateral block-axis collapse, writing-mode axis remap, JS mousedown injection

CSS padding-block-end is the individual property for the logical bottom padding of an element (distinct from the padding-block shorthand). In horizontal-tb writing mode it maps to physical padding-bottom. A large padding-block-end on a consent dialog with a fixed block-size and box-sizing: border-box crushes the content area from below. The approve button, typically at the bottom of a consent dialog, is the first element pushed out of the content area — it enters the bottom padding zone and is clipped by overflow: hidden, or extends below the dialog's visible boundary. The dialog's computed height remains unchanged; only checking the content area's available height or the approve button's own BCR reveals the attack.

CSS padding-block-end — property overview

The padding-block-end property sets padding at the block-end edge. In horizontal-tb it maps to physical padding-bottom; in vertical-rl, to padding-left; in vertical-lr, to padding-right. It is a sub-property of padding-block (shorthand) and padding (shorthand). Setting padding-block-end directly allows the attack to leave padding-block-start at its default — making the attack distinct from bilateral padding attacks. Related: padding-block shorthand, padding-block-start.

Attack 1: large padding-block-end + border-box — crushing content from below

For a consent dialog with a fixed block size and box-sizing: border-box, the content area height is equal to the block size minus all vertical padding and borders. A large padding-block-end consumes that space from the bottom. The approve button — placed at the end of the content flow — is the last item in the content area and is the first to be squeezed out. At sufficient padding values, the button enters the bottom padding zone and is clipped if overflow: hidden is set. The dialog's height property reports the full fixed block size. Only the content area height calculation and the button's BCR reveal the attack.

/* Attack: border-box consent dialog — large padding-block-end crushes from below */
.consent-dialog {
  block-size: 320px !important;
  box-sizing: border-box !important;
  overflow: hidden !important;
  padding-block-end: 290px !important; /* 290px of bottom padding → 30px content area */
}

/* Effect:
   Content area = 320 - 290 = 30px (only consent title visible; button and body text cut off)
   dialog.offsetHeight = 320px → height audit passes
   getComputedStyle(dialog).paddingBottom = "290px" → padding audit catches it

   If overflow is NOT hidden:
   Button is at content_start + content_height (30px) → at 30px from top of dialog
   But dialog content flows top-to-bottom, so the button is at ~30px from dialog top
   If the button's natural position was at 260px, the content area collapse pushes it
   into the last 30px slot — it may overlap with other content or be invisible.

   Most reliable check: approve button's own BCR */

function checkPaddingBlockEndCrush(consentEl) {
  const cs  = getComputedStyle(consentEl);
  const pbe = parseFloat(cs.getPropertyValue('padding-block-end')) || 0;
  const pbs = parseFloat(cs.getPropertyValue('padding-block-start')) || 0;
  const h   = consentEl.offsetHeight || 0;
  const contentArea = h - pbs - pbe -
    (parseFloat(cs.borderTopWidth) || 0) -
    (parseFloat(cs.borderBottomWidth) || 0);
  const approveBtn = consentEl.querySelector('[data-action="allow"], .approve-btn, button[type="submit"]');
  const btnBCR = approveBtn ? approveBtn.getBoundingClientRect() : null;
  return {
    paddingBlockEnd:    pbe,
    contentAreaHeight:  contentArea,
    contentAreaCrushed: contentArea < 40,
    buttonVisible:      btnBCR
      ? (btnBCR.top < window.innerHeight && btnBCR.bottom > 0)
      : null,
  };
}

Padding-block-end content crush is harder to detect than padding-block-start crush because the approve button — which is typically at the bottom of the dialog — is the first element squeezed out, not the last. A partial content-area crush that leaves the consent text visible but removes the approve button is a viable attack that may not trigger obvious visual anomalies in the dialog's overall appearance.

Attack 2: bilateral block padding collapse — padding-block-start + padding-block-end sum exceeds block size

When both padding-block-start and padding-block-end are set to values that together exceed the dialog's block size minus border widths, the content area height becomes zero or negative (clamped to zero). The entire content area collapses. Unlike the inset-based collapse (which reduces the element's BCR height to zero), this attack leaves the element's BCR height at its full fixed block size — but the content area inside is zero. The consent text and approve button are both invisible and unclickable. No height, min-height, or overflow check at the element level reveals the attack; only the padding sum check does.

/* Attack: bilateral padding collapse — both block paddings sum beyond block-size */
.consent-dialog {
  block-size: 300px !important;
  box-sizing: border-box !important;
  overflow: hidden !important;
  padding-block-start: 180px !important;
  padding-block-end:   180px !important;
  /* content area = 300 - 180 - 180 = -60 → clamped to 0 */
}

/* Effect:
   dialog.offsetHeight = 300px → unchanged
   dialog.getBoundingClientRect().height = 300px → unchanged
   Content area = 0px → everything clipped
   The dialog appears as a solid block with no visible content.

   An auditor not checking padding might conclude the dialog
   is just a styled container; only the padding sum check reveals the attack. */

function checkBilateralPaddingBlockCollapse(consentEl) {
  const cs  = getComputedStyle(consentEl);
  const pbs = parseFloat(cs.getPropertyValue('padding-block-start')) || 0;
  const pbe = parseFloat(cs.getPropertyValue('padding-block-end'))   || 0;
  const h   = consentEl.offsetHeight || 0;
  const bw  = (parseFloat(cs.borderTopWidth) || 0) + (parseFloat(cs.borderBottomWidth) || 0);
  const contentArea = h - bw - pbs - pbe;
  return {
    paddingBlockStart: pbs,
    paddingBlockEnd:   pbe,
    blockSize:         h,
    contentArea:       Math.max(0, contentArea),
    collapsed:         contentArea <= 0,
  };
}

Attack 3: writing-mode: vertical-rlpadding-block-end maps to physical padding-left

In writing-mode: vertical-rl, the block axis runs horizontally from right to left. padding-block-end maps to physical padding-left. An audit reading getComputedStyle(el).paddingBottom finds zero and reports no attack. The content area is crushed from the left side — the approve button (which is at the inline-end of content in vertical-rl flow) is pushed off the left edge of the element's content box. Only reading getPropertyValue('padding-block-end') (the logical property) or checking the button's BCR catches the attack.

/* Attack: vertical-rl + padding-block-end → physical padding-left */
.consent-wrapper {
  writing-mode: vertical-rl;
}

.consent-dialog {
  inline-size: 300px !important;        /* width in vertical-rl = inline-size */
  box-sizing: border-box !important;
  overflow: hidden !important;
  padding-block-end: 270px !important;  /* maps to padding-left → crushes from left */
}

/* getComputedStyle(dialog).paddingBottom = "0px" → bottom audit misses it
   getComputedStyle(dialog).paddingLeft   = "270px" → physical left read catches it
   getPropertyValue('padding-block-end')  = "270px" → logical read always catches it */

function checkPaddingBlockEndWritingMode(consentEl) {
  const cs = getComputedStyle(consentEl);
  return {
    paddingBlockEnd:  parseFloat(cs.getPropertyValue('padding-block-end')) || 0,
    paddingBottom:    parseFloat(cs.paddingBottom) || 0,
    writingMode:      cs.writingMode,
    axisMismatch: cs.writingMode !== 'horizontal-tb',
  };
}

Attack 4: JS mousedown injection — large padding-block-end at click time

At page load, the consent dialog has correct padding and the approve button is fully visible. A mousedown listener on the approve button injects a large padding-block-end on the dialog. With box-sizing: border-box, the content area collapses from below during the press — the button shifts upward inside the dialog, and what the user was clicking is now the consent body text or empty space. The click fires on whichever element occupies that position. At mouseup, the padding is restored.

/* Mousedown: inject padding-block-end to collapse content area during press */
(function () {
  document.querySelectorAll('.approve-btn, [data-action="allow"]').forEach(btn => {
    const dialog = btn.closest('.consent-dialog');
    if (!dialog) return;

    btn.addEventListener('mousedown', () => {
      dialog.style.setProperty('padding-block-end', '500px', 'important');
    }, { passive: true });

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

Use a MutationObserver on the consent dialog's style attribute to detect mousedown padding injection. When padding-block-end or padding-bottom changes during an active pointer event, re-check both the content area height and the approve button's BCR. A transient collapse during mousedown is HIGH severity even if the padding is restored at mouseup.

Detection summary

HIGH Content area height ≤ 0 — bilateral padding-block-start + padding-block-end sum exceeds the dialog's fixed block size; entire content area is collapsed and clipped.
HIGH Approve button BCR not in viewport, or approve button BCR height < 4px — button has been pushed out of the content area by padding-block-end combined with border-box and fixed block size.
MEDIUM Content area height < 40px — approve button may be partially crushed from below; on smaller viewports the button may exit the visible area entirely.
MEDIUM padding-block-end value > 100px on a consent dialog with box-sizing: border-box and a fixed block size — content area at high risk of crush.
MEDIUM Mousedown listener on approve button injects padding-block-end on the consent dialog — transient content-area crush not detectable at page-load audit time.
/* Complete padding-block-end consent audit */
function auditPaddingBlockEnd(consentEl) {
  const cs  = getComputedStyle(consentEl);
  const pbe = parseFloat(cs.getPropertyValue('padding-block-end'))   || 0;
  const pbs = parseFloat(cs.getPropertyValue('padding-block-start')) || 0;
  const h   = consentEl.offsetHeight || 0;
  const bw  = (parseFloat(cs.borderTopWidth) || 0) + (parseFloat(cs.borderBottomWidth) || 0);
  const contentArea = h - bw - pbs - pbe;
  const approveBtn = consentEl.querySelector('[data-action="allow"], .approve-btn, button[type="submit"]');
  const btnBCR = approveBtn ? approveBtn.getBoundingClientRect() : null;
  return {
    paddingBlockEnd:    pbe,
    paddingBlockStart:  pbs,
    blockSize:          h,
    contentArea:        Math.max(0, contentArea),
    collapsed:          contentArea <= 0,
    contentCrushed:     contentArea < 40,
    buttonInViewport:   btnBCR
      ? (btnBCR.top < window.innerHeight && btnBCR.bottom > 0 &&
         btnBCR.left < window.innerWidth  && btnBCR.right  > 0)
      : null,
    writingMode:        cs.writingMode,
  };
}

SkillAudit checks padding-block-end by reading the logical property directly (catching writing-mode remaps), computing the available content area height against the dialog's block size and border widths, and checking the approve button's own BCR independently. Bilateral padding collapse is detected by summing both block padding values against the element's block size. Run a free audit →