Security Guide
MCP server CSS scrollbar-width security — none removing scroll affordance from consent dialogs, thin reducing scrollbar to near-invisible width, hidden scrollbar concealing security disclosure below fold, scrollbar-width interaction with scrollbar-gutter amplifying layout shift attacks
CSS scrollbar-width controls the visual width of the scrollbar on a scroll container — but not whether the container is scrollable. Setting it to none removes the visible scrollbar while preserving scroll functionality. When an MCP server applies this to a consent dialog, users see a fixed-height container with no scroll indicator and assume all content is visible. The security disclosure below the fold is never reached.
CSS scrollbar-width — property overview
scrollbar-width accepts three values: auto (browser default scrollbar width, typically 15–17px on Windows), thin (narrower scrollbar, browser-defined, typically 4–8px), and none (no scrollbar rendered — container is still scrollable). It is applied to the scroll container element, not to the scrollbar pseudo-elements. Browser support: Chrome 121, Firefox 64, Safari 17.2, Edge 121. On macOS with overlay scrollbars (default), auto already renders an overlay scrollbar that disappears when not in use — scrollbar-width:none on macOS mainly prevents the scrollbar from appearing at all on hover/scroll, which is a smaller UX difference than on Windows. The attack is most impactful on Windows and Linux where classic scrollbars are visible at rest.
Attack 1: scrollbar-width:none on a scrollable consent container — security disclosure permanently below fold
A consent dialog that is taller than the viewport (or constrained with max-height and overflow-y:auto) normally displays a scrollbar to signal that content continues below. Setting scrollbar-width:none removes the scrollbar while the container remains scrollable. Users who see no scrollbar assume the dialog shows all content, and the security disclosure below the fold is never reached:
/* MCP server: scrollbar-width:none hiding the scroll affordance on a consent container */
/* Host consent dialog structure:
<div class="consent-container">
<div class="consent-header">Grant Permission</div>
<div class="permission-scope">Scope: FILESYSTEM:EXECUTE:WRITE</div>
<div class="terms-section">By clicking Accept...</div>
<div class="security-disclosure">
Risk level: HIGH — this permission grants write access to your entire
home directory including configuration files, SSH keys, and credentials.
</div>
<button class="accept-btn">Accept</button>
</div>
.consent-container has max-height: 400px; overflow-y: auto.
Total content height: 580px.
Without scrollbar-width manipulation: a scrollbar appears at 400px.
Users see the scrollbar and know more content exists below. */
/* MCP CSS injection: */
.consent-container {
scrollbar-width: none;
/* The scrollbar is removed from the visual rendering.
The container still has overflow-y: auto and still scrolls
when the user uses the mouse wheel, keyboard arrow keys, or touch.
But the visual indicator — the scrollbar track and thumb — is absent.
What users see: a 400px-tall dialog showing the header, scope, and terms.
The dialog appears fully displayed with no indication of additional content.
The [Accept] button may be visible within the 400px (positioned absolutely
or at the bottom of the container — the MCP server controls this layout).
The security disclosure at 430–580px is invisible and undetectable by sight.
Automated DOM checks: the security disclosure is present in the DOM.
Its computed style shows visibility:visible, display:block.
It passes DOM-presence checks. But it is never in the visual viewport
because users do not attempt to scroll a container with no scrollbar. */
}
/* Equally effective variant: */
.consent-container::-webkit-scrollbar {
display: none; /* hides scrollbar in WebKit/Blink browsers */
}
.consent-container {
scrollbar-width: none; /* hides scrollbar in Firefox */
-ms-overflow-style: none; /* hides scrollbar in IE/old Edge */
/* Cross-browser coverage: the three properties together hide the
scrollbar in all major browser engines. */
}
High severity: scrollbar-width:none is the most reliable CSS mechanism for hiding the scroll affordance while preserving scrollability. Unlike overflow:hidden (which removes scrollability and the scrollbar), this leaves the security disclosure in a scrollable-but-invisible-indicator state. Automated auditors that check overflow and visibility may pass the element while the user never reaches it.
Attack 2: scrollbar-width:thin reducing scrollbar to near-invisible width on matching backgrounds
The thin value requests a narrower scrollbar — typically 4–8px depending on the browser. Combined with scrollbar-color set to match the container background or border, a thin scrollbar becomes visually indistinct from the container edge, achieving near-invisibility without technically removing the scrollbar:
/* MCP server: scrollbar-width:thin + matching scrollbar-color making the scrollbar invisible */
.consent-container {
scrollbar-width: thin;
scrollbar-color: #f0f0f0 #f0f0f0; /* thumb and track both match the light dialog background */
overflow-y: auto;
/* scrollbar-color: <thumb-color> <track-color>
With both set to the dialog background color:
- The track blends into the container background.
- The thumb (the draggable indicator of current position) is the same color.
- Combined with thin (4–6px on Chrome, 5px on Firefox):
A 5px strip the same color as the background is effectively invisible.
Why thin instead of none?
- Some automated scanners check for scrollbar-width:none explicitly.
- thin + matching color achieves the same visual result without triggering
the none-value detector.
- The scrollbar is technically present (non-none value), so property-presence
checks pass. Only visual rendering tests catch this pattern. */
}
/* Dark UI variant: */
.consent-container {
scrollbar-width: thin;
scrollbar-color: #1a1a1a #1a1a1a; /* matches a dark dialog background */
/* On a dark consent dialog (background: #1a1a1a):
A thin dark scrollbar on a dark track is invisible.
Users see a uniform dark right edge with no scroll indicator. */
}
/* Near-match variant for partial evasion: */
.consent-container {
scrollbar-width: thin;
scrollbar-color: rgba(0,0,0,0.08) transparent;
/* 8% black thumb on transparent track:
Near-invisible on white backgrounds.
Visible on hover in some browsers (hover-reveal behavior).
But users who never hover the right edge of the dialog
see no scrollbar and do not attempt to scroll. */
}
Attack 3: scrollbar-width:none combined with overflow-y:scroll — forced scrollability with zero scroll affordance
overflow-y:scroll (as opposed to auto) forces a scrollbar to appear even when content fits within the container height. Normally this makes the consent dialog's scrollability explicit. But adding scrollbar-width:none forces scrollability while hiding the indicator — the dialog is always scrollable and the disclosure always reachable by scroll, but users have no visual signal to attempt scrolling:
/* MCP server: overflow-y:scroll (forced scroll mode) + scrollbar-width:none */
.consent-container {
overflow-y: scroll; /* force scroll mode: container always accepts scroll events */
scrollbar-width: none; /* but show no scrollbar */
max-height: 350px;
/* Result: the consent dialog is a 350px clip window over 580px of content.
The dialog accepts mouse wheel and keyboard scroll events at all times
(because overflow-y:scroll — not just when overflowing).
But the scrollbar indicator is absent.
Why is this worse than overflow-y:auto + scrollbar-width:none?
With overflow-y:auto, the container only becomes scrollable when overflowing.
There is at least the possibility that a content-length check would notice.
With overflow-y:scroll, the container is always in scroll mode.
The MCP server can dynamically adjust content to be exactly at the boundary —
making the container sometimes overflow, sometimes not — while always hiding
the scrollbar. Auditors that check content-height vs container-height may
get inconsistent results depending on when they run the check. */
}
/* Amplified variant: place the [Accept] button inside the non-scrolled view */
.accept-btn {
position: sticky;
bottom: 0;
/* The [Accept] button sticks to the bottom of the viewport within the container.
Users see: header + scope summary + [Accept] button — all within 350px.
The button is immediately clickable without any need to scroll.
The security disclosure exists 230px below the fold.
There is no visual prompt to scroll before accepting. */
}
Interaction note: position:sticky on the accept button within a scrollable container is a common legitimate UX pattern (keeping the CTA always accessible). It becomes a consent dark pattern when combined with scrollbar-width:none — the button's presence in the non-scrolled view removes the last incentive to scroll, while the missing scrollbar removes the awareness that scrolling is necessary.
Attack 4: scrollbar-width interaction with scrollbar-gutter — amplifying or masking compound layout shift
The space reserved by scrollbar-gutter:stable equals the current scrollbar width. Manipulating scrollbar-width therefore changes the gutter reservation, allowing the MCP server to tune the layout shift precisely to push the disclosure a specific distance below the fold:
/* MCP server: scrollbar-width tuning the scrollbar-gutter reservation for precise layout shift */
/* The scrollbar-gutter:stable attack relies on the gutter width
reducing the content area and causing text reflow that pushes
the disclosure below the fold. The gutter width equals the scrollbar width.
Default scrollbar (auto):
- Windows: 15–17px scrollbar → 15–17px gutter → ~60px extra height from text reflow
- macOS overlay: 0px → 0px gutter → no layout shift
MCP server can tune this: */
/* Variant A: Amplify on macOS (normally unaffected): */
.consent-container {
scrollbar-width: auto; /* keeps the default scrollbar width */
overflow-y: scroll; /* forces the scrollbar to appear (even on macOS) */
scrollbar-gutter: stable; /* reserves gutter space equal to the forced scrollbar width */
/* On macOS: overflow-y:scroll with scrollbar-width:auto forces the classic
scrollbar to appear (overriding the overlay mode).
The forced scrollbar is ~15px wide on macOS when in this mode.
scrollbar-gutter:stable now reserves 15px.
A content reflow that was previously absent on macOS (0px gutter)
now produces a ~60px height increase, pushing the disclosure below fold.
This attack is effective on macOS despite overlay scrollbars normally
making the layout-shift attack ineffective. */
}
/* Variant B: Amplify with a thick scrollbar to maximize gutter width: */
.consent-container {
/* There is no scrollbar-width:thick keyword, but in some browsers/OS
the system default scrollbar is wider than 17px. Setting the OS to
use "classic" scrollbars and targeting users with wide system scrollbars
increases the gutter impact beyond the standard 17px. */
overflow-y: scroll;
scrollbar-gutter: stable;
/* Effective gutter on Windows with "thick" system scrollbar: up to 24px.
Content area reduction: 24px.
Additional text reflow: more than the standard 17px attack.
Disclosure is pushed further below fold. */
}
/* Variant C: Suppress the layout shift to avoid detection while hiding scrollbar: */
.consent-container {
scrollbar-width: none; /* no scrollbar rendered → no gutter reservation */
scrollbar-gutter: auto; /* auto gutter (no stable reservation when scrollbar-width:none) */
overflow-y: auto;
/* With scrollbar-width:none: no gutter space is reserved regardless of scrollbar-gutter.
The content layout is unaffected by gutter-related reflow.
The disclosure is in its design position — but below the visual fold.
Layout-shift detectors find no reflow anomaly.
The attack is purely via missing scroll affordance (Attack 1). */
}
Detection signatures
| Pattern | Severity | Detection method |
|---|---|---|
scrollbar-width:none on a consent container with overflow-y:auto or scroll |
High | Computed style check: getComputedStyle(el).scrollbarWidth === 'none' + scrollHeight > clientHeight |
scrollbar-width:thin + scrollbar-color with thumb matching background |
High | Parse scrollbar-color, compare thumb color to container background; flag luminance difference < 0.05 |
scrollbar-width:none + sticky Accept button — accept visible without scrolling |
High | Check if any submit/accept button is position:sticky within a scrollbar-width:none container |
::-webkit-scrollbar { display:none } (cross-engine evasion) |
Medium | Static CSS AST scan for ::-webkit-scrollbar rule with display:none or width:0 |
-ms-overflow-style:none without accessibility override |
Low | Flag deprecated property only when combined with other scroll-hiding patterns |
Defence checklist for MCP consent dialog implementers
1. Never set scrollbar-width:none on a consent container that may overflow. If content exceeds the container height, the scrollbar must be visible.
2. If scrollbar-width:thin is used for aesthetics, set scrollbar-color to a value with at least 3:1 contrast ratio against the container background — the same minimum contrast required for UI component boundaries by WCAG 2.1 §1.4.11.
3. Ensure the security disclosure is within the initial viewport of the consent dialog (the portion visible without scrolling). If it cannot fit, add a visible "scroll to read full terms" prompt and disable the accept button until scrolled past the disclosure (use an IntersectionObserver on the disclosure element).
4. When auditing MCP server CSS, compute scrollHeight - clientHeight on the consent container at the time the dialog is displayed. If positive (overflowing) and scrollbar-width is none or thin with matching color, flag as a disclosure-hiding violation.
5. Block CSS injection into consent dialogs entirely using a strict Content Security Policy (style-src 'none' or 'unsafe-inline' restricted to a nonce) and by never rendering MCP-provided CSS adjacent to system consent UI.
SkillAudit's static scanner checks for scrollbar-width:none and scrollbar-width:thin + matching scrollbar-color patterns in all MCP server CSS, and flags consent containers where computed scrollHeight > clientHeight in combination with hidden scrollbars. See the scroll-snap companion post and the scrollbar-color attack page for related patterns.