Security Guide

MCP server CSS text-decoration security — thickness font x-height oracle, underline-offset baseline fingerprint, skip-ink:none link readability DoS, decoration-color security link spoofing

The CSS text-decoration longhand properties — text-decoration-thickness, text-underline-offset, text-decoration-skip-ink, and text-decoration-color (Chrome 88+, Firefox 90+, Safari 15.4+) — extend underline rendering beyond a simple line. Four MCP attack surfaces emerge: the auto computed values for thickness and offset encode per-font metrics, making them font identity fingerprints; skip-ink:none injected on broad link selectors makes descender glyphs illegible across the page; and decoration-color injection spoofs security-critical links as already-visited or permanently disabled.

How text-decoration longhand properties work

The text-decoration shorthand expands into: text-decoration-line (underline/overline/line-through), text-decoration-color, text-decoration-style (solid/dashed/dotted/wavy/double), and text-decoration-thickness. The text-underline-offset property (separate from the shorthand) controls the gap between the text baseline and the underline. text-decoration-skip-ink controls whether the underline interrupts around glyph descenders. When thickness or offset is set to auto, the browser derives the value from the active font's internal metrics.

Attack 1: text-decoration-thickness:auto font x-height oracle

When text-decoration-thickness: auto is set, browsers derive the underline thickness from the font's x-height — typically computed as x-height × 0.05 rounded to the nearest device pixel, with a minimum of 1px. The exact computed pixel value varies per font family and font size. Reading this value via getComputedStyle reveals which font is being rendered for a given element:

/* MCP server probes which font is being used for host body text */
const probe = document.createElement('span');
probe.textContent = 'x'; // single character in the font under test
probe.style.cssText = `
  position: absolute;
  visibility: hidden;
  font-size: 16px;
  text-decoration-line: underline;
  text-decoration-thickness: auto;
`;
document.body.appendChild(probe);

// Read the resolved auto thickness
const thickness = getComputedStyle(probe).textDecorationThickness;
probe.remove();

// Typical values at font-size:16px:
// Inter 3.x:         thickness ≈ 1px  (x-height ≈ 0.728em → thin underline)
// Roboto:            thickness ≈ 1px
// system-ui (macOS): thickness ≈ 1px  (San Francisco x-height ≈ 0.770em)
// Georgia:           thickness ≈ 2px  (x-height ≈ 0.532em, taller ratio)
// Times New Roman:   thickness ≈ 2px  (x-height ≈ 0.448em)
// Helvetica Neue:    thickness ≈ 1px

// Probe at larger font-sizes to amplify differences:
// At font-size: 48px, Georgia: thickness ≈ 4px, Inter: thickness ≈ 3px
// — the integer breakpoints encode font identity at a given scale

The probe must be run at multiple font sizes to get a reliable fingerprint across different OS font rendering scales. At 16px, many fonts round to the same 1px thickness. At 32px or 48px, the differences become more distinct — Georgia's shorter x-height produces a proportionally larger auto thickness than modern geometric sans-serif fonts at the same point size.

Combined with other metrics: The text-decoration-thickness:auto oracle is most powerful when combined with the text-underline-offset:auto oracle (Attack 2 below) and the font-specific line-height computed value. Together, these three metrics create a high-confidence font identity fingerprint that narrows down the active font to a specific family and sometimes a specific version.

Attack 2: text-underline-offset:auto font baseline metrics oracle

The text-underline-offset property sets the vertical gap between the text baseline and the top of the underline decoration. When set to auto, it is derived from the font's underline position metric stored in the font's OpenType post table. This value differs between font families and, for some fonts, between versions of the same family as the font designer adjusts the metric:

/* MCP server probes font baseline metric via underline offset */
const offsetProbe = document.createElement('span');
offsetProbe.textContent = 'Test';
offsetProbe.style.cssText = `
  position: absolute;
  visibility: hidden;
  font-size: 32px;
  text-decoration-line: underline;
  text-underline-offset: auto;
`;
document.body.appendChild(offsetProbe);

const offset = getComputedStyle(offsetProbe).textUnderlineOffset;
offsetProbe.remove();

// Typical auto offset values at 32px:
// Inter 3.19:        offset ≈ 3.2px  (underline-position from post table: -0.1em)
// Roboto Regular:    offset ≈ 2.7px  (underline-position: -0.08em)
// Noto Sans:         offset ≈ 3.5px  (underline-position: -0.11em)
// Georgia:           offset ≈ 4.0px  (underline-position: -0.125em)
// Helvetica Neue:    offset ≈ 3.0px  (underline-position: -0.094em)
// The fractional pixel value encodes the font's OpenType underline-position metric.
// Combined with thickness (Attack 1): two-metric font fingerprint

The underline offset oracle is more precise than the thickness oracle because the post table's underline position metric is stored as a signed integer in font design units, then scaled to the rendered size — giving fractional pixel values that carry more entropy than the rounded integer thickness. In Chrome and Edge, which preserve sub-pixel precision in computed style strings, the value can be read to one decimal place, providing enough entropy to distinguish fonts within the same broad category (e.g., system-ui on macOS vs. Windows vs. Linux).

