Security Guide
MCP server CSS padding-block-start security — content-area crush (border-box), em-relative font-size coupling, writing-mode axis remap, JS mousedown injection
CSS padding-block-start is the individual sub-property for the logical top padding of an element (distinct from the padding-block shorthand). In horizontal-tb writing mode it maps to physical padding-top. When the consent dialog has a fixed block size and box-sizing: border-box, a large padding-block-start consumes the dialog's available height from the top — the content area shrinks, the approve button is pushed below the dialog's bottom edge, and all content is clipped if overflow: hidden is set. The dialog's computed height appears normal; only the content area's available height and the approve button's BCR reveal the attack.
CSS padding-block-start — property overview
The padding-block-start property sets the padding at the block-start edge of an element's padding box. In horizontal-tb it maps to padding-top. In vertical-rl, it maps to padding-right. It is an individual property (not a shorthand) and participates in the padding-block shorthand (which sets block-start and block-end together). Setting only padding-block-start leaves padding-block-end at its default — this is why attacks using only this property are distinct from bilateral padding attacks using the shorthand. Related: padding-block shorthand, padding-inline.
Attack 1: large padding-block-start + border-box content-area crush
A consent dialog with a fixed block-size (height) and box-sizing: border-box has a total block dimension that includes padding. A large padding-block-start consumes that fixed height from the top. For a 300px-tall dialog: padding-block-start: 280px leaves only 20px of content area below the padding — not enough to display the consent text or the approve button. If the dialog also has overflow: hidden, the button is invisible and unclickable. Even without hidden overflow, the button may be outside the dialog's visual boundary, below the fold. The dialog's computed height property remains 300px — a height check does not reveal the attack.
/* Attack: border-box + large padding-block-start crushes content area */
.consent-dialog {
block-size: 300px !important;
box-sizing: border-box !important;
overflow: hidden !important; /* clips content below the content area */
padding-block-start: 280px !important; /* only 20px content area remains */
}
/* Effect:
dialog.offsetHeight = 300 (normal — border-box locks height)
dialog.clientHeight = 300 (no border in this example)
Content area height = 300 - 280 = 20px → consent text + button invisible
getComputedStyle(dialog).height = "300px" → height audit passes
getComputedStyle(dialog).paddingTop = "280px" → only padding audit catches it
If overflow is NOT hidden:
The button is below the dialog's visual bottom (300px from top, pushed to 280+buttonHeight)
BCR of the button: top ≈ dialog.BCR.top + 300px → potentially below viewport */
function checkPaddingBlockStartCrush(consentEl) {
const cs = getComputedStyle(consentEl);
const pbs = parseFloat(cs.getPropertyValue('padding-block-start')) || 0;
const h = consentEl.offsetHeight || 0;
const contentAreaH = h - pbs -
(parseFloat(cs.getPropertyValue('padding-block-end')) || 0) -
(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 {
paddingBlockStart: pbs,
contentAreaHeight: contentAreaH,
contentAreaCrushed: contentAreaH < 40, /* less than one button row of space */
buttonBelowFold: btnBCR ? btnBCR.top >= window.innerHeight : null,
};
}
Border-box content-area crush leaves the dialog's computed height unchanged. The height audit passes: 300px === 300px. Only checking getPropertyValue('padding-block-start') against the dialog's block size — or checking the approve button's own BCR — reveals the attack.
Attack 2: em-relative padding-block-start + font-size amplification
A padding-block-start expressed in em units is proportional to the element's font-size. If the consent dialog's font-size is later changed (or was set by the MCP server to a large value), the em-based padding grows proportionally. For example, padding-block-start: 20em at a font-size: 16px produces 320px of padding. An MCP server that sets font-size: 24px on the dialog elevates the padding to 480px without ever touching the padding value itself. An audit that reads the computed pixel value of padding at a specific moment may see the attack only if the font-size injection happens first.
/* Attack: em-relative padding + font-size injection */
/* Step 1: set padding in em */
.consent-dialog {
padding-block-start: 20em !important; /* depends on font-size */
}
/* Step 2: inject font-size to amplify */
.consent-dialog {
font-size: 24px !important; /* 20em × 24px = 480px of top padding */
}
/* Effect:
At font-size:16px → padding-block-start resolved = 320px
At font-size:24px → padding-block-start resolved = 480px
The padding declaration itself never changes — only the font-size does.
An audit that captures padding-block-start at page load (before font-size injection)
sees 320px. After font-size change it becomes 480px — but the style hasn't changed.
Re-evaluation is required after any font-size change on consent elements. */
function checkEmPaddingBlockStart(consentEl) {
const cs = getComputedStyle(consentEl);
const pbs = parseFloat(cs.getPropertyValue('padding-block-start')) || 0;
const fontSize = parseFloat(cs.fontSize) || 16;
const pbsEm = pbs / fontSize; /* if ratio is large, em-based attack suspected */
return {
paddingBlockStartPx: pbs,
fontSizePx: fontSize,
paddingInEm: pbsEm,
suspiciousEmRatio: pbsEm > 10, /* 10em+ top padding is an attack indicator */
};
}
Attack 3: writing-mode: vertical-rl — padding-block-start maps to physical padding-right
In writing-mode: vertical-rl, the block axis runs from right to left. padding-block-start maps to physical padding-right. A consent dialog inside a vertical-rl container with a large padding-block-start is effectively attacked on its right side — the content area is crushed from the right, not the top. An audit reading getComputedStyle(el).paddingTop finds zero and reports no attack. The logical property read via getPropertyValue('padding-block-start') returns the large value regardless of writing mode.
/* Attack: writing-mode:vertical-rl makes padding-block-start map to physical right */
.consent-wrapper {
writing-mode: vertical-rl;
}
.consent-dialog {
padding-block-start: 400px !important; /* maps to padding-right in vertical-rl */
/* Content area crushed from the right side */
/* getComputedStyle(dialog).paddingTop = "0px" → top audit misses it */
/* getComputedStyle(dialog).paddingRight = "400px" → physical right read catches it */
/* getPropertyValue('padding-block-start') = "400px" → logical read always catches it */
}
/* Detection: always read the logical property, not the physical one */
function checkPaddingBlockStartWritingMode(consentEl) {
const cs = getComputedStyle(consentEl);
return {
paddingBlockStart: parseFloat(cs.getPropertyValue('padding-block-start')) || 0,
paddingTop: parseFloat(cs.paddingTop) || 0, /* physical — may be 0 in vertical-rl */
writingMode: cs.writingMode,
mismatch: (parseFloat(cs.getPropertyValue('padding-block-start')) || 0) !==
(parseFloat(cs.paddingTop) || 0),
};
}
Attack 4: JS mousedown injection — large padding-block-start at click time
At page load, the consent dialog is correctly laid out. A mousedown listener on the approve button injects a large padding-block-start on the dialog, collapsing the content area from the top during the press. The button shifts downward inside the dialog and may exit the visible area. The user's cursor is aimed at the button's original position, which is now empty or occupied by another element. At mouseup, the padding is removed and the dialog restores to its valid layout.
/* Mousedown: inject padding-block-start 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-start', '500px', 'important');
}, { passive: true });
btn.addEventListener('mouseup', () => dialog.style.removeProperty('padding-block-start'), { passive: true });
btn.addEventListener('mouseleave', () => dialog.style.removeProperty('padding-block-start'), { passive: true });
});
})();
Mousedown padding-block-start injection requires a MutationObserver on the consent dialog's style attribute. When padding-block-start or padding-top changes during an active pointer event, immediately re-check the approve button's BCR and the dialog's content area height. A transient crush during mousedown is HIGH severity.
Detection summary
padding-block-start minus padding-block-end) is less than 40px — approve button is pushed out of the visible content area.
BCR.top >= window.innerHeight or BCR.bottom <= 0 — button is outside the visible viewport, pushed out by padding-block-start combined with a fixed block-size.
padding-block-start (>100px) on a consent dialog with box-sizing: border-box and a fixed block-size — content area may be critically reduced on smaller viewport sizes.
padding-block-start is expressed in em units and the ratio exceeds 10em — font-size amplification can dramatically increase the padding without changing the padding declaration.
padding-block-start on the consent dialog — transient content-area crush not detectable at page-load audit time.
/* Complete padding-block-start consent audit */
function auditPaddingBlockStart(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 contentArea = h - pbs - pbe -
(parseFloat(cs.borderTopWidth) || 0) -
(parseFloat(cs.borderBottomWidth) || 0);
const fontSize = parseFloat(cs.fontSize) || 16;
const approveBtn = consentEl.querySelector('[data-action="allow"], .approve-btn, button[type="submit"]');
const btnBCR = approveBtn ? approveBtn.getBoundingClientRect() : null;
return {
paddingBlockStart: pbs,
paddingBlockEnd: pbe,
contentAreaHeight: contentArea,
contentAreaCrushed: contentArea < 40,
emRatio: pbs / fontSize,
buttonBelowFold: btnBCR ? btnBCR.top >= window.innerHeight : null,
writingMode: cs.writingMode,
};
}
SkillAudit reads padding-block-start via getPropertyValue (catching writing-mode remaps), computes the available content area against the dialog's block size and box-sizing, and independently checks the approve button's own BCR. Font-size amplification is flagged by computing the padding-to-font-size ratio. Run a free audit →