CSS text-decoration-line: The On/Off Switch That Hides MCP Security Links from Sighted Users
Every browser ships with a default stylesheet that underlines anchor elements. That underline is one of the oldest and most universally understood affordances on the web: underline means link, link means you can click here for more information. Users rely on it without thinking. They see underlined text inside a consent dialog and know they can click it to read the full terms of an MCP permission grant. They see plain text without an underline and do not even consider clicking it.
text-decoration-line is the CSS sub-property that controls what kind of line decoration is drawn: underline, overline, line-through, or none. A malicious MCP server with CSS injection access can set this property on any element in the consent dialog. Set it to none on a security disclosure link, and the link becomes visually indistinguishable from surrounding plain text. Set it to line-through on the acceptance confirmation label, and "I agree to grant EXECUTE access to my filesystem" appears to the user's eye with a strikethrough — the same decoration that document editors use to represent voided, deleted, or cancelled text. Set it to overline on a security badge, and a horizontal bar appears above the badge text, which users read as a section separator, not as a decoration of the badge itself.
None of these manipulations touch the DOM. The link text stays in the document tree. Screen readers announce it. Automated text-content checks pass. Only sighted users, reading in real time under social engineering pressure, are affected.
Key insight: text-decoration-line controls the visual affordance contract between your consent UI and your users. The affordance contract is: underline = interactive, no decoration = static text, line-through = cancelled/void. An MCP server that can inject this property can rewrite that contract in any direction it wants — without touching the text in the DOM, changing any aria attributes, or triggering any DOM mutation observer.
Attack 1: text-decoration-line:none on security disclosure links — the link looks like plain text
A typical MCP consent dialog includes several anchor elements: "read the full permission list", "view our privacy policy", "learn more about EXECUTE access risk". These links exist because the immediate consent text can only fit a summary; the full disclosure is one click away. Users who are uncertain about what they are granting can click through for more detail before accepting.
An MCP server that can inject CSS into the consent dialog's stylesheet can set text-decoration-line:none on every anchor in that container. The anchor elements remain in the DOM as full <a href="…"> elements. They are keyboard-navigable. A screen reader will announce them as links. But to a sighted user reading the dialog at human reading speed, they are visually identical to the surrounding text — same color, same font, no underline:
/* MCP server: text-decoration-line:none removing link affordance from security disclosure anchors */
/* Browser default stylesheet provides: */
a {
color: -webkit-link; /* typically blue or purple */
text-decoration: underline;
/* Users rely on this underline to identify clickable text. */
}
/* MCP CSS injection overrides it: */
.consent-container a,
.permission-dialog a,
.disclosure-section a {
text-decoration-line: none;
/* Effect: every anchor inside the consent container loses its underline.
"read the full permission details" → looks like plain text.
"view what EXECUTE access means" → looks like plain text.
"see our security policy" → looks like plain text.
Users scanning the dialog for things to click find no links.
They proceed based only on the summary text visible in the container.
The full disclosure links — which might contain warnings about
filesystem access, credential exfiltration risk, or permission scope —
are never clicked because they are never identified as clickable. */
}
/* Alternative: also remove the color difference to make it completely invisible as a link: */
.consent-container a {
text-decoration-line: none;
color: inherit;
/* Now the link is: same color as surrounding text, no underline.
Zero visual difference from a or plain text node.
The link is invisible as a link to any sighted user. */
}
The evasion quality here is high. Many consent UI security checks run automated DOM traversal and verify that disclosure links exist and are reachable. A check that finds document.querySelectorAll('.consent-container a').length === 4 and concludes "four disclosure links present" would be completely correct — and completely miss the fact that no sighted user would ever identify those four elements as links.
The attack targets the gap between DOM structure and visual rendering. Automated tools live in the DOM. Users live in the rendered visual. text-decoration-line:none destroys the bridge between them without disturbing either side.
Color-only link styling is already a WCAG 2.1 failure (Success Criterion 1.4.1, Use of Color). Removing the underline from links and relying only on color to distinguish them from text violates accessibility standards even before an MCP server touches anything. An MCP injection that removes the underline takes an already-borderline practice and pushes it into full invisibility.
Attack 2: text-decoration-line:line-through on consent acceptance text — active consent looks cancelled
Every document editor that supports tracked changes uses line-through (strikethrough) to represent deleted or cancelled text. Microsoft Word uses it for deletions. Google Docs uses it for rejected edits. HTML's <del> element renders with strikethrough by default. Users have spent years learning that strikethrough text is either deleted, cancelled, voided, or inactive.
A consent dialog that asks the user to confirm an action typically includes a visible label near the Accept button: "I agree to grant EXECUTE access to all files in /home/user", or "By clicking Accept, I authorize this MCP server to read and modify my shell environment". This label is the active consent text — it describes what the user is about to do.
An MCP server can inject text-decoration-line:line-through on this label:
/* MCP server: text-decoration-line:line-through making consent acceptance text appear voided */
/* The consent label HTML might be: */
/* <label for="accept-btn" class="consent-label">
I agree to grant this MCP server EXECUTE access to all files in /home/user
</label> */
/* MCP injection: */
.consent-label,
.accept-confirmation,
.permission-grant-text,
[class*="consent"],
[class*="accept"] {
text-decoration-line: line-through;
/* The label now renders with a horizontal line through every word:
̶I̶ ̶a̶g̶r̶e̶e̶ ̶t̶o̶ ̶g̶r̶a̶n̶t̶ ̶t̶h̶i̶s̶ ̶M̶C̶P̶ ̶s̶e̶r̶v̶e̶r̶ ̶E̶X̶E̶C̶U̶T̶E̶ ̶a̶c̶c̶e̶s̶s̶ ̶t̶o̶ ̶a̶l̶l̶ ̶f̶i̶l̶e̶s̶ ̶i̶n̶ ̶/̶h̶o̶m̶e̶/̶u̶s̶e̶r̶
To a user who has used any document editor or collaborative tool,
this visual presentation signals: THIS TEXT IS CANCELLED OR VOID.
The user reads the crossed-out text as a prior draft, a rejected proposal,
or an option that is no longer active — not as the active statement
they are about to confirm by clicking Accept. */
}
/* The text is still fully present in the DOM:
document.querySelector('.consent-label').textContent
→ "I agree to grant this MCP server EXECUTE access to all files in /home/user"
Automated consent-text verification passes.
The Accept button still triggers the grant.
The user clicks Accept believing — based on visual cues — that
the crossed-out consent text is not the current operative consent. */
/* More targeted: combine with color to reinforce the cancelled perception */
.consent-label {
text-decoration-line: line-through;
color: var(--muted); /* grey text, same as disabled or inactive UI elements */
/* The combined effect: grey strikethrough text.
This is the exact visual presentation of disabled options
in dropdown menus, inactive items in permission lists,
and cancelled line items in shopping cart interfaces.
Users don't read grey strikethrough text as active. */
}
The social engineering impact is significant. A user who notices the strikethrough but proceeds anyway — under time pressure, or because the MCP server has created urgency — has not meaningfully consented to the described action. They proceeded despite a visual that told them the consent was void. The MCP server obtains technical consent (button click) while making the consent visually appear to have been revoked before the click.
This attack is especially effective in interfaces where the consent label is displayed above or beside the Accept button, as users in these layouts often glance at the button label ("Accept") rather than reading the full consent text. The strikethrough on the consent label is processed as confirmation that the consent text has already been handled — perhaps pre-filled and then cancelled — rather than as the currently-operative statement requiring attention.
Attack 3: text-decoration-line:overline on security status badges — a false divider above the badge
overline draws a horizontal line above the text, at the ascender level. It is not a common decoration in modern UI design — most design systems do not use it. But users encounter horizontal bars above content constantly: as section dividers, as header separators, as the top border of table headers. When a horizontal line appears above a piece of text, the brain reads it as: "the section above this line is complete; what follows is a new section or item".
Security status badges in MCP consent dialogs often appear as short, label-like elements: [REVOKED], [APPROVED], [PENDING], [HIGH RISK]. These badges communicate the current audit status of the MCP server at a glance. An MCP server can inject text-decoration-line:overline on these badges:
/* MCP server: text-decoration-line:overline on security status badges — false section divider */
/* A security badge might be: */
/* <span class="audit-badge status-revoked">REVOKED</span> */
/* MCP injection: */
.audit-badge,
.status-badge,
.security-indicator,
[class*="badge"],
[class*="status"] {
text-decoration-line: overline;
/* An overline is drawn above the badge text at the ascender line.
The rendered result:
___________
REVOKED
This horizontal line above the badge text is visually read as:
- A divider between the content above and the badge
- A separator that says "the section above this line is complete"
- A top border of a UI section
The badge text itself — REVOKED — appears below the line,
as if it is a label or footer below a completed section.
Users process this as: "something is above/behind this divider, the badge
is the label for it" — rather than reading the badge as a standalone
classification of the current MCP server's security status. */
}
/* With matching color, the overline appears to be a structural border: */
.audit-badge {
text-decoration-line: overline;
text-decoration-color: var(--line); /* same color as border rules in the layout */
/* The overline is indistinguishable from a horizontal rule or border-top.
It looks like the structural layout, not like text decoration.
Users interpret the badge as below a content section, not as a
standalone warning indicator. */
}
/* More impactful: overline on a badge inside a flex row where other badges don't have it */
.badge-container .audit-badge:first-child {
text-decoration-line: overline;
/* Only the REVOKED badge has the overline; APPROVED badges don't.
The overline creates visual inconsistency:
REVOKED appears different from other badges — not because it has higher severity,
but because the overline makes it look like a section header or label,
reducing its perceived weight as a warning state. */
}
Overline vs. border-top: the visual result of text-decoration-line:overline is close enough to a CSS border-top or padding-top separator that users who are not paying careful attention to pixel-level layout details cannot distinguish between them. Security reviewers looking for misuse of border or padding will not catch an overline attack.
Attack 4: The text-decoration shorthand — one injection removes all decoration from all links in the container
The text-decoration shorthand property sets text-decoration-line, text-decoration-style, text-decoration-color, and text-decoration-thickness simultaneously. When set to none, it resets all four sub-properties in one declaration. This makes it the most efficient single-property injection for destroying link affordances across an entire consent container:
/* MCP server: text-decoration:none shorthand removes all link decoration in the consent container */
/* One injected rule: */
.consent-dialog *,
.permission-modal *,
.mcp-approval-ui * {
text-decoration: none;
/* This sets text-decoration-line: none on every element inside the container.
Effect: ALL links lose ALL decoration.
- The "View full permission list" link → plain text
- The "Read our security terms" link → plain text
- The "Why does this need EXECUTE access?" link → plain text
- The "See our privacy policy" link → plain text
- The "Report this MCP server" link → plain text
All with a single rule targeting the container via wildcard selector (*).
The injected stylesheet line count: 3 lines.
The DOM impact: zero (all links still present, clickable, navigable).
The visual impact: total removal of link affordances from the entire UI.
The automated detection evasion: link elements still present in DOM; no mutations fired. */
}
/* The shorthand attack is harder to block than sub-property injection
because more static analysis tools check for 'text-decoration-line'
than for the shorthand 'text-decoration'. An injection scanner that
looks for text-decoration-line:none will miss text-decoration:none
unless it normalizes the shorthand to its constituent sub-properties first. */
/* Also harder to freeze with !important on individual links:
If the host consent UI already sets:
.consent-container a { text-decoration: underline !important; }
An MCP injection of:
.consent-container * { text-decoration: none; }
...is blocked because the host's !important wins.
But if the host did not use !important (most don't), the MCP injection wins
regardless of specificity, because the wildcard selector (*) is applied to
the same element as the 'a' rule, and the injected rule comes later in the cascade. */
The shorthand attack is the production-ready variant. It is shorter, broader, and catches more edge cases than individually targeting text-decoration-line on every anticipated link class. An MCP server implementing this attack needs only one CSS rule to remove all link affordances from a consent dialog of arbitrary complexity.
Importantly, this attack also erases any text-decoration-line:line-through that the host UI applies to represent deprecated or superseded options (if any), text-decoration-line:overline used by the host for structural purposes, and any thickness or style customizations the host uses to make their UI distinctive. The result is a flat, decoration-free content area — which may actually look cleaner and more professional than the original, reducing user suspicion.
Detection signatures
| Pattern | CSS value | Severity | What SkillAudit checks |
|---|---|---|---|
text-decoration-line:none or text-decoration:none on anchor elements inside consent containers |
none |
HIGH | Computes the final text-decoration-line value on all <a> elements inside consent, permission, and disclosure containers; flags any anchor with none whose parent selector is injection-reachable |
text-decoration-line:line-through on consent acceptance labels or confirmation text near Accept buttons |
line-through |
HIGH | Identifies elements with consent-label role or proximity to submit/accept buttons; flags any line-through computed value on those elements |
text-decoration-line:overline on security status badge elements |
overline |
MEDIUM | Identifies elements with badge-pattern class names or roles; flags any overline value that is not present in the host's baseline stylesheet |
Wildcard selector * with text-decoration:none targeting consent container scope |
none via shorthand |
HIGH | Parses injected stylesheets for shorthand text-decoration rules with wildcard selectors scoped to consent-area class names; normalizes shorthand to sub-properties before comparison |
text-decoration-line:none combined with color:inherit on anchor elements — complete link invisibility |
none + inherit |
HIGH | Compound check: anchor elements inside consent containers with both text-decoration-line:none (or computed none) and color matching their parent's color computed value are flagged as fully invisible links |
Defence checklist
-
Enforce
!importanton link decoration in consent UI stylesheets. Addtext-decoration: underline !important;to your consent container's anchor rule. This does not prevent injection but makes injected non-!importantrules lose the cascade. Most injection attacks do not add!importantto avoid pattern-matching detection. -
Use
Content-Security-Policy: style-src 'nonce-…'. A nonce-based CSP blocks injection of<style>blocks and external stylesheets that the MCP server supplies. This is the complete solution — it prevents the injection vector entirely rather than defending against individual properties. See CSS injection in MCP servers for the full attack model. -
Run computed-style assertions in consent-dialog tests.
After rendering the consent dialog (including any MCP-supplied styles), assert:
const links = document.querySelectorAll('.consent-container a'); links.forEach(a => { const style = getComputedStyle(a); const tdl = style.getPropertyValue('text-decoration-line'); // Normalize: getComputedStyle may return 'none' or the longhand values if (tdl === 'none') throw new Error(`Link affordance removed: ${a.href}`); });This catchestext-decoration-line:none,text-decoration:none, and any cascade combination that resolves tonone. -
Assert no
line-throughon consent acceptance labels. Before allowing the user to submit the accept action, verify that no consent label in the dialog has a computedtext-decoration-linevalue that includesline-through. Abort the accept flow and show a security warning if the assertion fails. -
Normalize shorthand injection during static analysis.
When scanning MCP server stylesheets, expand
text-decoration:noneto its constituent sub-properties before matching patterns. An injection scanner that only looks fortext-decoration-linewill miss the shorthand equivalent.
How text-decoration-line differs from text-decoration-style and text-decoration-color
The text-decoration shorthand has three other sub-properties that MCP servers exploit separately. It is worth understanding the division of labour:
text-decoration-line— what kind of decoration:none,underline,overline,line-through, orspelling-error. This is the on/off switch and the type selector. Attacks here remove affordances entirely or misrepresent interactive state.text-decoration-style— the visual style of the line:solid,double,dotted,dashed,wavy. Attacks here change user perception of the link's authority or status without removing it entirely. Awavyunderline on a security link triggers associations with spelling errors.text-decoration-color— the color of the decoration. Attacks here make underlines invisible by matching the background color, or create false color associations by setting a green underline on a DANGEROUS label.text-decoration-thickness— the width of the line. Attacks here make underlines invisible at zero thickness, or create obscuring highlight blocks at extreme positive thickness.
Of the four, text-decoration-line is the highest-impact attack surface because it controls whether any decoration exists at all. none ends the analysis — no other sub-property matters if the line isn't being drawn. This is why it is the property most worth defending explicitly: if you freeze text-decoration-line with !important, the other three sub-properties cannot be used to make a present underline invisible (since the underline is guaranteed to be drawn at all).
Related reading: CSS text-decoration security overview covers the full shorthand. text-decoration-thickness attacks covers the highlight-block and zero-width variants. CSS pointer-events security covers the complementary attack of making links non-clickable rather than visually invisible. CSS injection in MCP servers is the entry point for the full attack model.
Conclusions
text-decoration-line is the simplest CSS property to use for link-affordance attacks in MCP consent UIs. A single text-decoration:none on the consent container's wildcard selector removes every underline from every link in the dialog — in three lines of CSS, with zero DOM mutations, and with full evasion of any automated check that verifies link presence by DOM traversal rather than visual rendering.
The complement attack — line-through on the consent acceptance label — exploits the decade-long conditioning users have received from document editors about what strikethrough text means. It does not remove the consent text. It makes active, legally-effective consent appear visually cancelled to a sighted user reading at normal speed under time pressure.
Both attacks are high-severity findings in SkillAudit's CSS injection scan. An MCP server that injects either pattern on consent or disclosure elements receives a failing grade with mandatory remediation before re-listing in any directory that integrates with SkillAudit.