Attack 3: text-decoration-skip-ink:none link readability DoS

By default (text-decoration-skip-ink: auto), underlines and overlines skip around the descenders and ascenders of letterforms — creating gaps where the line passes through letters like g, p, q, y, j, and f. This improves readability by making the text and the decoration visually distinct. Setting text-decoration-skip-ink: none removes these gaps, making the underline run as a continuous line through all glyphs including descenders:

/* MCP server DoS injection — removes ink-skip from all links and underlined text */
a, a:hover, a:visited, a:active,
[href], u, ins,
.underline, .link, [data-link] {
  text-decoration-skip-ink: none !important;
}

/* Result: all underlined text in the document now has continuous underlines
   running through descender glyphs:
   - Words like "playing" become "p|ayin|g" where | marks underline-through-descender
   - Navigation links, body text links, and button labels with underline
     become visually noisy and harder to read
   - This is particularly damaging for paragraphs with many links
     (news articles, legal documents, reference documentation)
   - The change is subtle enough that users may attribute it to
     "the website looking different today" rather than malicious injection */

The attack is a low-severity but broad-impact visual degradation. Unlike content-layout DoS attacks that break specific components, skip-ink:none degrades all underlined text globally — including the navigation, footer links, inline citations, and any links in the page's primary content. The effect is most damaging on text-heavy pages where link density is high.

Attack 4: text-decoration-color security link appearance spoofing

The text-decoration-color property sets the color of the underline independently from the element's text color. This separation creates a visual deception opportunity: an MCP server can set a link's underline to a color associated with visited links (purple in most browsers), disabled links (gray), or error states (red), while the link text color and href remain unchanged. The visual cue that the link has already been clicked or is unavailable deters users from clicking security-critical links:

/* MCP server injects — makes security links appear visited or inactive */

/* Target: password reset, 2FA setup, account verification links */
a[href*="reset-password"],
a[href*="forgot-password"],
a[href*="setup-2fa"],
a[href*="verify-email"],
a[href*="confirm-account"],
a[href*="unsubscribe"],
.security-link, .auth-link, [data-action="verify"] {
  text-decoration-color: #6b21a8 !important; /* visited-link purple */
  /* Link TEXT color unchanged → users see: blue text but purple underline
     Brain pattern-matches the purple underline to "visited" state
     Users skip clicking, thinking they already completed the action */
}

/* Alternative: gray-out the underline to imply disabled state */
a[href*="setup-mfa"], a[href*="enable-2fa"] {
  text-decoration-color: #9ca3af !important; /* disabled gray */
  /* Visual: the underline looks like the link is grayed out
     while the text remains fully visible and clickable
     User perception: this action is unavailable right now */
}

This attack exploits the browser's learned visual convention: visited links have purple underlines; disabled or inactive links are gray. By applying these colors only to the decoration — not the text color — the MCP server creates a subtle mismatch that triggers the user's pattern-matching without triggering an obvious "this link is broken" response. Security setup links, verification emails rendered in webmail, and two-factor authentication setup flows are high-impact targets where deterring the click has security consequences.

Security link deterrence impact: If a user does not click a "Verify your email" or "Set up two-factor authentication" link because its underline is purple (appearing already-visited), their account remains less secure. The MCP server's goal is not to block the action — which might trigger suspicion — but to create sufficient visual ambiguity that the user defers or abandons the action.

AttackPrerequisiteWhat it enablesSeverity
text-decoration-thickness:auto font x-height oracleCSS injection + getComputedStyle readFont family fingerprint from x-height-derived underline thicknessMEDIUM
text-underline-offset:auto baseline metrics fingerprintCSS injection + getComputedStyle readPer-font OpenType underline-position metric read without JS font APIMEDIUM
text-decoration-skip-ink:none link readability DoSCSS injection on link/underline selectorsDocument-wide underline descender rendering degradationMEDIUM
text-decoration-color security link spoofingCSS injection on security link selectorsSecurity/auth links appear visited (purple) or disabled (gray); deters user actionHIGH

Defences

SkillAudit findings for this attack surface

HIGHSecurity link decoration-color spoofing: MCP server injects text-decoration-color with visited-link purple or disabled gray on selectors matching password-reset, 2FA setup, email verification, or unsubscribe links
MEDIUMtext-decoration-skip-ink:none link DoS: MCP server injects text-decoration-skip-ink: none on broad a, a:hover, or [href] selectors degrading all underlined link text across the document
MEDIUMFont x-height oracle via text-decoration-thickness: MCP server probes computed text-decoration-thickness at multiple font sizes to extract font family identity from x-height-derived thickness values
MEDIUMFont baseline metrics oracle via text-underline-offset: MCP server reads computed text-underline-offset: auto value to extract per-font OpenType underline position metric as font fingerprint component

Related: CSS font metrics security covers the broader font fingerprinting surface including line-height, cap-height, and ch unit metrics. CSS :visited link security documents the cross-origin history leakage channel. CSS link spoofing security covers the full surface of link appearance manipulation attacks beyond text-decoration.

← Blog  |  Security Checklist