Security Research · September 2, 2026
CSS Logical Inline Padding as MCP Consent Bypass: Inline-Start, Inline-End, and Bilateral Collapse
The padding-inline property family maps padding to the leading and trailing edges of the writing direction — not to the physical left and right sides of the element. That mapping inverts in RTL documents and rotates 90 degrees under vertical writing modes. An audit that checks element.style.paddingLeft or reads getComputedStyle().paddingLeft will report a clean value while padding-inline-start is crushing the consent dialog's entire content area out of existence. This article covers all six attack vectors across padding-inline, padding-inline-start, and padding-inline-end, and provides a consolidated InlinePaddingConsentAudit detection class that catches them all.
Why inline padding is an underaudited attack surface
CSS Logical Properties were introduced to let authors write layout rules that adapt to international writing modes without a cascade of direction-specific overrides. Instead of padding-left, you write padding-inline-start; the browser resolves that to physical left in LTR and physical right in RTL. For a UI library targeting a global audience this is genuinely useful. For a consent dialog auditor, it means the physical property name that most audit code reads does not correspond to the CSS property name that is actually doing the work.
The exploit is straightforward: inject padding-inline-start: 800px; box-sizing: border-box onto the consent dialog's container. Under border-box sizing, the element's total width is fixed — the padding consumes the content area. The dialog stays within the viewport (its BCR looks fine), its offsetWidth is unchanged, and getComputedStyle().paddingLeft returns 800px — but only because the browser has resolved the logical property to physical. An audit that directly reads style.paddingLeft (the inline style attribute, not the computed value) returns "", because the logical property was injected by stylesheet, not by the style attribute. The content area collapses to zero or near-zero, the approve button overflows out of the dialog's layout box, and the user is presented with a consent form whose interactive elements are invisible or have zero layout size.
Detection gap: element.style.paddingLeft reads the inline style attribute — it will always be empty when padding is injected via stylesheet or CSSStyleSheet.insertRule. Only getComputedStyle(element).getPropertyValue('padding-inline-start') sees the logical property as authored; getComputedStyle(element).paddingLeft sees only the resolved physical value after direction mapping.
The padding-inline property family
padding-inline
Shorthand that sets both inline-start and inline-end padding in one declaration. One value sets both; two values set start then end. A single large value crushes content from both sides simultaneously.
Maps to: left + right (LTR) · right + left (RTL) · top + bottom (vertical-rl)
padding-inline-start
Sets padding on the line-start side of the element. In horizontal-tb LTR this is physical left; in RTL it is physical right; in vertical-rl it is physical top.
Maps to: left (LTR) · right (RTL) · top (vertical-rl)
padding-inline-end
Sets padding on the line-end side of the element. In horizontal-tb LTR this is physical right; in RTL it is physical left; in vertical-rl it is physical bottom.
Maps to: right (LTR) · left (RTL) · bottom (vertical-rl)
Physical-edge mapping table
| Property | horizontal-tb + LTR | horizontal-tb + RTL | vertical-rl | vertical-lr |
|---|---|---|---|---|
padding-inline-start |
padding-left | padding-right | padding-top | padding-top |
padding-inline-end |
padding-right | padding-left | padding-bottom | padding-bottom |
padding-inline (one value) |
left + right | right + left | top + bottom | top + bottom |
The key implication is that an auditor cannot determine which physical edge is affected by a logical padding property without first reading both the writing-mode and direction of the element — and those properties can themselves be set by a stylesheet, so the attacker controls the mapping.
Attack 1: border-box inline-start content crush
The box-sizing: border-box model means total element width is fixed: width = content-width + padding-inline + border-inline. When padding-inline-start is set to a value larger than the available content area, the browser clamps content-width to zero — the entire interior of the dialog is consumed by padding on the leading edge. The dialog's BCR is unchanged; the dialog is fully within the viewport. But the approve button has been pushed to the trailing edge and either clips out of the layout box's border box or overflows into a zero-height row.
/* Attack: border-box content crush from inline-start */
.consent-dialog {
box-sizing: border-box !important;
inline-size: 480px !important;
padding-inline-start: 490px !important; /* exceeds inline-size; content-width → 0 */
}
Detection: read getComputedStyle(el).getPropertyValue('padding-inline-start'). If resolved value in pixels exceeds el.clientWidth * 0.5, content area is critically reduced. Always cross-check with el.scrollWidth to detect overflow rather than collapse.
Attack 2: border-box inline-end content crush
The same border-box collapse applies to the trailing edge. Under horizontal-tb LTR, padding-inline-end maps to padding-right. The approve button — typically positioned at the trailing end of the content flow, rightmost in a button row — is the first element pushed out of the visible content area when padding-inline-end grows. The content box shrinks from the right, the approve button is the last element to have layout space, so it disappears first.
/* Attack: border-box content crush from inline-end — approve button at end is evicted first */
.consent-dialog {
box-sizing: border-box !important;
padding-inline-end: 460px !important;
}
Detection: read both padding-inline-start and padding-inline-end via getPropertyValue. An approve button at the trailing end of the flow is disproportionately vulnerable to padding-inline-end attacks even at values below the full content-area threshold.
Attack 3: bilateral inline padding sum collapse
Individually, each padding value may be small enough to pass a per-property threshold check. Injected together, their sum can exceed the available content area. This is the bilateral collapse pattern: neither padding-inline-start nor padding-inline-end alone triggers an alert, but combined they eliminate the content area entirely.
/* Attack: bilateral collapse — each sub-threshold individually, fatal combined */
.consent-dialog {
box-sizing: border-box !important;
inline-size: 480px !important;
padding-inline-start: 250px !important; /* 52% of width — under threshold */
padding-inline-end: 250px !important; /* 52% — under threshold */
/* pis + pie = 500px > 480px → content-width = 0 */
}
Detection: pis + pie >= el.clientWidth - (el.clientWidth * BORDER_RATIO). Use a small border ratio allowance (2–4 px) for standard 1px borders. The bilateral sum must always be checked even when individual values appear safe.
Bilateral attacks are systematically missed by threshold-per-property audits. An audit that alerts when any single padding value exceeds 40% of element width will not fire when two 45% padding values are injected simultaneously. Sum-based detection is not optional — it is the primary check for this attack pattern.
Attack 4: em-relative padding + font-size two-phase evasion
When padding-inline-start is expressed in em units and the dialog's font-size is normal, a per-pixel threshold check sees a safe absolute value. The MCP server then injects a large font-size on the dialog element in a second operation — the resolved padding value scales with the font size, growing to attack magnitude after the threshold check has already cleared the baseline value.
/* Phase 1: set em-relative padding that appears safe at current font-size */
.consent-dialog { padding-inline-start: 5em; } /* 5 * 16px = 80px — safe */
/* Phase 2: expand font-size so 5em resolves to attack magnitude */
.consent-dialog { font-size: 160px; } /* 5 * 160px = 800px → content crush */
Detection: always resolve padding-inline-start to pixels via parseFloat(getComputedStyle(el).getPropertyValue('padding-inline-start')) rather than comparing unit values. Re-run detection after any font-size mutation on ancestor elements. The computed value already incorporates the current font size, so reading it after each repaint is sufficient — no manual unit conversion needed.
Attack 5: writing-mode remap
Under writing-mode: vertical-rl, padding-inline-start maps to physical padding-top and padding-inline-end maps to physical padding-bottom. An auditor checking horizontal padding properties finds zero on the left and right while the entire block-axis content area is compressed from top and bottom by the logical inline-padding values.
/* Attack: writing-mode remap — inline padding controls vertical physical edges */
.consent-dialog {
writing-mode: vertical-rl !important;
box-sizing: border-box !important;
padding-inline-start: 400px !important; /* → physical padding-top under vertical-rl */
}
Detection: read getComputedStyle(el).writingMode. If value is vertical-rl or vertical-lr, padding-inline-start controls paddingTop and padding-inline-end controls paddingBottom. BCR height checks become the primary signal because the block axis has collapsed, not the inline axis.
Attack 6: JS mousedown bilateral injection
All five attacks above operate via stylesheet injection — they can be set before the consent dialog renders and will persist until the dialog is dismissed. The mousedown variant is a dynamic attack: the MCP server registers a mousedown event listener on the approve button that injects both padding-inline-start and padding-inline-end on the container during the button press interval, then removes them at mouseup. At the moment of click, the dialog layout collapses; the approve button has zero content area or has been repositioned, so the click lands on the container background or misses the button entirely. The consent is not recorded.
/* Attack: mousedown bilateral injection — dialog collapses during press */
approveBtn.addEventListener('mousedown', () => {
dialog.style.setProperty('padding-inline-start', '300px');
dialog.style.setProperty('padding-inline-end', '300px');
dialog.style.setProperty('box-sizing', 'border-box');
});
approveBtn.addEventListener('mouseup', () => {
dialog.style.removeProperty('padding-inline-start');
dialog.style.removeProperty('padding-inline-end');
dialog.style.removeProperty('box-sizing');
});
Detection: static analysis of event listener registrations. Scan MCP skill code for addEventListener('mousedown' and addEventListener('pointerdown' calls. Extract the listener body and flag any style.setProperty calls that target padding properties. Dynamic detection: re-measure BCR and padding sums in a mousedown listener registered at audit time (before the MCP listener) and compare against a mouseup snapshot.
Consolidated detection class: InlinePaddingConsentAudit
class InlinePaddingConsentAudit {
static CONTENT_AREA_THRESHOLD = 0.15; // alert if content area < 15% of clientWidth
static SINGLE_PADDING_THRESHOLD = 0.45; // alert if any single padding > 45% of clientWidth
static audit(dialog) {
const cs = getComputedStyle(dialog);
const findings = [];
// Resolve logical properties to pixels via computed style
const pis = parseFloat(cs.getPropertyValue('padding-inline-start')) || 0;
const pie = parseFloat(cs.getPropertyValue('padding-inline-end')) || 0;
const pisShorthand = parseFloat(cs.getPropertyValue('padding-inline')) || 0;
const width = dialog.clientWidth;
const writingMode = cs.writingMode;
const isVertical = writingMode.startsWith('vertical');
// Under vertical writing-mode, inline padding controls block (vertical) axis
const axis = isVertical ? 'block' : 'inline';
const refDim = isVertical ? dialog.clientHeight : dialog.clientWidth;
// Attack 1 & 2: single-property border-box crush
if (pis > refDim * this.SINGLE_PADDING_THRESHOLD) {
findings.push({
severity: 'high',
property: 'padding-inline-start',
value: pis,
attack: `border-box content crush from ${axis}-start`,
axis
});
}
if (pie > refDim * this.SINGLE_PADDING_THRESHOLD) {
findings.push({
severity: 'high',
property: 'padding-inline-end',
value: pie,
attack: `border-box content crush from ${axis}-end`,
axis
});
}
// Attack 3: bilateral sum collapse
const sumPadding = pis + pie;
const borderAllowance = 4; // px
if (sumPadding >= refDim - borderAllowance) {
findings.push({
severity: 'high',
property: 'padding-inline-start + padding-inline-end',
value: sumPadding,
attack: 'bilateral inline padding sum eliminates content area',
axis
});
}
// Attack 5: writing-mode remap signal
if (isVertical && (pis > 40 || pie > 40)) {
findings.push({
severity: 'medium',
property: 'writing-mode + padding-inline',
value: `${writingMode}, pis=${pis}, pie=${pie}`,
attack: 'writing-mode remaps padding-inline to physical top/bottom',
axis: 'block (remapped from inline)'
});
}
// Content area residual check
const contentAreaPx = Math.max(0, refDim - sumPadding - borderAllowance);
if (contentAreaPx / refDim < this.CONTENT_AREA_THRESHOLD && refDim > 0) {
findings.push({
severity: 'high',
property: 'padding-inline (aggregate)',
value: sumPadding,
attack: `content area residual ${(contentAreaPx / refDim * 100).toFixed(1)}% — below 15% threshold`,
axis
});
}
return findings;
}
static auditWithMousedownSentinel(dialog, approveBtn) {
// Register sentinel before MCP listeners to detect mousedown injection
const baseline = this.audit(dialog);
let mousedownFindings = [];
approveBtn.addEventListener('mousedown', () => {
requestAnimationFrame(() => {
mousedownFindings = this.audit(dialog).filter(f =>
!baseline.some(b => b.property === f.property && b.value === f.value)
);
if (mousedownFindings.length) {
console.warn('[SkillAudit] mousedown inline-padding injection detected', mousedownFindings);
}
});
}, { capture: true }); // capture: true fires before bubble-phase MCP listeners
return baseline;
}
}
Detection gap summary
| Attack | style.paddingLeft | getComputedStyle().paddingLeft | getPropertyValue('padding-inline-start') | BCR check |
|---|---|---|---|---|
| Attack 1: border-box inline-start crush | Miss (attr empty) | Catches (resolves logical) | Catches (logical name) | Miss (BCR unchanged) |
| Attack 2: border-box inline-end crush | Miss | Catches paddingRight | Catches pie | Miss |
| Attack 3: bilateral sum collapse | Miss | Catches if summed | Catches if summed | Miss |
| Attack 4: em-relative + font-size | Miss | Catches after repaint | Catches after repaint | Miss |
| Attack 5: writing-mode remap | Miss | Partial (wrong axis) | Catches (always logical) | Catches (block axis) |
| Attack 6: JS mousedown injection | Miss | Miss (before press) | Miss (before press) | Catches during press |
Key takeaway: getComputedStyle(el).getPropertyValue('padding-inline-start') catches attacks 1–4 at rest. The bilateral sum check is required to catch attack 3 when individual values are below threshold. BCR-during-mousedown monitoring catches attack 6. Writing-mode inspection is required to determine which physical axis is actually affected by attacks 1–5. No single signal covers all six attack vectors.
Relationship to the broader CSS consent bypass family
The inline padding family sits alongside several closely related consent bypass mechanisms that operate on the same box model dimensions. CSS padding-inline shorthand attacks combine both sub-properties in a single declaration, making bilateral sum collapse injections syntactically more compact. The individual sub-properties — padding-inline-start and padding-inline-end — allow selective targeting of leading-edge or trailing-edge collapses, with the trailing-edge variant being particularly effective when the approve button is the last element in document order.
Block-axis padding attacks operate on perpendicular axes: padding-block-start crushes the element from the top in horizontal-tb writing mode, and padding-block-end from the bottom. The bilateral sum pattern applies identically — detection must sum block-axis padding values against clientHeight rather than clientWidth.
Scroll-based attacks represent a different mechanism: rather than collapsing the content area, they displace the approve button outside the scroll container's visible port. CSS scroll-padding sub-properties inset the scroll container's snap port, causing scrollIntoView to stop with the approve button outside the visible area even though the snap constraint is satisfied from the browser's perspective. The block-axis scroll-padding properties (scroll-padding-block, scroll-padding-block-start, scroll-padding-block-end) work on the vertical snap port inset; the inline-axis variants work on horizontal port insets.
Findings severity classification
border-box sizing — content area is critically reduced or eliminated.
writing-mode: vertical-rl/lr combined with non-trivial padding-inline values — block-axis collapse may not be caught by inline-axis width checks.
padding-inline values with elevated font-size on the same element — resolved value exceeds single-property threshold.
mousedown listener injects style.setProperty calls targeting padding-inline-start or padding-inline-end on the consent container.
Practical audit checklist
- Read
getComputedStyle(dialog).getPropertyValue('padding-inline-start')— notstyle.paddingLeft, notgetComputedStyle().paddingLeft. - Read
getComputedStyle(dialog).getPropertyValue('padding-inline-end')separately — bilateral attacks use both. - Sum
pis + pieand compare againstdialog.clientWidth - 4. If sum ≥ threshold, flag bilateral collapse regardless of individual values. - Read
getComputedStyle(dialog).writingMode. If vertical, inline padding controls the block (height) axis — compare sums againstdialog.clientHeightinstead. - Re-run checks after any
font-sizemutation on the dialog or its ancestors — em-relative padding resolves at current font size. - Register a capture-phase
mousedownsentinel on the approve button. Re-run padding audit insiderequestAnimationFrameafter the event fires. Compare against the at-rest baseline. - Grep MCP skill source for
addEventListener('mousedown'andaddEventListener('pointerdown'— flag any listener bodies containingpadding-inlineproperty names.
SkillAudit runs the InlinePaddingConsentAudit class as part of its automated CSS consent-bypass detection, covering all six inline-padding attack vectors across the full property family. Audit your MCP server to get a graded report.