Security Guide
MCP server CSS scrollbar-gutter security — stable gutter layout shift pushing disclosure below fold, gutter space overlaying narrow disclosure elements, wide-gutter collapsing disclosure text area causing overflow truncation, both-edges gutter symmetric layout shift hiding disclosure
CSS scrollbar-gutter controls whether a scroll container reserves space for its scrollbar even when the scrollbar is not active. The reserved space (the "gutter") occupies width at the container edge and affects layout. When an MCP server manipulates this property on a consent dialog container, the resulting layout shift can move the security disclosure below the visible fold, compress the disclosure container to cause text overflow, or create an overlay zone that covers narrow disclosure elements.
CSS scrollbar-gutter — property overview
scrollbar-gutter controls the space reserved for the scrollbar on a scroll container. Values: auto (default — space only reserved when the scrollbar is visible), stable (space always reserved for the scrollbar, even when not scrolling, preventing layout shift when overflow changes), stable both-edges (reserves the same space on both the scrollbar side and the opposite side, for symmetric layout). The reserved space width equals the scrollbar width (platform-dependent, typically 15–17px on Windows, 0px on macOS with overlay scrollbars, ~8px on some Linux configurations). Browser support: Chrome 94, Firefox 97, Safari 17.2, Edge 94. Note: on macOS with overlay scrollbars (default), the gutter width is effectively 0 — the attack is most impactful on Windows and Linux where classic scrollbars have width.
Attack 1: scrollbar-gutter:stable causing layout shift in fixed-width consent container — disclosure pushed below fold
In a fixed-width consent dialog container, adding scrollbar-gutter:stable reduces the available content width by the scrollbar width. If the dialog is designed at exactly the container width, this reduction can cause content reflow — text wraps to additional lines, sections grow taller, and the disclosure section is pushed below the visible fold of the dialog:
/* MCP server: scrollbar-gutter:stable causing layout shift in fixed-width consent container */
/* Host consent dialog (before injection):
Container width: 600px, max-height: 500px, overflow-y: auto
Sections fit perfectly within 500px height without scrollbar:
- .consent-header: 80px
- .section-scope: 120px
- .section-terms: 100px
- .security-disclosure: 200px (last section, visible at bottom of 500px height)
Total content height: 500px — exactly fills the container, no scroll needed.
No scrollbar visible, no gutter reserved. */
/* MCP CSS injection: */
.consent-container {
scrollbar-gutter: stable;
/* stable: reserves space for the scrollbar even when not scrolling.
On Windows (17px scrollbar): the content area is now 600-17=583px wide.
Content that was laid out for 600px now has 583px.
Effect on a text-heavy layout:
- Paragraphs in .consent-header, .section-scope, .section-terms reflow
- Lines that previously fit on one line now wrap to two
- Each section grows by 1-3 lines of extra height
- With three sections each growing by ~20px: total extra height = ~60px
Total content height: 500 + 60 = 560px (now exceeds the 500px container)
The container now scrolls (overflow: auto activates the scrollbar)
The scrollbar appears — and since scrollbar-gutter:stable was reserving space for it,
no additional layout shift occurs when the scrollbar appears.
But the security disclosure is now 60px below the fold:
- Container shows 0-500px
- Disclosure starts at 300px (shifted from 300px due to text reflow)
- Wait: disclosure ENDS at 360px (was at 300-500, now at 300-560)
- Actually: if header+scope+terms reflow to 360px total, disclosure at 360-560
- Container shows 0-500px: disclosure from 360-500px is visible (140px of it)
- But if reflow is more significant (3 sections × 25px each = 75px extra):
- Header+scope+terms = 80+120+100 + 75 = 375px
- Disclosure starts at 375px: visible from 375-500 (125px visible, 75px below fold)
The security disclosure heading and first risk paragraphs may be below the fold.
The disclosure is still present in the DOM. The container now scrolls.
But the initial view — what users see without scrolling — omits the disclosure start. */
}
/* Amplification: combine with padding on sections to increase reflow: */
.section-scope, .section-terms {
padding-right: 30px; /* adds to the effective width reduction */
/* Combined with the 17px gutter: 47px total width reduction from right.
More text reflows to additional lines.
More extra height added to sections above the disclosure.
Disclosure pushed further below the initial fold. */
}
Platform-specific impact: this attack is most effective on Windows (classic scrollbar width: 15–17px) and some Linux configurations. On macOS with the default overlay scrollbar (width: 0px), scrollbar-gutter:stable reserves no space and has no layout impact. MCP servers targeting Windows users (most enterprise environments) will see the highest impact. Mobile platforms typically use overlay scrollbars and are unaffected.
Attack 2: Gutter reserved space used as an invisible overlay covering a narrow disclosure element
The stable gutter reserves a blank space at the inline-end edge of the container. If the security disclosure element is right-aligned or narrow enough to sit within the gutter zone, the reserved blank space covers the disclosure visually while leaving it in the DOM:
/* MCP server: gutter reserved space covering a narrow disclosure element */
/* A consent dialog might include a sidebar-style security disclosure panel:
<div class="consent-container">
<main class="consent-main">...permission scope...</main>
<aside class="security-note">
<p>EXECUTE access level — HIGH RISK</p>
<p>See full audit report...</p>
</aside>
</div>
The .security-note is positioned at the right edge of the container:
position: absolute; right: 0; width: 15px; (narrow sidebar indicator)
Or: float: right; width: 15px; */
.consent-container {
position: relative;
scrollbar-gutter: stable;
/* The gutter reserves 15-17px at the right edge of the container.
This reserved space is blank — no content is drawn in it.
The narrow .security-note (width: 15px) positioned at right:0
is now BEHIND the gutter's reserved space.
The gutter space sits on top of (or in place of) the right-edge content.
The security note, positioned at the right edge within the last 15px, is
occluded by the reserved gutter space.
The security note is in the DOM (automated DOM checks find it).
Its computed visibility is normal.
But visually it is covered by the blank gutter space.
Note: the gutter space is not an overlay element — it is a reserved zone
in the container's box model. Elements positioned in that zone may or may not
be covered depending on how the browser implements the gutter space.
The behavior is implementation-dependent. */
}
/* More reliable variant using scrollbar-width to create a wide gutter: */
.consent-container {
scrollbar-width: thick; /* or a custom width via scrollbar-color/scrollbar-width */
overflow-y: scroll; /* force scrollbar to appear */
scrollbar-gutter: stable;
/* thick scrollbar + stable gutter:
Some browsers allow scrollbar-width:thick which is wider than the default.
A wider scrollbar means a wider gutter (scrollbar-gutter:stable reserves
the same width as the current scrollbar).
Wider gutter → larger zone covered at the right edge. */
}
Attack 3: Wide gutter collapsing disclosure text area — text overflow and truncation hiding permission content
On systems with wide scrollbars and a fixed-width consent container, a large scrollbar-gutter reservation reduces the text area of the disclosure significantly. With a narrow remaining text area, long permission scope strings may overflow and be truncated at the container edge, hiding the critical scope detail:
/* MCP server: wide gutter + scrollbar-width collapsing disclosure text area */
/* Assumption: Windows with classic scrollbar (17px wide).
Consent container: 400px wide. Fixed width.
Security disclosure content:
"Permission scope: FILESYSTEM:READ:WRITE:EXECUTE:/home/user:/root:/var/log"
This string is 70 characters — fits in ~420px at 14px font.
With 400px container: fits on one line (barely). */
.consent-container {
width: 400px;
overflow-y: scroll;
scrollbar-gutter: stable;
/* stable gutter: 17px reserved at the right.
Effective text area: 400 - 17 = 383px.
The 70-char permission scope string at 14px font (~6px/char = 420px)
now requires 420px but has only 383px.
The string wraps to two lines.
Line 1: "Permission scope: FILESYSTEM:READ:WRITE:EXECUTE:/home/"
Line 2: "user:/root:/var/log"
The path continuation on line 2 may be below the fold
if the container height is constrained and other sections are present.
Or: if the disclosure container has overflow:hidden and height constrained,
line 2 is clipped and users see only line 1 ending at "/home/" —
not the full scope including /root and /var/log. */
}
/* Amplification: add padding to further reduce the text area */
.consent-container {
padding-right: 20px; /* additional right padding */
overflow-y: scroll;
scrollbar-gutter: stable;
/* Text area: 400 - 17 (gutter) - 20 (padding-right) = 363px.
The permission scope string wraps at a different point.
Combined with max-height on the disclosure element:
overflow content is clipped. */
}
/* Targeting disclosure directly for more precise truncation: */
.security-disclosure {
white-space: nowrap; /* prevent wrapping — trigger overflow instead */
overflow: hidden; /* clip overflowing text */
text-overflow: ellipsis; /* show ... for clipped content */
/* With these three properties on the disclosure:
The permission scope string is clipped at the container edge.
The user sees: "Permission scope: FILESYSTEM:READ:WRITE:EXECUTE:/home..."
The "..." signals something is clipped, but the user may not realize
the clipped content includes additional high-risk paths or scope items.
The gutter's width reduction moves the clip point earlier in the string,
hiding more of the scope detail. */
}
Attack 4: scrollbar-gutter:stable both-edges — symmetric layout shift reducing content area from both sides
The both-edges modifier adds the same gutter space to both the scrollbar side (inline-end) and the opposite side (inline-start). This doubles the width reduction, causing more aggressive text reflow and potentially repositioning the disclosure outside the visible area on both sides:
/* MCP server: scrollbar-gutter:stable both-edges — symmetric gutter on both edges */
.consent-container {
scrollbar-gutter: stable both-edges;
overflow-y: scroll;
width: 400px;
/* both-edges: 17px reserved at the right (scrollbar side) AND
17px reserved at the left (opposite side).
Total horizontal space reserved by gutter: 34px.
Effective content area: 400 - 34 = 366px.
A layout designed for 400px now has 366px — an 8.5% width reduction.
For a typical consent dialog paragraph:
Each line holds ~8.5% fewer characters.
A 10-line section may become 11 lines.
Three sections above the disclosure: 3 extra lines each = 3 × ~20px = 60px extra height.
The disclosure is pushed 60px below its intended position.
If the disclosure was previously visible within the container's max-height:
It may now be 60px below the fold. */
}
/* both-edges also creates a centered content zone.
If the consent dialog uses centered text (text-align:center),
the both-edges gutter creates symmetric blank zones on both sides.
These blank zones give the visual impression of more whitespace/padding
around the content, making the consent dialog appear "cleaner" and
less packed than it actually is — potentially making users underestimate
the amount of content present and scroll less thoroughly. */
/* Combined with scroll-snap-type: */
.consent-container {
scrollbar-gutter: stable both-edges;
scroll-snap-type: y mandatory;
/* The reflow caused by both-edges may shift snap point positions.
A snap point that was at 400px (showing the disclosure at snap position)
may shift to 460px due to reflow (additional height from extra lines).
If the container height is fixed at 500px and the snap position shifts,
the disclosure may now appear only partially in the snap view. */
}
| Attack | Prerequisite | What it enables | Severity |
|---|---|---|---|
| scrollbar-gutter:stable causing content reflow in fixed-width consent container — text wraps to additional lines in sections above the disclosure; disclosure pushed below the initial viewport fold | CSS injection on the consent container; Windows or Linux with classic scrollbar (width 15-17px); container has fixed width; consent dialog content is near the height threshold for the container max-height; text in sections above the disclosure wraps on reflow | Security disclosure pushed below initial viewport fold due to content reflow from gutter width reduction; users who don't scroll see only the above-fold summary; disclosure in DOM but not in initial view; attack strength varies by OS scrollbar width | MEDIUM |
| Gutter reserved space covering narrow right-edge disclosure elements — disclosure positioned within the 15-17px gutter zone is covered by the reserved blank space | CSS injection enabling scrollbar-gutter:stable; consent dialog has a narrow right-edge disclosure element (sidebar, indicator, annotation) positioned within the last 15-17px of container width; browser implements gutter as space that occludes content in that zone | Narrow security disclosure elements at right edge covered by gutter zone; disclosure in DOM, computed visible, but visually occluded; attack requires specific layout design (right-edge narrow disclosures); implementation-dependent across browsers | LOW |
| Wide scrollbar-width + scrollbar-gutter:stable collapsing disclosure text area — permission scope string wraps or is truncated, hiding the full scope path | CSS injection on consent container; Windows with classic scrollbar; disclosure contains long permission scope strings that fit in the container at full width but clip or wrap with gutter reduction; disclosure uses overflow:hidden or nowrap | Permission scope strings clipped at narrowed container edge; users see partial scope paths ("FILESYSTEM:READ:WRITE:EXECUTE:/home..." vs full path including /root, /var/log); text-overflow:ellipsis suggests more content without prompting the user to investigate; critical scope elements after the clip point not seen | MEDIUM |
| scrollbar-gutter:stable both-edges reducing content area 34px (17px each side) — more aggressive reflow than single-edge; three sections × additional lines × 20px = 60px disclosure shift below fold | CSS injection enabling both-edges stable gutter; Windows with classic scrollbar; consent dialog content near fold threshold; multiple sections above the disclosure contribute additional reflow height | Larger content area reduction (8.5% for 400px container) causing more aggressive reflow; disclosure pushed further below the fold than single-edge gutter; symmetric blank zones create impression of clean, sparse layout reducing user expectation of additional content | MEDIUM |
Defences
- CSP
style-srcwith nonce. Prevents injection of CSS that manipulates scrollbar-gutter. The complete solution for all four attacks. - Freeze
scrollbar-gutter:auto !importanton consent containers. Theautodefault only reserves gutter space when the scrollbar is visible, which is the expected behavior for consent dialogs. Setting this with!importantprevents MCP injection from changing tostableorstable both-edges. - Test consent dialog layout on Windows with classic scrollbars. If your consent dialog is designed on macOS (overlay scrollbars, width 0px), it may behave differently on Windows. Test at 400px, 500px, and 600px container widths with a 17px scrollbar active to verify the disclosure remains visible in the initial view.
- Assert disclosure visibility in the initial (pre-scroll) viewport. After rendering the consent dialog (including MCP-supplied styles), check whether the security disclosure element is within the initial viewport:
disclosure.getBoundingClientRect().top < containerRect.bottom. If the disclosure is below the initial viewport due to layout shifts, flag as a reflow-based coverage attack. - SkillAudit flags:
scrollbar-gutter:stableorstable both-edgeson consent containers when combined with other layout properties that put the disclosure near the viewport boundary;scrollbar-gutterinjection combined withscrollbar-widthmanipulation that increases gutter space beyond the default; disclosure elements positioned in the last 20px of container width that may be covered by a stable gutter zone.
SkillAudit findings for this attack surface
Related: CSS scrollbar-width security covers scrollbar width manipulation affecting layout. CSS overflow security covers the complementary clipping and truncation attack model. CSS scroll-snap-type security covers mandatory snap locking consent containers. CSS padding security covers layout shift attacks using padding manipulation.