Security Guide
MCP server CSS padding-inline-end security — border-box inline-end content crush, bilateral inline collapse, scrollWidth overflow signal, JS mousedown injection
CSS padding-inline-end is the individual logical property for the end-side inline padding of an element. In horizontal-tb with direction: ltr, it maps to physical padding-right. A large padding-inline-end on a consent dialog with a fixed inline-size and box-sizing: border-box crushes the content area from the right. Approve buttons commonly appear at the end of dialog content — they are the first controls pushed out of the content area when end-side padding expands. Unlike block-end attacks, this attack is often invisible: the dialog appears full-width, the button may still have a non-zero BCR height, but its inline position is past the dialog's visible right edge.
CSS padding-inline-end — property overview
The padding-inline-end property sets padding at the inline-end edge of an element's content box. In horizontal-tb LTR it maps to physical padding-right; in horizontal-tb RTL, to padding-left; in vertical-rl, to padding-bottom; in vertical-lr, to padding-top. It is a sub-property of padding-inline (shorthand) and padding (shorthand). Related: padding-inline-start, padding-block-end, padding-block shorthand.
Attack 1: large padding-inline-end + border-box — crushing content from the inline end
When a consent dialog has a fixed inline-size and box-sizing: border-box, the content area width equals the inline size minus all horizontal padding and borders. A large padding-inline-end eats into that space from the right. Consent flows typically place the approve button at the trailing end of the content — it is the last child and has the largest inline offset. When end-side padding expands, the approve button is pushed beyond the content area's right boundary and is either clipped by overflow: hidden or visually exits the dialog box. The dialog's offsetWidth remains unchanged; auditing offsetWidth alone reports nothing suspicious.
/* Attack: border-box — large padding-inline-end crushes content from the right */
.consent-dialog {
inline-size: 400px !important;
box-sizing: border-box !important;
overflow: hidden !important;
padding-inline-end: 370px !important; /* 370px right padding → 30px content area */
}
/* Effect:
Content area width = 400 - 370 = 30px
Approve button (aligned right in content flow) is the first control pushed off-right
dialog.offsetWidth = 400px → width check passes
getPropertyValue('padding-inline-end') = "370px" → always reveals the attack */
function checkPaddingInlineEndCrush(consentEl) {
const cs = getComputedStyle(consentEl);
const pie = parseFloat(cs.getPropertyValue('padding-inline-end')) || 0;
const pis = parseFloat(cs.getPropertyValue('padding-inline-start')) || 0;
const w = consentEl.offsetWidth || 0;
const bw = (parseFloat(cs.borderLeftWidth) || 0) + (parseFloat(cs.borderRightWidth) || 0);
const contentArea = w - bw - pis - pie;
const approveBtn = consentEl.querySelector('[data-action="allow"], .approve-btn, button[type="submit"]');
const btnBCR = approveBtn ? approveBtn.getBoundingClientRect() : null;
return {
paddingInlineEnd: pie,
contentAreaWidth: contentArea,
contentCrushed: contentArea < 40,
buttonVisible: btnBCR
? (btnBCR.left < window.innerWidth && btnBCR.right > 0)
: null,
};
}
End-side inline attacks are directionally mirrored in RTL: in dir="rtl" layouts, padding-inline-end maps to physical padding-left. An auditor checking paddingRight in an RTL dialog finds zero and misses the attack. Approve buttons in RTL dialogs are physically on the left — the content is crushed from the left by the same property name. Always read the logical property.
Attack 2: bilateral inline padding collapse — padding-inline-start + padding-inline-end sum exceeds inline size
When both padding-inline-start and padding-inline-end are set to values that together exceed the dialog's inline size minus border widths, the content area width collapses to zero. Neither padding value individually exceeds a per-property threshold — each appears moderate. Only summing both against the element's inline size reveals the attack. The dialog's BCR width remains at the full fixed inline size; all content is squeezed into zero pixels of content area. This is distinct from the collapse caused by margin-inline attacks; here the element itself appears correctly sized but is internally empty.
/* Attack: bilateral inline padding — both sum beyond inline-size */
.consent-dialog {
inline-size: 400px !important;
box-sizing: border-box !important;
overflow: hidden !important;
padding-inline-start: 220px !important;
padding-inline-end: 220px !important;
/* content area = 400 - 220 - 220 = -40 → clamped to 0 */
}
/* Effect:
Each padding individually reads 220px — below some "this looks suspicious" threshold?
Combined: 440px padding on a 400px dialog → 0px content area
dialog.getBoundingClientRect().width = 400px → unchanged
Content: fully collapsed and clipped.
Detection requires: pis + pie >= inlineSize - borderWidths */
function checkBilateralPaddingInlineCollapse(consentEl) {
const cs = getComputedStyle(consentEl);
const pis = parseFloat(cs.getPropertyValue('padding-inline-start')) || 0;
const pie = parseFloat(cs.getPropertyValue('padding-inline-end')) || 0;
const w = consentEl.offsetWidth || 0;
const bw = (parseFloat(cs.borderLeftWidth) || 0) + (parseFloat(cs.borderRightWidth) || 0);
const contentArea = w - bw - pis - pie;
return {
paddingInlineStart: pis,
paddingInlineEnd: pie,
inlineSize: w,
contentArea: Math.max(0, contentArea),
collapsed: contentArea <= 0,
};
}
Attack 3: scrollWidth overflow — large padding-inline-end without border-box
When the consent dialog does not use box-sizing: border-box, padding-inline-end does not consume from the fixed width — instead it expands the total element width, creating a horizontal scrollable overflow beyond the dialog's visual boundary. With overflow: auto or no overflow set, the approve button is rendered at a correct content-area position but its absolute horizontal offset places it beyond the visible right edge of the container. clientWidth reports the visible width; scrollWidth reports the expanded total. When scrollWidth > clientWidth, content has overflowed horizontally. The approve button may be accessible via horizontal scroll — an unusual interaction that the user is not prompted to perform.
/* Attack: content-box sizing — large padding-inline-end expands scrollWidth */
.consent-dialog {
width: 400px !important;
box-sizing: content-box !important; /* or default */
overflow: auto !important; /* allows horizontal scroll */
padding-inline-end: 800px !important; /* expands total width to 400 + 800 = 1200px */
}
/* Effect:
clientWidth = 400px (visible area)
scrollWidth = 1200px
Approve button rendered at ~380px from left → within clientWidth, visible
But dialog consent text may be at >400px → only reachable via scroll
Alternatively: approve button itself at >400px → invisible without scroll
Signal: scrollWidth > clientWidth on a consent dialog is suspicious */
function checkPaddingInlineEndScrollOverflow(consentEl) {
const cs = getComputedStyle(consentEl);
const pie = parseFloat(cs.getPropertyValue('padding-inline-end')) || 0;
return {
paddingInlineEnd: pie,
clientWidth: consentEl.clientWidth,
scrollWidth: consentEl.scrollWidth,
horizontalOverflow: consentEl.scrollWidth > consentEl.clientWidth,
overflowRatio: consentEl.scrollWidth / (consentEl.clientWidth || 1),
};
}
Attack 4: JS mousedown injection — large padding-inline-end at click time
At page load, the consent dialog is correctly sized. A mousedown listener injects a large padding-inline-end on the dialog during the press interval. With border-box, the content area collapses from the right — the approve button shifts left of its expected position. The click fires on whatever element occupies the former button position. At mouseup, the padding is restored. As with all mousedown injection attacks, the static page state appears legitimate; only an observer monitoring style mutations during active pointer events detects the injection.
/* Mousedown: inject padding-inline-end to collapse content from inline-end 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-inline-end', '500px', 'important');
}, { passive: true });
btn.addEventListener('mouseup', () => dialog.style.removeProperty('padding-inline-end'), { passive: true });
btn.addEventListener('mouseleave', () => dialog.style.removeProperty('padding-inline-end'), { passive: true });
});
})();
Bilateral mousedown injection — some attacks inject both padding-inline-start and padding-inline-end simultaneously at mousedown. Each individual value may be below detection thresholds when examined in isolation. Always check the bilateral sum against the dialog's inline size during active pointer events, not just the individual property values.
Detection summary
padding-inline-end alone or combined with padding-inline-start exceeds the dialog's fixed inline size; all content is collapsed.
padding-inline-end with border-box.
scrollWidth > clientWidth on a consent dialog — horizontal overflow indicates large padding-inline-end without border-box, pushing content off the visible right edge.
padding-inline-end on the consent dialog — transient inline-end crush not detectable at page-load audit time.
/* Complete padding-inline-end consent audit */
function auditPaddingInlineEnd(consentEl) {
const cs = getComputedStyle(consentEl);
const pie = parseFloat(cs.getPropertyValue('padding-inline-end')) || 0;
const pis = parseFloat(cs.getPropertyValue('padding-inline-start')) || 0;
const w = consentEl.offsetWidth || 0;
const bw = (parseFloat(cs.borderLeftWidth) || 0) + (parseFloat(cs.borderRightWidth) || 0);
const contentArea = w - bw - pis - pie;
const approveBtn = consentEl.querySelector('[data-action="allow"], .approve-btn, button[type="submit"]');
const btnBCR = approveBtn ? approveBtn.getBoundingClientRect() : null;
return {
paddingInlineEnd: pie,
paddingInlineStart: pis,
inlineSize: w,
contentAreaWidth: Math.max(0, contentArea),
collapsed: contentArea <= 0,
contentCrushed: contentArea < 40,
horizontalOverflow: consentEl.scrollWidth > consentEl.clientWidth,
buttonInViewport: btnBCR
? (btnBCR.left < window.innerWidth && btnBCR.right > 0 &&
btnBCR.top < window.innerHeight && btnBCR.bottom > 0)
: null,
writingMode: cs.writingMode,
direction: cs.direction,
};
}
SkillAudit checks padding-inline-end by reading the logical property directly (catching direction and writing-mode remaps), computing the bilateral padding sum against the dialog's inline size, and checking scrollWidth for overflow-based attacks. The approve button's BCR is verified independently regardless of the padding value's absolute magnitude. Run a free audit →