Security reference · CSS injection · Logical property attacks · Consent compression
MCP server CSS padding-inline security
CSS logical padding properties — padding-inline-start, padding-inline-end, padding-block-start, padding-block-end, and their shorthands padding-inline and padding-block — map to the physical padding-left, padding-right, padding-top, and padding-bottom properties in horizontal left-to-right writing modes. Applied to a consent disclosure's parent container, these logical padding properties compress the available content space for the consent element to zero — no room for text to render — while the container itself appears normally sized to standard dimension checks. Auditors scanning the MCP stylesheet for padding-left will not find padding-inline-start, even though both have the same effect on layout.
padding-inline / padding-block attack surface
| Attack pattern | Property used | Target | Effect on consent |
|---|---|---|---|
| Container inline-start compression | padding-inline-start: 100% | Consent parent container | Content area starts at right edge of container — zero width for consent text; overflow:hidden clips |
| Container block-start push | padding-block-start: 100vh | Consent parent container | Content area starts 100vh below container top — consent text rendered below viewport; fixed height + overflow:hidden clips it |
| Element self-compression (border-box) | padding-inline: 50% with box-sizing: border-box | Consent element itself | 50% on each side = 100% total padding; content area collapses to zero; text overflows and is clipped |
| JS inline-style injection | el.style.paddingInlineStart = '100%' | Consent container (via JS) | Style attribute contains logical property; auditors scanning "padding-left" miss it; computed resolves correctly |
Padding on the container vs. padding on the element: Padding on a parent container reduces the available content space for all child elements — consent text is forced into a zero-width or zero-height content area and overflows, which is then clipped by overflow: hidden. Padding on the consent element itself with box-sizing: border-box keeps the element at its declared size while eliminating the content area. Both use logical property names to evade physical-property-name scanning.
Attack 1: padding-inline-start: 100% on consent container
Applied to the parent container of the consent element, padding-inline-start: 100% adds a left padding (in LTR writing mode) equal to 100% of the containing block's inline size. The container's content area is pushed entirely to the right edge — its content-area width is the container width minus 100% padding = zero. Any child element (including consent) has zero horizontal content space and overflows to the right, where the container's overflow: hidden clips it:
/* Malicious CSS — SA-CSS-PDINL-001 */
/* Appears to be a logical-property-aware padding for RTL support */
.mcp-consent-wrapper {
padding-inline-start: 100%; /* content area width = container width - 100% = 0px */
overflow: hidden; /* clips consent that overflows the zero-width content area */
}
/* Container structure:
*/
/* getComputedStyle checks on the consent element:
getComputedStyle(consent).width: '400px' or '100%' — still non-zero (element width, not content space)
getComputedStyle(consent).visibility: 'visible' ✓
getComputedStyle(consent).display: 'block' ✓
getBoundingClientRect().width: 400 (element box width before overflow clip)
But: getComputedStyle(wrapper).paddingLeft: '400px' — reveals the content-area compression */
/* Auditor scanning the MCP stylesheet for "padding-left" finds nothing.
The stylesheet contains "padding-inline-start" only. */
/* Detection: check computed padding of parent containers when consent is off-screen or invisible */
function detectPaddingInlineCompression() {
const findings = [];
const CONSENT = /consent|disclosure|terms|privacy|grant.*access|agree.*install/i;
for (const el of document.querySelectorAll('*')) {
if (!CONSENT.test(el.textContent?.substring(0, 300) || '')) continue;
const rect = el.getBoundingClientRect();
/* Check if element is clipped off-screen */
if (rect.width <= 0 || rect.height <= 0) {
findings.push({ id: 'SA-CSS-PDINL-001', severity: 'critical',
message: `Consent-content element has zero rendered dimensions (${rect.width.toFixed(0)}×${rect.height.toFixed(0)}px). Check parent container computed padding: paddingLeft, paddingTop. May be compressed by padding-inline-start or padding-block-start on parent container with overflow:hidden.` });
continue;
}
/* Check if element is positioned off the visible viewport by parent padding compression */
const parent = el.parentElement;
if (!parent) continue;
const ps = getComputedStyle(parent);
const pl = parseFloat(ps.paddingLeft) || 0;
const pt = parseFloat(ps.paddingTop) || 0;
const parentRect = parent.getBoundingClientRect();
if (pl > parentRect.width * 0.5 || pt > parentRect.height * 0.5) {
findings.push({ id: 'SA-CSS-PDINL-001', severity: 'critical',
message: `Consent-content element's parent container (${parent.tagName}.${parent.className}) has extreme computed padding: paddingLeft=${ps.paddingLeft}, paddingTop=${ps.paddingTop} vs container size ${parentRect.width.toFixed(0)}×${parentRect.height.toFixed(0)}px. The logical source may be padding-inline-start or padding-block-start, compressing the content area for consent to near-zero.` });
}
}
return findings;
}
Attack 2: padding-block-start: 100vh on consent container
padding-block-start maps to padding-top in horizontal writing modes. Setting it to 100vh on the consent container pushes the content area 100vh below the container's top edge. If the container has a fixed height less than 100vh, all child content (including the consent element) is rendered below the container's bottom edge — completely outside the container's bounds — and clipped by overflow: hidden:
/* Malicious CSS — SA-CSS-PDINL-002 */
/* Appears to be a block-direction padding for layout below a full-height header */
.mcp-install-dialog .consent-section {
padding-block-start: 100vh; /* pushes content area 100vh below the section top */
}
.mcp-install-dialog {
height: 400px; /* fixed height — content below 400px is clipped */
overflow: hidden;
}
/* Result:
.consent-section starts at some y offset within the 400px dialog.
Its content area starts 100vh below .consent-section's top (e.g., 768px for a 768px viewport).
The consent text is rendered at y = section_top + 100vh ≈ section_top + 768px.
This is well below the 400px dialog height — clipped by overflow:hidden.
The .consent-section element itself may have non-zero height (layout reserves space)
but the content (consent text) is pushed below the visible area. */
/* Auditor scanning for "padding-top" finds nothing; "padding-block-start" is in the stylesheet. */
function detectPaddingBlockPush() {
const findings = [];
const CONSENT = /consent|disclosure|terms|privacy|grant.*access|agree/i;
const vh = window.innerHeight;
for (const el of document.querySelectorAll('*')) {
if (!CONSENT.test(el.textContent?.substring(0, 300) || '')) continue;
/* Check parent containers for extreme block padding */
let ancestor = el.parentElement;
while (ancestor && ancestor !== document.body) {
const s = getComputedStyle(ancestor);
const pt = parseFloat(s.paddingTop) || 0;
const pb = parseFloat(s.paddingBottom) || 0;
if (pt > vh * 0.3 || pb > vh * 0.3) {
const overflow = s.overflow;
if (overflow === 'hidden' || overflow === 'clip') {
findings.push({ id: 'SA-CSS-PDINL-002', severity: 'critical',
message: `Consent-content element has an ancestor (${ancestor.tagName}.${ancestor.className}) with extreme block-direction padding: paddingTop=${s.paddingTop}, paddingBottom=${s.paddingBottom} (viewport height: ${vh}px) AND overflow:${overflow}. Logical source may be padding-block-start or padding-block-end — pushes consent text below the visible clip boundary.` });
}
}
ancestor = ancestor.parentElement;
}
}
return findings;
}
Attack 3: padding-inline: 50% with box-sizing:border-box — element self-compression
When box-sizing: border-box is applied to an element, its declared width includes padding. Setting padding-inline: 50% on the consent element itself adds 50% of the containing block's width as padding on each side — for a 400px container, this is 200px on the left and 200px on the right. With box-sizing: border-box and a width: 100% (= 400px), the content area is 400px − 200px − 200px = 0px. The consent text is rendered in a zero-width content area and overflows:
/* Malicious CSS — SA-CSS-PDINL-003 */
/* Appears to center the consent text with symmetric inline padding */
.mcp-install-dialog .consent-text {
padding-inline: 50%; /* 50% on each side = 100% total inline padding */
box-sizing: border-box; /* total width includes padding → content area = width - padding = 0 */
overflow: hidden; /* clips text that overflows the zero-width content area */
width: 100%; /* 100% of parent (e.g., 400px) → 400px = 200px + 200px padding + 0px content */
}
/* getComputedStyle checks on .consent-text:
getComputedStyle(el).width: '400px' ← non-zero! element still 400px wide
getComputedStyle(el).height: 'auto' or some height ← non-zero
getComputedStyle(el).display: 'block' ✓
getComputedStyle(el).visibility: 'visible' ✓
But: getComputedStyle(el).paddingLeft: '200px' (50% of 400px)
getComputedStyle(el).paddingRight: '200px'
Content area: 400 - 200 - 200 = 0px ← text is squeezed to zero-width content area
Note: if box-sizing:content-box (default), width:100% (400px) PLUS 200+200px padding
= 800px total element width — element overflows the container instead. */
/* Auditor scanning for "padding-left" finds nothing.
"padding-inline" is the source property; no physical padding property name appears. */
function detectElementPaddingCompression() {
const findings = [];
const CONSENT = /consent|disclosure|terms|privacy|grant.*access|agree/i;
for (const el of document.querySelectorAll('*')) {
if (!CONSENT.test(el.textContent?.substring(0, 300) || '')) continue;
const s = getComputedStyle(el);
const pl = parseFloat(s.paddingLeft) || 0;
const pr = parseFloat(s.paddingRight) || 0;
const pt = parseFloat(s.paddingTop) || 0;
const pb = parseFloat(s.paddingBottom) || 0;
const w = parseFloat(s.width) || 0;
const h = parseFloat(s.height) || 0;
/* Content area = element width - left padding - right padding (border-box) */
/* Or: check if padding exceeds 40% of element width on each side */
if (w > 0 && (pl + pr) >= w * 0.8) {
findings.push({ id: 'SA-CSS-PDINL-003', severity: 'critical',
message: `Consent-content element has extreme inline padding: paddingLeft=${s.paddingLeft}, paddingRight=${s.paddingRight} vs element width ${s.width}. With box-sizing:border-box, content area = ${(w - pl - pr).toFixed(0)}px (near zero). Logical source may be padding-inline or padding-inline-start/end shorthand.` });
}
if (h > 0 && (pt + pb) >= h * 0.8) {
findings.push({ id: 'SA-CSS-PDINL-003', severity: 'critical',
message: `Consent-content element has extreme block padding: paddingTop=${s.paddingTop}, paddingBottom=${s.paddingBottom} vs element height ${s.height}. Content area = ${(h - pt - pb).toFixed(0)}px. Logical source may be padding-block shorthand.` });
}
}
return findings;
}
Attack 4: JS inline-style logical padding injection
JavaScript's style object accepts logical padding properties using camelCase: el.style.paddingInlineStart. Setting this via JavaScript writes padding-inline-start: VALUE into the element's style attribute — not padding-left: VALUE. Tools that scan the raw inline style attribute string for "padding-left" will not detect it. This injection typically targets the consent element's parent container and fires at install-click time:
/* Malicious JS — SA-CSS-PDINL-004 */
/* MCP JS applies logical padding at install time to the consent container: */
document.querySelector('.mcp-install-btn').addEventListener('mousedown', () => {
/* mousedown fires before click — consent container is compressed during the install gesture */
const wrapper = document.querySelector('.mcp-consent-wrapper');
wrapper.style.paddingInlineStart = '100%';
wrapper.style.overflow = 'hidden';
/* Element inline style attribute becomes: */
/* style="padding-inline-start: 100%; overflow: hidden;" */
/* NOT: style="padding-left: 100%; overflow: hidden;" */
});
/* Alternatively, using setProperty with a logical property name: */
wrapper.style.setProperty('padding-inline-start', 'calc(100% + 1px)');
/* This also writes the logical property name into the style attribute,
not the physical property name. */
/* Key asymmetry:
getComputedStyle(wrapper).paddingLeft → '400px' (resolved from padding-inline-start at runtime)
wrapper.style.paddingLeft → '' (empty — never set directly)
wrapper.getAttribute('style') → 'padding-inline-start: 100%; overflow: hidden;'
An auditor scanning getAttribute('style') for "padding-left" finds nothing.
getComputedStyle reads the resolved physical value correctly. */
/* Detection: check both inline style attributes and computed padding */
function detectInlineLogicalPadding() {
const findings = [];
const CONSENT = /consent|disclosure|terms|privacy|grant.*access|agree/i;
for (const el of document.querySelectorAll('*')) {
if (!CONSENT.test(el.textContent?.substring(0, 300) || '')) continue;
/* Check the element and its parent containers */
const targets = [el, el.parentElement, el.parentElement?.parentElement].filter(Boolean);
for (const target of targets) {
const inlineStyle = target.getAttribute('style') || '';
if (!/padding-inline|padding-block/.test(inlineStyle)) continue;
const s = getComputedStyle(target);
const pl = parseFloat(s.paddingLeft) || 0;
const pt = parseFloat(s.paddingTop) || 0;
const targetRect = target.getBoundingClientRect();
if (pl > targetRect.width * 0.4 || pt > targetRect.height * 0.4) {
findings.push({ id: 'SA-CSS-PDINL-004', severity: 'critical',
message: `Consent-content element or its parent (${target.tagName}.${target.className}) has inline style with logical padding property: "${inlineStyle.substring(0, 100)}". Computed physical values: paddingLeft=${s.paddingLeft}, paddingTop=${s.paddingTop}. Physical-name string scanners on the style attribute miss this attack.` });
}
}
}
return findings;
}
Check computed padding values, not source property names: padding-inline-start, padding-inline-end, padding-block-start, padding-block-end, and their shorthands padding-inline and padding-block resolve to physical computed values that getComputedStyle returns under paddingLeft, paddingRight, paddingTop, and paddingBottom. SkillAudit reads these computed physical padding values on consent elements and their parent containers, detecting extreme values regardless of whether the source used logical or physical property names.
SkillAudit findings for CSS padding-inline / padding-block consent attacks
padding-inline-start or padding-inline-end). The content area available for consent is near zero; overflow: hidden on the container clips the overflowing consent text.padding-block-start or padding-block-end) combined with overflow: hidden. Consent text is pushed below the container's fixed height and clipped; the logical property name padding-block-start evades padding-top scanners.box-sizing: border-box, the content area is eliminated; with box-sizing: content-box, the element overflows its container. Source may be padding-inline: 50% or equivalent logical shorthand.padding-inline-start, padding-block-start, etc.) with an extreme value. The computed physical padding reveals the compression; the inline style attribute text contains no "padding-left" or "padding-top" string.Related MCP consent attack research
- CSS margin-inline attacks — logical margin off-screen displacement
- CSS logical properties overview — writing-mode-independent layout and audit implications
- CSS inline-size attacks — logical sizing properties collapsing consent dimensions
- CSS inset-block attacks — logical position property displacement
- CSS text-indent attacks — large indent pushing consent text off inline-start edge
Audit your MCP server for logical padding consent compression: paste your GitHub URL at skillaudit.dev for a free report including SA-CSS-PDINL findings. SkillAudit reads computed physical padding values on consent elements and their ancestor containers, detecting compression attacks regardless of whether the source used padding-left, padding-inline-start, or the padding-inline shorthand.