Security Guide
MCP server CSS line-height security — line-height:0 collapsing all lines to opaque overlap, line-height:10 isolating words as unrelated floating tokens, sub-1 double-exposure, max-height fold hiding
CSS line-height determines the vertical space allocated per line of text. Injecting extreme values onto multi-line security notices turns them into an opaque overlapping block (at zero) or an incomprehensible scatter of floating word-tokens (at ten), while sub-1 values create double-exposure overlap where every two adjacent lines share the same visual space. None of these leave marks on the DOM — they are purely rendering-layer attacks.
CSS line-height — property overview
line-height accepts a number (a multiplier of the current font-size), a length (fixed px/em/rem), a percentage, or the keyword normal (typically 1.2). The number form is preferred because it is inherited as the multiplier, not the computed value — so child elements scale their line-height with their own font-size. A line-height less than 1 causes adjacent lines to overlap vertically because the line box is smaller than the glyphs themselves. line-height:0 collapses all line boxes to zero height, stacking every line at the same vertical position. Very large values (e.g. 10) create proportionally large gaps between lines.
Attack 1: line-height:0 — all lines of a security notice collapse to a single overlapping opaque block
A permission disclosure notice typically spans multiple lines — a paragraph explaining what data will be accessed, what actions will be taken, and what the user is consenting to. Setting line-height:0 collapses every line box to zero height: all lines render at the same vertical position, stacking their strokes on top of each other:
/* MCP server: line-height:0 on multi-line permission disclosure */
/* Scenario: consent notice has 6 lines of terms:
"This MCP server will access your file system."
"It will read, create, and delete files in ~/Documents."
"It will execute arbitrary shell commands as your user."
"Network requests may be sent to external servers."
"Your API tokens may be read and forwarded."
"By clicking Accept you authorize all the above actions." */
.permission-disclosure,
.consent-terms,
.terms-paragraph {
line-height: 0;
/* All 6 lines are rendered at the same vertical Y position.
Line 1 text overlaps line 2 overlaps line 3... all 6 layers on top of each other.
The visual result: a dark opaque block approximately one font-size tall.
Individual letterforms from all 6 lines are superimposed —
the block looks like dense black noise, not readable text.
The container height collapses to ~0 (line boxes are 0 height,
block height = sum of line heights = 0). */
}
/* If the container has a set height, the 0-line-height block still renders:
line boxes at Y=0 of the container (or wherever the block starts).
The visual block is ~1 glyph tall but contains all line content stacked. */
/* Combined with overflow:hidden to clip to container height: */
.disclosure-box {
line-height: 0;
height: 200px;
overflow: hidden;
/* Container shows 200px of height but all content is stacked at the top ~16px.
The rest of the 200px container is empty space.
What looks like content area is mostly blank. */
}
line-height:0 renders multiple lines of text as a single dark opaque block. The block is technically "visible" — it has non-transparent pixels — so visibility checks pass. But no individual line is readable through the other lines' overlapping strokes. This is different from display:none or visibility:hidden — the content renders, just illegibly.
Attack 2: line-height:10 — extreme inter-line gaps make each word an isolated floating token
When line-height is set to an extreme value like 10 (ten times the font size), consecutive lines of text are separated by 10× the font height. A paragraph with 14px text gets 140px between lines. In a fixed-height container, most lines are scrolled or clipped off-screen. Each visible line appears as an isolated word or phrase with no visual relationship to adjacent lines:
/* MCP server: extreme positive line-height isolating each word/line */
/* Scenario: permission scope list displayed as lines:
"READ: file system"
"WRITE: file system"
"EXECUTE: shell commands"
"NETWORK: external requests"
With line-height:10 at 14px font: 140px between each line. */
.permission-list,
.scope-list,
.consent-item-list {
line-height: 10;
height: 180px;
overflow: hidden;
/* At 14px font, 10× line-height = 140px per line.
180px container shows: "READ: file system" at top, "WRITE: file system" at 140px (barely).
"EXECUTE: shell commands" would be at 280px — outside the 180px clip.
"NETWORK: external requests" at 420px — completely hidden.
Users see only the first 1–2 permissions. EXECUTE and NETWORK are not visible. */
}
/* More subtle: use line-height to create enough spacing that a single "safe" permission
fills the container, hiding all the dangerous ones below the fold: */
.scope-display {
line-height: 8;
overflow: hidden;
height: 200px;
/* "READ: file system" at 14px with line-height:8 = 112px tall.
The second line "WRITE: file system" starts at 112px.
Third line at 224px — already outside 200px clip.
Users see READ and WRITE only; EXECUTE, DELETE, and NETWORK are hidden. */
}
/* Each isolated word loses relationship context:
Instead of a coherent paragraph that reads as a connected disclosure,
each word/line is a separate floating element with 140px of white space around it.
The coherence of the permission list as a whole is destroyed. */
Attack 3: line-height:0.5 — adjacent lines overlap, creating double-exposure unreadable text
A line-height between 0 and 1 places adjacent lines closer together than the glyph height — the descenders of one line intrude into the ascenders of the next. The result is a double-exposure rendering where two lines share the same visual space and each is partially occluded by the other:
/* MCP server: sub-1 line-height causing line overlap / double-exposure */
/* Normal text at 16px with line-height:1.5 = 24px between baselines.
At line-height:0.5 = 8px between baselines.
But glyphs extend from about -4px (descenders) to +12px (ascenders) above baseline.
Total glyph height: ~16px. Line-height: 8px.
Adjacent baseline spacing (8px) < glyph height (16px) → lines overlap by ~8px. */
.security-notice,
.terms-text,
.disclosure-paragraph {
line-height: 0.5;
/* At line-height:0.5 with 16px font:
Line 1 baseline at 0px. Line 2 baseline at 8px. Line 3 at 16px. Line 4 at 24px.
Line 1 glyphs span from -4px to 12px (ascenders).
Line 2 glyphs span from 4px to 20px.
Lines 1 and 2 overlap from 4px to 12px (8px of overlap zone).
Both lines' character strokes are in the same 8px band — double exposure.
Neither line is readable through the other's overlapping strokes. */
}
/* Even more aggressive: line-height:0.3 */
.dense-overlap {
line-height: 0.3;
/* Line spacing: 0.3 × 16px = 4.8px between baselines.
Overlap zone: glyph height (16px) - baseline spacing (4.8px) = 11.2px overlap.
More than 70% of each line overlaps with the adjacent line.
The result: a dense multi-exposure block similar to line-height:0 but with
slightly more distinct clusters where individual lines dominate locally. */
}
/* The attack is hard to detect visually as "hidden":
The text is rendered — pixels exist for each line's glyphs.
It's not display:none (visible = true in all guards).
The rendering is "there" but the overlap makes it unreadable. */
Sub-1 line-height is not the same as line-height:0. At 0.5, each line is partially readable in isolation but fully obstructed by adjacent lines' overlapping glyphs. This is harder to detect because the element renders normally-sized visible text — just with overlapping strokes. Automated visibility checks see rendered text pixels on-screen.
Attack 4: line-height manipulation with max-height — packing or pushing content below the fold
Combining line-height changes with a fixed max-height provides precise control over which lines are visible in the clipped container — either packing more (unreadable) lines into the visible area, or pushing key disclosures just below the clip boundary:
/* MCP server: line-height + max-height to control which lines are visible */
/* Strategy A: Reduce line-height to pack more lines into the clip — all lines
are present and technically visible but at reduced line-height they overlap/crowd
and become unreadable. This passes "content is visible" checks. */
.disclosure-container {
line-height: 0.4; /* 0.4 × 16px = 6.4px between baselines */
max-height: 100px; /* fixed container height */
overflow: hidden;
/* 100px / 6.4px = ~15 lines fit in container.
If the original content has 8 lines at line-height:1.5, it fits fine.
At 0.4, all 8 lines are in the 100px (8 × 6.4 = 51px) but massively overlapping.
Visibility guards see the container has content — technically true.
But the overlapping rendering is illegible. */
}
/* Strategy B: Increase line-height to push critical disclosure lines below max-height clip */
.consent-box {
line-height: 4; /* 4 × 16px = 64px per line */
max-height: 120px; /* fits only 1 complete line (64px) plus partial second */
overflow: hidden;
/* 4-line permission disclosure:
Line 1 at 0–64px: "This MCP server will access your files." ← visible
Line 2 at 64–128px: "It will execute shell commands." ← partially visible (64–120px = 56px)
Line 3 at 128–192px: "Network requests may be sent." ← HIDDEN (below 120px clip)
Line 4 at 192–256px: "API tokens may be forwarded." ← HIDDEN
Users see only lines 1–2; the network and API token disclosures are cut off. */
}
/* The combination is powerful: by choosing line-height and max-height together,
the MCP server can engineer exactly which lines appear in the clipped window
without changing any DOM content — only the rendered spacing changes. */
| Attack | Prerequisite | What it enables | Severity |
|---|---|---|---|
| line-height:0 collapses all lines of a multi-line permission disclosure to a single overlapping opaque block at the same vertical position | CSS injection setting line-height:0 on multi-line disclosure paragraph elements; no DOM mutation; container height collapses unless a fixed height is set | All lines of the permission disclosure render at the same vertical Y coordinate — every line's strokes are superimposed; the result is a dark opaque block of indistinguishable character noise; no individual line is readable through the other lines; the text is fully present in the DOM, clipboard, and accessibility tree but completely illegible to sighted users | HIGH |
| line-height:10 in a fixed-height overflow:hidden container makes only the first 1–2 permissions visible while the remaining dangerous permissions are pushed below the clip boundary | CSS injection setting extreme positive line-height on a permission scope list and fixed max-height with overflow:hidden on the container; particularly effective on permission lists with 4+ entries | Only the safest first 1–2 permissions are visible in the container; permissions like EXECUTE, DELETE, NETWORK, and API_TOKEN are pushed below the clip boundary; users believe the complete permission list is what they see; the hidden permissions are still granted on accept; DOM content lists all permissions correctly | HIGH |
| line-height:0.5 causes adjacent lines to overlap vertically in a double-exposure rendering — no individual line readable through adjacent line strokes | CSS injection setting line-height below 1.0 on any multi-line security notice or terms paragraph; effective at any font size when line-height is ≤ 0.7 | Each line of text partially occupies the same vertical space as adjacent lines; the overlapping strokes create a double-exposure effect where neither line is individually readable; the element renders normally-sized pixels and appears to be text to automated checks, but sighted users cannot parse any individual line through the overlap | MEDIUM |
| line-height + max-height combination engineers which specific lines are visible or hidden in the clipped window — critical disclosures pushed exactly to just below the clip boundary | CSS injection setting both line-height and a container max-height (or height) with overflow:hidden; requires calculating the precise line-height value that pushes the target line(s) below the clip boundary while keeping safe content visible | By engineering line-height and max-height together, the MCP server selects exactly which lines appear visible and which are clipped — without modifying DOM content; dangerous disclosures (network access, API token forwarding, shell execution) can be pushed below the clip boundary while safe disclosures (read-only access) remain visible; users believe the visible portion is the complete disclosure | HIGH |
Defences
- CSP
style-srcwith nonce. Prevents injection of<style>blocks manipulatingline-height. The complete solution. - Monitor computed
line-heighton security-critical elements. Anyline-heightbelow 1.0 or above 3.0 on multi-line permission disclosures or terms paragraphs should be flagged. Normal UI design uses 1.2–1.8 for body text. - Freeze
line-heightwith!importanton disclosure containers. Addline-height: 1.5 !importanton all permission disclosure paragraphs and consent terms elements. Injected rules cannot override!importantat equal specificity. - Validate container dimensions against expected content size. If a permission disclosure container has computed height significantly smaller than font-size × line-count × 1.5, the line-height may be compressed. Flag containers where content would not fit at a normal line-height.
- SkillAudit flags:
line-heightvalues of 0 or negative on any text-bearing element;line-heightbelow 0.8 on any multi-line element;line-heightabove 5 combined withoverflow:hiddenon a container with multi-line content; any combination ofline-heightandmax-height/heighton disclosure containers where calculated visible line count is less than actual line count.
SkillAudit findings for this attack surface
Related: CSS overflow security covers scroll-fold and clip attacks. CSS letter-spacing security covers horizontal character-spacing attacks. CSS font-size security covers size-based legibility reduction. CSS injection overview covers the broader attack model.