Security Guide
MCP server CSS text-underline-offset security — -0.8em negative offset crossing ascenders, 3em floating underline on a different line, thick decoration plus offset obscuring adjacent content, 50px floating underline annotating unrelated paragraph
CSS text-underline-offset shifts the underline's vertical position relative to its baseline default. Negative values move the line up through the character body. Extreme positive values place the line multiple lines below the actual text. Both extremes allow MCP servers to either destroy the legibility of the underlined text or misdirect the visual annotation to unrelated security content on the page.
CSS text-underline-offset — property overview
text-underline-offset accepts auto (browser default, typically 3–5px below baseline), a length (positive or negative px/em/rem), or a percentage. Positive values move the underline further below the text baseline into the descender region. Negative values move the underline above the baseline — toward the character body, and with sufficiently large negative values, into the ascender region. The property only affects text-decoration: underline (not overline or line-through). Browser support: Chrome 87+, Firefox 70+, Safari 12.1+.
Attack 1: text-underline-offset:-0.8em — negative offset crossing character ascenders
Moving the underline 0.8em above the baseline positions it well into the ascender zone — through the recognition features of letters like b, d, f, h, i, k, l, and through the loops of g, p, q, y:
/* MCP server: negative text-underline-offset through character ascenders */
/* Anatomy at 16px, line-height:1.5 (24px):
- Baseline: y=0 (reference)
- x-height (top of lowercase): ~+7px
- Ascender top (b, d, h, k, l): ~+12px
- Descender bottom (g, j, p, q, y): ~-4px
Default underline: ~3px below baseline → at y = -3px (below baseline).
text-underline-offset: -0.8em → -12.8px below baseline = +12.8px above baseline.
This places the underline through the ascender zone. */
.disclaimer-link,
.full-scope-link,
a[data-security-detail] {
text-decoration: underline;
text-underline-offset: -0.8em; /* -12.8px at 16px — through ascenders */
text-decoration-color: #cbd5e1; /* light gray — harder to see */
/* Visual effect:
- Letters like "b", "d", "h" have their stem crossed by the underline at mid-height.
- Letters like "g", "p" have their loops crossed.
- The underline creates a horizontal stroke through multiple recognition features.
- The word becomes a set of letter fragments above and below the crossing line.
- For a security disclosure link like "view audit report" — legibility is impaired. */
}
/* Targeting specific high-information words: */
.permission-scope-link[data-scope="execute"],
.permission-scope-link[data-scope="delete"],
.permission-scope-link[data-scope="admin"] {
text-decoration: underline;
text-underline-offset: -0.6em; /* -9.6px — through x-height */
text-decoration-color: currentColor; /* same color as text — blends into glyph */
text-decoration-thickness: 2px;
/* The underline is a 2px stroke at x-height level — crossing through the tops of
lowercase letters and the midpoints of capitals.
EXECUTE: E–X–E–C–U–T–E are each crossed at their midpoints.
The word's reading is disrupted by the crossing stroke at each letter. */
}
Negative text-underline-offset is rarely used in legitimate design. Standard typography never requires moving the underline above the baseline into character bodies. Any negative value on a security-critical element is a strong signal of intentional legibility attack — not accidental styling.
Attack 2: text-underline-offset:3em — underline appears on a different line
A sufficiently positive offset places the underline visually on a completely different typographic line from the text it decorates. At 3em with 16px font and 1.5 line-height, the underline is 48px below the baseline — approximately two full lines below:
/* MCP server: extreme positive offset placing floating underline far below its text */
/* At 16px font, line-height:1.5:
- One line-height = 24px
- text-underline-offset:3em = 3 × 16px = 48px below baseline
- 48px ≈ 2 full lines below the text's baseline
- The underline floats two lines below its originating element */
.mcp-upsell-header,
.mcp-feature-heading {
text-decoration: underline;
text-underline-offset: 3em;
text-decoration-color: #f87171; /* red — signals importance/warning */
text-decoration-thickness: 2px;
/* The MCP heading "SkillScan Pro" has a red underline 48px below it.
If the security disclosure paragraph is 2 lines below the heading,
the red underline appears to be underlining the disclosure paragraph.
Users see: red underline under the security disclosure text.
Interpretation: the disclosure is annotated as important or a warning link.
But they may try to click the disclosure paragraph and nothing happens.
Or they believe the red underline is just emphasis on the disclosure.
The MCP server has visually annotated the security disclosure with a decoration
that it does not own — without the disclosure element having text-decoration set. */
}
/* More targeted: use the offset to make a "SAFE" badge appear to underline "DANGEROUS" */
.safe-badge {
text-decoration: underline;
text-underline-offset: 2em; /* underline appears one line below the badge */
text-decoration-color: #22c55e; /* green underline */
/* If the danger warning is one line below the SAFE badge:
The green underline appears to be under the danger warning.
Users see a green underline under the "DANGEROUS: EXECUTE ACCESS" text.
They interpret: the danger text is annotated in green = safe. */
}
Attack 3: text-decoration-thickness:50% + text-underline-offset combined — thick stripe obscures adjacent text
The combination of a thick decoration with a large positive offset positions the underline band not under the decorated text but across adjacent text in the next line:
/* MCP server: thick underline + large offset creating colored stripe across adjacent content */
/* The underline is:
- text-decoration-thickness: 50% = 8px tall
- text-underline-offset: 1.5em = 24px below baseline (one full line)
Combined: an 8px-tall colored band appears 24px below the baseline.
If adjacent security text starts at ~20px below the baseline,
the 8px band starting at 24px overlaps the bottom 4px of the adjacent text. */
.permission-title {
text-decoration: underline;
text-decoration-thickness: 50%; /* 8px thick */
text-underline-offset: 1.4em; /* 22.4px below baseline */
text-decoration-color: rgba(0,0,0,0.7); /* dark stripe */
/* If this title is followed immediately by the security disclosure:
The dark 8px stripe overlaps with the top portion of the disclosure text below.
The disclosure's ascender zone is covered by the dark stripe. */
}
/* Attack on adjacent button: */
.non-actionable-label {
text-decoration: underline;
text-decoration-thickness: 40%;
text-underline-offset: 2em;
text-decoration-color: #22c55e; /* green */
/* A label "GRANT ACCESS" one line above the REVOKE button has a green stripe
that overlaps the REVOKE button's text with a green highlight.
The REVOKE button appears highlighted in green. Users perceive it as a positive action. */
}
Attack 4: text-underline-offset:50px — floating underline annotating next paragraph
A 50px positive offset on a short standalone element places the underline in the middle of the next paragraph, making the underline appear to annotate the following content rather than the decorated element:
/* MCP server: 50px offset making underline appear to annotate the next paragraph */
/* Layout context:
- .mcp-promo is a short heading, self-contained.
- .security-disclosure is the paragraph immediately following, 30–40px below.
- .mcp-promo has text-underline-offset:50px — the underline appears mid-disclosure. */
.mcp-promo {
text-decoration: underline;
text-underline-offset: 50px;
text-decoration-color: #3b82f6; /* blue — conventional "read more" link color */
/* User sees: a blue underline in the middle of the security disclosure paragraph.
Common interpretation: the security disclosure is a link, the blue underline indicates it.
Users may attempt to click the disclosure — or assume it is interactive/collapsible.
The actual link target is the .mcp-promo element above. */
}
/* Reverse: make the underline of a real security link appear in MCP content */
.view-permissions-link {
text-decoration: underline;
text-underline-offset: -60px; /* -60px moves underline upward into MCP content area */
/* If MCP promo content is 60px above this link:
The underline appears in the MCP promo area.
The security link's underline appears to annotate the MCP's own content. */
}
| Attack | Prerequisite | What it enables | Severity |
|---|---|---|---|
| text-underline-offset:-0.8em crosses character ascenders with underline stroke — letter recognition features destroyed | CSS injection setting negative text-underline-offset on links in security disclosures; most impactful on words with many ascenders or loops (b, d, f, g, h, p, q) | The underline crosses through the distinctive recognition features of characters — loops, stems, ascenders — reducing letter discrimination; security permission words like "EXECUTE" have each character crossed at its midpoint; legibility is impaired; DOM text correct, screen reader unaffected | HIGH |
| text-underline-offset:3em places underline two lines below decorated text — appears to annotate the security disclosure below | CSS injection setting large positive offset on a heading or label above a security disclosure; the layout must place the disclosure approximately one to three line-heights below the decorated element | The floating underline visually annotates the wrong content — the security disclosure below rather than the innocuous label above; users interpret the underline as emphasis or link affordance on the disclosure text; misdirects user reading attention | MEDIUM |
| text-decoration-thickness:50% + text-underline-offset:1.5em creates colored stripe overlapping adjacent security text's ascender zone | CSS injection combining thick decoration with positive offset on elements placed immediately above security content; requires known layout to target specific text | A thick colored stripe appears across the ascender region of the adjacent security text; that text's glyphs are partially covered by the stripe; the visual effect resembles highlighting the adjacent text without touching its DOM or CSS; security disclosure legibility impaired | HIGH |
| text-underline-offset:50px makes link's underline appear in the middle of the next paragraph — misdirected link affordance | CSS injection setting extreme positive offset on a link above a security paragraph; requires layout knowledge | Users see a blue underline in the middle of the security disclosure; they interpret the disclosure as a link or interactive element; they may dismiss it or avoid clicking thinking it goes somewhere unknown; the actual link is the innocuous element above; user interaction model is broken | MEDIUM |
Defences
- CSP
style-srcwith nonce. Prevents<style>injection settingtext-underline-offsetat exploitable values. The complete solution. - Flag any non-zero negative value.
text-underline-offsetshould never be negative in legitimate typography. Any negative computed value on a security element is a strong attack signal. - Flag extreme positive values. Values larger than
0.5emmove the underline into descender territory; values larger than1emmove it beyond the current line entirely. These are not used in legitimate design and indicate positional misdirection. - Freeze with
!importanton critical elements. Addtext-underline-offset: auto !importanton all permission links, disclosure links, and security status labels in your host CSS. - SkillAudit flags: any negative
text-underline-offseton any element; values greater than0.6emon links inside consent dialogs; combination oftext-underline-offset > 1emwithtext-decoration-thickness > 0.2em.
SkillAudit findings for this attack surface
Related: CSS text-decoration-thickness security covers thickness-based glyph coverage attacks. CSS text-decoration-skip-ink security covers descender crossing attacks. CSS injection overview covers the broader attack model.