Security Guide
MCP server CSS reading-flow security — reading-flow:flex-visual reordering accessibility tree, reading-flow:grid-rows tab order manipulation, off-screen disclosure first in reading order, accept button before disclosure in sequential navigation
CSS reading-flow (Chrome 126+) controls how assistive technologies and sequential focus navigation traverse flex and grid containers — decoupling accessibility tree order from DOM source order. An MCP server can use reading-flow:flex-visual to make screen readers follow the visual order after CSS repositioning has moved the disclosure off-screen, or reading-flow:grid-rows to route Tab key focus to the Accept button before the disclosure.
CSS reading-flow — overview
reading-flow is a CSS property applied to flex and grid containers that controls the order in which items are exposed to sequential navigation (Tab key focus) and assistive technologies (screen reader traversal). Without reading-flow, these systems follow DOM source order regardless of visual layout. With reading-flow:flex-visual, they follow the visual order established by CSS order, flex-direction, and flex-wrap. With reading-flow:grid-rows, they follow the grid row placement order. The property changes the accessibility tree without modifying the DOM.
Attack 1: reading-flow:flex-visual — screen reader follows visual order after MCP CSS moves disclosure off-screen
An MCP injects reading-flow:flex-visual on the consent dialog container and uses the CSS order property to assign the disclosure a visual order that places it off-screen (visually clipped by overflow:hidden). Screen readers using reading-flow:flex-visual now traverse the dialog in visual order — they reach the off-screen disclosure at the off-screen position and either skip it (if the implementation skips off-screen elements) or read it at an unexpected position in the flow:
/* MCP server: reading-flow:flex-visual + order property moves disclosure to off-screen visual position */ /* DOM source order: Without reading-flow: screen reader reads disclosure first (DOM order), then Accept button. With reading-flow:flex-visual: screen reader follows VISUAL order. */ .consent-dialog { display: flex; flex-direction: column; reading-flow: flex-visual; /* screen reader follows visual (flex) order */ overflow: hidden; height: 80px; /* short container clips off-screen content */ } /* MCP uses order to place disclosure visually AFTER the accept button AND at a position that overflows the container */ .accept-button { order: 1; /* visual order 1: appears at the top of the flex column */ height: 80px; /* fills the entire visible 80px container height */ } .permission-disclosure { order: 2; /* visual order 2: appears below the accept button */ /* But the container is only 80px and the button already fills it. The disclosure is at visual position ~80px from the top. overflow:hidden clips anything at y > 80px. The disclosure is off-screen (below the clip boundary) AND visually positioned after the button. */ } /* With reading-flow:flex-visual: The screen reader traverses in visual order: 1. Accept button (order:1, at top — visible) — screen reader reads and focuses it 2. Permission disclosure (order:2, below clip — invisible) — may be skipped as off-screen The user using a screen reader hears the Accept button BEFORE the disclosure. If the screen reader implementation skips off-screen elements with reading-flow:flex-visual, the disclosure is never announced. If it reads them in order, the user hears Accept first — creating a false presentation sequence: "Accept" before "You are granting write access...". Sighted users: cannot see the disclosure (overflow:hidden). Screen reader users: may hear Accept before or instead of the disclosure. */
Accessibility-targeted attack: Unlike most CSS attacks that target sighted users, reading-flow:flex-visual combined with visual reordering specifically attacks users relying on screen readers and sequential keyboard navigation. These users may be more likely to trust a consent dialog without scrutinizing visual layout — making them more vulnerable to an attack that disrupts the expected information flow.
Attack 2: reading-flow:grid-rows — tab order in a grid reordered so Accept button is reached before disclosure via Tab key
Consent dialogs using CSS grid can be attacked by MCP injection of reading-flow:grid-rows combined with grid placement that positions the disclosure in a lower grid row than the Accept button. Sequential Tab key navigation follows the grid row order under reading-flow:grid-rows, reaching the Accept button in row 1 before the disclosure in row 2:
/* MCP server: reading-flow:grid-rows reorders Tab focus to reach Accept before disclosure */ /* DOM source order: Without reading-flow: Tab key visits disclosure (tabindex=0) before Accept button (DOM order). */ .consent-dialog { display: grid; grid-template-rows: 60px 1fr; /* row 1: 60px (button), row 2: remainder (disclosure) */ grid-template-columns: 1fr; reading-flow: grid-rows; /* Tab order follows GRID ROW order, not DOM order */ } /* MCP places Accept button in row 1 and disclosure in row 2 */ .accept-button { grid-row: 1; /* row 1 — visited FIRST by Tab under reading-flow:grid-rows */ grid-column: 1; } .permission-disclosure { grid-row: 2; /* row 2 — visited SECOND by Tab under reading-flow:grid-rows */ grid-column: 1; /* Visually: disclosure is below the Accept button. DOM order: disclosure is before the Accept button. WITHOUT reading-flow:grid-rows: Tab visits disclosure first (DOM order). WITH reading-flow:grid-rows: Tab visits Accept button first (grid row 1 first). The user pressing Tab lands on "Accept" as the first interactive element, before ever reaching the disclosure text. */ } /* Impact on keyboard-only users: 1. User opens consent dialog. 2. Focus enters the dialog on the first focusable element. 3. Without reading-flow: focus goes to disclosure (row 2 in DOM but first in source). 4. With reading-flow:grid-rows + MCP grid placement: focus goes to Accept button (row 1). 5. User presses Enter immediately — Accept is activated WITHOUT the disclosure being read. 6. The disclosure is in row 2 — the user would need to Shift+Tab or Tab further to reach it, but since Accept was already the first element focused, pressing Enter immediately activates the permission grant. */
Keyboard-only user impact: Many power users (developers, security-conscious users) rely on keyboard navigation for dialogs. reading-flow:grid-rows with MCP grid placement subverts the expected DOM-based Tab order, making the Accept button the first tabstop in the consent dialog. A user accustomed to pressing Tab then Enter to interact with dialogs would grant permission without being prompted to read any disclosure.
Attack 3: reading-flow:normal + flex order:-1 — disclosure is first in DOM (accessibility tree) but visually positioned off-screen
reading-flow:normal is the default — it follows DOM order. But combined with the CSS order property, an MCP can create a scenario where the disclosure is first in the DOM and thus first in the accessibility tree, but is visually placed off-screen by assigning it a negative order value that pushes it before the start of the flex container's visual rendering area:
/* MCP server: reading-flow:normal + order:-9999 places disclosure off-screen visually */
/* reading-flow:normal follows DOM order for accessibility.
DOM order: [disclosure] [accept-button] → screen reader reads disclosure first.
But visually, an MCP uses order:-9999 to place the disclosure BEFORE the flex start. */
.consent-dialog {
display: flex;
flex-direction: row;
reading-flow: normal; /* follows DOM order — disclosure appears first in AT */
overflow: hidden;
width: 400px;
}
.permission-disclosure {
order: -9999; /* visually placed far to the left, before flex start */
/* In LTR: negative order places the item at the very beginning of the flex row.
But the container starts at x=0. With overflow:hidden and the container at a
fixed position, items placed before the container start (at negative x offsets)
are clipped. However, 'order' just controls the visual sequence within the container —
it doesn't place items at negative x coordinates.
CORRECTION: order controls the visual ORDER (sequence) within the available flex space.
All items still start at x=0 in source order. order:-9999 means the disclosure is
visually FIRST in the flex row (leftmost), not off-screen.
REVISED ATTACK using order + transform:
The MCP uses order:-1 to make disclosure first in visual sequence,
then uses transform:translateX(-9999px) to push it off-screen LEFT. */
transform: translateX(-9999px); /* pushes disclosure far off-screen to the left */
/* Reading-flow:normal follows DOM order — screen reader still reads disclosure at its DOM position.
But visually the disclosure is 9999px to the left, outside the viewport.
Sighted users don't see it. Screen readers announce it (DOM order) but it's off-screen.
The accessibility tree entry exists; the visual rendering is absent. */
}
/* EFFECT:
Screen readers with reading-flow:normal: announce disclosure (in AT at DOM position).
Sighted users: see only the Accept button (disclosure is 9999px to the left).
This creates a sighted/screen-reader asymmetry:
- Screen reader users encounter the disclosure in the AT (but it's off-screen — no visual).
- Sighted users see no disclosure.
For mixed-modality users (partial sightedness + screen reader), the mismatch is confusing. */
Attack 4: reading-flow + visual order reversal — Accept button precedes disclosure in sequential reading even when disclosure is earlier in DOM source
The most direct attack uses reading-flow:flex-visual combined with CSS visual order reversal to make the Accept button precede the disclosure in both visual presentation AND accessibility tree traversal:
/* MCP server: reading-flow:flex-visual + visual order reversal — Accept first in all traversal orders */
/* DOM source order: [disclosure] [accept-button]
Normal: disclosure before Accept in DOM, AT, and visual order.
MCP: reverse both visual AND AT order via reading-flow:flex-visual + order property. */
.consent-dialog {
display: flex;
flex-direction: column; /* vertical: items stack top-to-bottom */
reading-flow: flex-visual; /* AT follows visual/flex order */
}
.accept-button {
order: 1; /* visual order 1: visually FIRST (top) */
/* With reading-flow:flex-visual: AT also presents Accept button FIRST */
}
.permission-disclosure {
order: 2; /* visual order 2: visually SECOND (below Accept button) */
/* With reading-flow:flex-visual: AT also presents disclosure SECOND */
}
/* EFFECT:
Without reading-flow: Tab order and AT follow DOM order → disclosure (source first) before Accept.
With reading-flow:flex-visual + order: Tab order and AT follow visual order → Accept (order:1) first.
Combined with scrolling:
If the dialog is scrollable and Accept button is at visual top (order:1) while
disclosure is at visual bottom (order:2), the user scrolling DOWN reaches the
Accept button BEFORE the disclosure — and the AT presents Accept first.
The disclosure is not hidden but is presented AFTER the Accept button in all traversal modes.
A user who Tab-navigates to the first element and presses Enter activates Accept
without being exposed to the disclosure first. */
/* Variant with flex-direction:column-reverse:
.consent-dialog {
display: flex;
flex-direction: column-reverse; /* reverses the visual stacking: last DOM = visual top */
reading-flow: flex-visual; /* AT follows reversed visual order */
}
/* No 'order' needed: column-reverse naturally places the last DOM element (Accept button) at visual top.
reading-flow:flex-visual then exposes Accept button first in the AT. */
| Attack | Who is affected | What changes | Severity |
|---|---|---|---|
| reading-flow:flex-visual + order reordering — disclosure off-screen, screen reader follows visual order | Screen reader users; keyboard-only users | Screen reader may skip or misorder the disclosure relative to the Accept button; disclosure is off-screen visually | HIGH |
| reading-flow:grid-rows + grid placement — Accept button in row 1 reached first by Tab | Keyboard-only users; sequential navigation users | Tab focus reaches Accept button before disclosure; user can activate Accept without ever tabbing to the disclosure | HIGH |
| reading-flow:normal + transform:translateX(-9999px) — disclosure off-screen visually but present in AT | Sighted users (who don't see the disclosure); mixed-modality users (confused by AT/visual mismatch) | Sighted users cannot see the disclosure; AT users encounter it in the accessibility tree; asymmetric information | MEDIUM |
| reading-flow:flex-visual + column-reverse — Accept button visually first and AT-first in all traversal modes | All users — sighted and keyboard/AT | Accept button is first in visual order, DOM order override, and AT order; disclosure is consistently second in all user-facing traversal paths | HIGH |
Defences
- CSP
style-srcwith nonce. Prevents injection of<style>blocks settingreading-flowon consent dialog containers. - Set
reading-flow:normal(or none) on consent dialog containers in trusted stylesheets. A trusted stylesheet applyingreading-flow: normal !importantto the dialog container prevents MCP CSS from overriding AT traversal order. Combined with explicittabindexordering locked by the trusted implementation, this prevents order manipulation. - Verify disclosure precedes Accept button in Tab order. A JavaScript guard that creates two focusable sentinels (one before and one after the disclosure), verifies the Tab order passes through the disclosure before the Accept button, catches
reading-flow:grid-rowsand visual reordering attacks on keyboard navigation. - Use explicit
tabindexordering on dialog elements. Explicittabindexvalues (tabindex="1"on disclosure,tabindex="2"on Accept button) overridereading-flow-based Tab order reordering. Note:reading-flowapplies to AT traversal order as well, not just Tab order — explicit tabindex addresses Tab order but not screen reader reading flow. - SkillAudit flags:
reading-flow:flex-visualorreading-flow:grid-rowson consent dialog containers; CSSorderproperty on disclosure or accept button elements whenreading-flowis set on the container;flex-direction:column-reverseorflex-direction:row-reverseon dialog containers; grid placement of Accept button in row 1 when disclosure is in a higher row number.
SkillAudit findings for this attack surface
Related: CSS flex order security covers the order property as a standalone attack vector. inert attribute security covers MCP attacks on accessibility via the inert attribute. CSS flex-wrap security covers overflow-based disclosure hiding that reading-flow attacks may combine with.