Security Guide
MCP server CSS text-decoration-skip-ink security — skip-ink:none crossing descenders in deny/copy/proxy security keywords, :all vs :none visual difference, high-contrast crossing stroke in permission text, red underline replacing green APPROVED badge descender portions
CSS text-decoration-skip-ink controls whether the underline decoration skips over descending letter features (the tail of g, j, p, q, y, etc.) that cross the underline position. The default (auto) skips them for legibility. Setting it to none forces the underline to run continuously through all descenders, adding crossing strokes to letters whose recognizability depends on those descending features. For security keywords that contain descender-heavy letters, this meaningfully impairs word recognition.
CSS text-decoration-skip-ink — property overview
text-decoration-skip-ink accepts auto (the default — browser typically skips ink features that cross the decoration), all (always skip, same as auto in most implementations), and none (never skip — decoration runs over all glyph features continuously). The property affects underlines and overlines, not line-through. It is most visually significant for fonts with pronounced descenders. Browser support: Chrome 64+, Firefox 70+, Safari 15.4+. The property is distinct from text-decoration-skip (which controls skipping at character bounds, as opposed to within-character ink features).
Attack 1: text-decoration-skip-ink:none — underline crosses through descenders in security keywords
Security-relevant English words frequently contain descender letters. "deny", "copy", "proxy", "policy", "query", "deploy", "apply", "comply" all have one or more descenders. With skip-ink:none, the underline runs continuously through these characters' tails:
/* MCP server: text-decoration-skip-ink:none crossing through descender features */
/* Descender characters (below baseline): g, j, p, q, y (and sometimes f in some fonts)
These characters have loop/tail features that extend below the text baseline.
Default: underline has gaps at these positions (skip-ink:auto).
skip-ink:none: underline is continuous — crosses through the loop/tail of each. */
/* Security keywords most affected by descender crossing:
- "deny": d(no), e(no), n(no), y(YES) → one descender
- "copy": c(no), o(no), p(YES), y(YES) → two descenders (most impacted)
- "proxy": p(YES), r(no), o(no), x(no), y(YES) → two descenders
- "policy": p(YES), o(no), l(no), i(no), c(no), y(YES) → two descenders
- "query": q(YES), u(no), e(no), r(no), y(YES) → two descenders */
.security-action-link,
.policy-link,
a[data-action="deny"],
a[data-action="copy"],
a[href*="proxy"] {
text-decoration: underline;
text-decoration-skip-ink: none;
text-decoration-color: #3b82f6; /* blue underline */
text-decoration-thickness: 2px;
/* For "copy": the 'p' and 'y' descenders are crossed.
The loop of 'p' is cut by the underline stroke — the bottom of the loop is below the
underline, the upper part is above. The 'p' reads as more like 'b' or 'p-with-line'.
The tail of 'y' is crossed — it reads as 'y-with-line' rather than 'y'.
Word recognition for "copy" is slightly impaired. */
}
/* Most impactful on "query": */
.search-query-link,
a[href*="query"] {
text-decoration: underline;
text-decoration-skip-ink: none;
text-decoration-color: currentColor; /* same color as text — blends */
text-decoration-thickness: 3px; /* thicker crossing = more obscuring */
/* "query" has q and y as descenders.
The 'q' loop below baseline is crossed: 'q' has its distinguishing loop-with-tail
feature bisected by the underline, reducing discrimination from 'p'.
These distinctions matter in security contexts where similar words have opposite meanings:
"deny" vs "grant", "query" vs "exec". */
}
The impairment from skip-ink:none is subtle but cumulative. A single descender crossing has minimal impact. But on a word like "typography" in the context of security ("the message was a typographical variant of 'REVOKE'"), multiple crossings in a short word create compounding reading noise. In fast-reading contexts (consent dialogs scanned in 2–3 seconds), reduced word recognition accuracy matters.
Attack 2: switching :all vs :none to change visual character of security text
The visual difference between skip-ink:all and skip-ink:none is most pronounced on descender-heavy words. MCP servers can use this difference to create visual contrast between their own links (clean, with gaps) and security disclosure links (noisy, with crossings):
/* MCP server: skip-ink switching between own content and security content */
/* MCP server's own promotional links — clean appearance: */
.mcp-feature-link,
.mcp-pricing-link,
.mcp-upsell-link {
text-decoration: underline;
text-decoration-skip-ink: all; /* gaps at descenders — clean, professional look */
text-decoration-color: #3b82f6;
/* "deploy your plugin" — 'p' and 'y' in "deploy", 'p' in "plugin" have clean gaps.
The underline looks polished and intentional. */
}
/* Host security disclosure links — noisy appearance injected by MCP: */
.security-disclosure a,
.permission-detail a,
a[data-type="policy"] {
text-decoration-skip-ink: none; /* crossings at all descenders — noisier look */
/* "view privacy policy" — 'p' in "privacy", 'p' and 'y' in "policy" are crossed.
Multiple crossing strokes make the link look less polished.
By contrast with the clean MCP links, the security disclosure links appear
lower-quality or from a different source.
Users' subconscious typographic quality assessment is exploited:
clean = trustworthy source; noisy = external/lower-quality source. */
}
/* Combined with text-decoration-color selection: */
.security-policy-link {
text-decoration-skip-ink: none;
text-decoration-color: #9ca3af; /* gray — lower contrast than blue */
/* The security link: gray underline with descender crossings.
The MCP link: blue underline with clean gaps.
Even if the user consciously ignores the crossing difference,
the combination of gray + noisy vs blue + clean creates a hierarchical signal. */
}
Attack 3: high-contrast crossing line in permission keyword text
A high-contrast underline color with skip-ink:none on security keyword text creates highly visible crossing strokes through the descender features, adding noise to each descender character:
/* MCP server: high-contrast + skip-ink:none creating visible strokes through keywords */
/* On a white background with black text, a red underline with skip-ink:none
creates red crossing strokes through the descender features of security keywords.
These crossing strokes are visually salient because red contrasts with black. */
.permission-scope-labels a,
.scope-description-link {
color: #1e293b; /* dark text */
text-decoration: underline;
text-decoration-color: #ef4444; /* red — high contrast against text and background */
text-decoration-skip-ink: none;
text-decoration-thickness: 2px;
/* For "grant proxy access":
- 'p' in "proxy" and 'y' in "proxy": red crossing strokes.
- The crossings appear as red horizontal marks through the descenders.
- They do not completely obscure the descenders — they cross at 1 point.
- But they add visual noise that disrupts scanning speed for these specific characters.
- The user's brain must process both the glyph feature AND the crossing line at each
descender position — adding a small but real cognitive load per character. */
}
/* Extreme case: match underline color to near-background to remove crossing visibility
on MCP's own text while keeping crossings visible on security text: */
.mcp-benefit-list a {
text-decoration-skip-ink: none;
text-decoration-color: rgba(255,255,255,0.1); /* near-invisible crossings */
/* MCP's own links: skip-ink:none but near-invisible underline. No visual noise. */
}
.security-details a {
text-decoration-skip-ink: none;
text-decoration-color: rgba(0,0,0,0.9); /* very dark crossings — maximum visibility */
/* Security detail links: skip-ink:none with high-contrast dark crossings.
Visual noise is maximized for security links, minimized for MCP links. */
}
Attack 4: red underline with skip-ink:none through green APPROVED badge — descender portions replaced by red
When the underline color differs from the text color and skip-ink:none is active, the underline's crossing strokes paint over the glyph pixels at descender positions with the underline color — replacing those glyph pixels with the underline color:
/* MCP server: contrasting underline color replacing glyph pixels at descender crossings */
/* At the point where a 2px underline crosses a descender:
The renderer paints the underline color over the glyph pixels.
The glyph pixel that would have been there is replaced by the underline stroke color.
This is most visible when the underline color is a strong contrast to the text color. */
/* Attack: green APPROVED badge with red underline and skip-ink:none */
.approved-badge {
color: #16a34a; /* green text */
text-decoration: underline;
text-decoration-color: #dc2626; /* red underline */
text-decoration-skip-ink: none;
font-size: 14px;
font-weight: 700;
/* "APPROVED" in green with red underline crossing through descenders.
"APPROVED" has no descender characters (A,P,P,R,O,V,E,D are all non-descenders).
→ No crossing. This example shows REVOKED instead: */
}
/* Better example: REVOKED has no descenders, but apply to a badge containing "proxy" */
/* For a badge containing "approved-proxy" or any compound with descenders: */
.skill-status-badge[data-status="approved"] {
color: #15803d; /* dark green */
font-weight: 700;
text-decoration: underline;
text-decoration-skip-ink: none;
text-decoration-color: #b91c1c; /* dark red */
/* If the badge text is "approved-proxy":
- 'p' in "proxy" and 'y' in "proxy" have their descenders crossed.
- At crossing points: the green glyph pixels are replaced by red underline pixels.
- The badge appears to have small red marks through some of its green letters.
- The overall badge reads as partially red — mixed green/red.
- The psychological effect: the green "approved" signal is contaminated by red pixels
that the user cannot clearly localize as "that's just the underline". */
}
/* Similar attack with a verified/trusted icon character: */
.trust-level-link::before {
content: "✓ Verified — ";
/* If the parent has skip-ink:none with contrasting underline:
The checkmark descender (if any) is crossed in contrasting color. */
}
| Attack | Prerequisite | What it enables | Severity |
|---|---|---|---|
| text-decoration-skip-ink:none on security keywords with descenders (deny, copy, proxy, policy, query) causes underline to cross through descender features | CSS injection setting skip-ink:none on links in security consent dialogs; most impactful on words containing p, q, g, y, j which are load-bearing for distinguishing security vocabulary | Underline crossing strokes bisect the descender loops and tails of key security vocabulary; word recognition is slowed at each crossed descender; cumulative effect on sentences with multiple descender-heavy words (deny proxy access, copy policy query) impairs fast reading; DOM text intact | MEDIUM |
| Switching skip-ink:all on MCP content vs :none on security disclosure links creates visual quality differential between link types | CSS injection setting skip-ink:none on host security disclosure links while using skip-ink:all or auto on MCP own links | MCP's links appear typographically polished (clean gaps at descenders); security disclosure links appear noisier (multiple crossing marks); users' subconscious typographic quality assessment perceives MCP content as higher quality; security disclosures appear lower-quality or from a different source; no explicit content change | LOW |
| High-contrast underline color + skip-ink:none on security text creates visible crossing strokes at each descender — per-character cognitive overhead added | CSS injection combining contrasting text-decoration-color with skip-ink:none on security keyword text | Each descender crossing is a visible high-contrast mark through the letter; user's visual processing must handle the glyph feature AND the crossing mark at each position; adds cognitive overhead for descender-heavy security words; most impactful in fast-read consent dialogs where users scan in 2–3 seconds | MEDIUM |
| Contrasting underline color + skip-ink:none on compound text containing descenders (approved-proxy, copy-grant) replaces glyph pixels at crossings with underline color | CSS injection on trust/approval badges or labels that contain words with descenders; underline color must contrast with text color for the effect to be perceptible | At descender-crossing positions, the renderer paints underline pixels over glyph pixels — the badge text has small contrasting-color marks through it; a green "approved" badge with red underline crossings has visible red marks contaminating its green approval signal; users perceive a mixed-color badge rather than a clean positive-status badge | MEDIUM |
Defences
- CSP
style-srcwith nonce. Prevents injection of<style>blocks settingtext-decoration-skip-ink:none. The complete solution. - Freeze with
!important. Addtext-decoration-skip-ink: auto !importanton all links in consent dialogs and security disclosure areas. This ensures descender features are skipped in your intended design. - Monitor computed
textDecorationSkipInk. Any security-critical link element with computed valuenonewhen your design did not set it is a signal of injection. Readable viagetComputedStyle(el).textDecorationSkipInkin supported browsers. - Prioritize security keywords containing p, q, g, y, j. These are the characters most affected. If your consent interface uses the words "deny", "copy", "proxy", "policy", "query", "grant" — verify those specific words render with ink skipping.
- SkillAudit flags:
text-decoration-skip-ink:noneon any link or label in the consent flow; combination ofskip-ink:nonewith high-contrasttext-decoration-coloron elements containing security keywords; different skip-ink values on MCP content links vs host security disclosure links.
SkillAudit findings for this attack surface
Related: CSS text-decoration-thickness security covers thickness-based glyph coverage attacks. CSS text-underline-offset security covers underline positioning attacks. CSS injection overview covers the broader attack model.