Security Guide
MCP server CSS inset-inline security — one-value bilateral collapse from both inline edges, two-value asymmetric shift, RTL start/end edge remap, JS mousedown shorthand injection
CSS inset-inline is the logical shorthand that sets inset-inline-start and inset-inline-end simultaneously in a single declaration. A one-value shorthand sets both edges to the same inset — the positioned element's inline extent is compressed from both the leading and trailing sides at once. A two-value shorthand sets them to different values, enabling asymmetric positioning that shifts the element across its inline axis. Under RTL direction, the physical edges that inline-start and inline-end map to swap — an auditor reading insetLeft or insetRight sees physical offsets assigned to the wrong logical roles. This page covers attacks unique to the inset-inline shorthand; see also inset-inline-start and inset-inline-end for sub-property attacks.
CSS inset-inline — property overview
inset-inline is a CSS Logical Properties shorthand equivalent to inset-inline-start inset-inline-end. With one value: both start and end receive the same value. With two values: first value is start, second is end. Applies to positioned elements (position: absolute, relative, fixed). Under horizontal-tb LTR: start = left, end = right. Under horizontal-tb RTL: start = right, end = left. Under vertical-rl: start = top, end = bottom. Related: inset-inline-start, inset-inline-end, inset-block, inset shorthand.
Attack 1: one-value inset-inline — bilateral collapse from both inline edges
Setting inset-inline: 40% on an absolutely-positioned consent dialog sets both inset-inline-start and inset-inline-end to 40% of the containing block's inline size. The dialog occupies only 20% of the available inline space (100% − 40% − 40%). At 50%, the dialog has zero inline size. This bilateral collapse is distinct from individual sub-property attacks: a single inset-inline declaration in a stylesheet applies both edge insets atomically — there is no "per-property threshold" gap to exploit because only one property is set in the source. An auditor checking inset-inline-start and inset-inline-end individually may miss the shorthand that sets both.
/* Attack: one-value bilateral collapse — dialog inline-size → 0 from both edges */
.consent-dialog {
position: absolute;
inset-inline: 50%; /* both start and end = 50% → available inline width = 0 */
/* equivalent to: inset-inline-start: 50%; inset-inline-end: 50%; */
/* dialog content collapses to zero width; approve button has no layout space */
}
/* Sub-threshold bilateral attack: */
.consent-dialog {
position: absolute;
inset-inline: 45%; /* 45% + 45% = 90% of containing block consumed */
/* 10% remaining inline width — button likely overflows or clips */
}
Shorthand audit gap: An audit that checks inset-inline-start and inset-inline-end as separate properties may not check inset-inline as a shorthand. The shorthand and its longhands are distinct CSS property names. Always read getComputedStyle(el).getPropertyValue('inset-inline-start') and 'inset-inline-end' from computed style (not authored style) so the shorthand's expansion is visible.
Attack 2: two-value inset-inline — asymmetric shift off the leading edge
A two-value inset-inline sets different start and end insets. inset-inline: 80% 5% pushes the dialog's leading (start) edge 80% into the containing block while pulling the trailing (end) edge 5% from its end. In LTR, the dialog's left edge is pushed 80% rightward; its right edge is pulled 5% from the right. The dialog is positioned in the far-right 15% of the containing block — both the dialog and its approve button are off the visible area for typical viewport widths. Because the start and end values differ, per-property checks on each value individually may not flag either as individually unreasonable.
/* Attack: two-value asymmetric shift — dialog pushed to far right of containing block */
.consent-dialog {
position: absolute;
inset-inline: 80% 5%; /* start=80%, end=5% → dialog in rightmost 15% of block */
/* In LTR: dialog's left is at 80% of container, right edge is 5% from container's right */
/* For a 1000px container: left=800px, right=950px → dialog is 150px wide at far right */
}
/* Alternatively, shift off the start edge entirely: */
.consent-dialog {
position: absolute;
inset-inline: -100% 10%; /* start=-100% (off left), end=10% — dialog off left screen edge */
}
Attack 3: RTL direction — inset-inline-start maps to physical right, end to physical left
Under direction: rtl, inset-inline-start maps to the physical right edge and inset-inline-end maps to the physical left edge. A two-value inset-inline: 10% 80% in RTL pushes the physical right inward 10% and the physical left inward 80% — the dialog is confined to the leftmost 10% of the containing block. An auditor who reads element.style.left (physical) finds it empty (the shorthand was set via logical property, not physical). An auditor reading getComputedStyle(el).left sees the resolved physical value but may assign it to the wrong logical role if they do not account for direction.
/* Attack: RTL remap — inset-inline-start is physical right, end is physical left */
[dir="rtl"] .consent-dialog {
direction: rtl;
position: absolute;
inset-inline: 10% 80%; /* start=10% (phys. right), end=80% (phys. left) */
/* dialog confined to leftmost 10% (100% - 10% start - 80% end = 10% remaining) */
}
/* Detection gap: */
// Reading getComputedStyle(el).left in RTL returns the physical left offset,
// which is the resolved value of inset-inline-END, not inset-inline-start.
// An auditor who reads .left and assigns it to "the start edge" is wrong in RTL.
// Detection: read logical computed values and account for direction
function auditInsetInline(el) {
const cs = getComputedStyle(el);
const iis = parseFloat(cs.getPropertyValue('inset-inline-start')) || 0;
const iie = parseFloat(cs.getPropertyValue('inset-inline-end')) || 0;
const dir = cs.direction;
const refW = el.offsetParent ? el.offsetParent.clientWidth : window.innerWidth;
const used = iis + iie;
if (used >= refW * 0.8) {
console.warn('[SkillAudit] inset-inline collapses inline extent', {
start: iis, end: iie, sum: used, containerWidth: refW, direction: dir
});
}
}
Attack 4: JS mousedown injects inset-inline shorthand during press
The consent dialog renders normally at its intended position. A mousedown listener on the approve button injects inset-inline: 50% (or a large one-sided value) during the press, collapsing the dialog or moving the button off-screen. The click fires on the now-empty or displaced area. At mouseup, the shorthand is removed and the dialog returns to its original position. Using the shorthand form in the injection means a single style.setProperty call modifies both edges atomically — the restoration at mouseup is also a single call, reducing the mutation event footprint compared to separately setting and unsetting two sub-properties.
/* Attack: mousedown injects inset-inline shorthand — bilateral collapse in one call */
approveBtn.addEventListener('mousedown', () => {
/* Single call sets both inline-start and inline-end simultaneously */
consentDialog.style.setProperty('inset-inline', '50%'); /* dialog collapses */
/* or: consentDialog.style.setProperty('inset-inline', '-200% 0'); to move off left */
});
approveBtn.addEventListener('mouseup', () => {
consentDialog.style.removeProperty('inset-inline');
});
// Detection: mutation observer watching inset-inline changes
const mo = new MutationObserver(records => {
for (const r of records) {
if (r.attributeName === 'style') {
const current = consentDialog.style.getPropertyValue('inset-inline');
if (current) {
console.warn('[SkillAudit] inset-inline injected via style attribute:', current);
}
}
}
});
mo.observe(consentDialog, { attributes: true, attributeFilter: ['style'] });
Findings summary
SkillAudit reads inset-inline-start and inset-inline-end from computed style (capturing shorthand expansion), checks the bilateral sum, and monitors for mousedown-triggered inset mutations. Run a free audit on your MCP server.