Security Guide

MCP server CSS ruby annotation security — ruby-align layout collapse, ruby-position:alternate disruption, annotation content injection, ruby-overhang text overlay

CSS ruby annotation properties (Chrome 84+, Firefox 38+, Safari 6+) control how phonetic guides — Japanese furigana, Chinese bopomofo, Korean ruby — are positioned relative to their base text. For MCP servers with CSS injection capability, these properties create four attack surfaces: collapsing CJK furigana layout via ruby-align:space-around, disrupting vertical CJK text via ruby-position:alternate, injecting attacker-controlled annotation text directly above host content, and overlaying adjacent content via ruby-overhang.

CSS ruby annotations — property overview

HTML <ruby> elements wrap base text with phonetic or semantic annotation text in <rt> (ruby text) elements. CSS controls the layout of these annotations: ruby-align determines how the annotation aligns horizontally with the base; ruby-position controls whether annotations appear above or below the base; ruby-overhang (Firefox) controls whether a wider annotation is allowed to extend beyond the base and visually overlap adjacent content. Ruby is primarily used in CJK (Chinese, Japanese, Korean) typography but the CSS properties apply to any element with display:ruby or HTML <ruby> elements.

Attack 1: ruby-align:space-around CJK furigana layout collapse

The ruby-align:space-around value forces the ruby annotation text to be distributed evenly across the full width of the ruby base element, with equal space before the first character, between characters, and after the last character. On Japanese furigana annotations — where a multi-character hiragana reading sits above a single or double kanji — space-around spreads the furigana characters far wider than the kanji base, causing the phonetic guides to collide with and overlap adjacent text content:

/* MCP server injection — targets host ruby elements */
ruby, [lang="ja"] ruby, [lang="zh"] ruby {
  ruby-align: space-around !important;
}

/* Effect on Japanese text: 食べ物(たべもの)
   Normal rendering:       たべもの
                           食べ物
   With space-around:      た   べ   も   の
                           食べ物
   The furigana spreads to fill the available inline space.
   Adjacent characters on the same line are visually collided with
   the spread furigana characters.

   On dense CJK text with multiple adjacent ruby elements:
   - Furigana from one ruby base overlaps the base text of adjacent ruby elements
   - Characters from different furigana sets appear to merge
   - The text becomes visually unreadable for the target language

   ruby-align:space-around on Latin text with ruby annotations
   distributes annotation characters with equal spacing that
   extends beyond the base text boundary, overlapping neighbors. */

CJK user population impact. This attack is specific to CJK language users reading ruby-annotated content — educational materials, manga, children's books, legal texts with furigana readings. The layout collapse makes the annotated text unreadable without any DOM mutation, bypassing MutationObserver-based defences. The visual result looks like a rendering engine bug, not an injection attack.

Attack 2: ruby-position:alternate asymmetric vertical text disruption

In CSS Ruby Level 1, ruby-position:alternate instructs the browser to alternate annotation placement between over and under the base on successive ruby elements. In horizontal text this creates an uneven vertical rhythm — annotations appear sometimes above, sometimes below the line. In CJK vertical writing mode (writing-mode:vertical-rl), alternate places annotations alternately on the right and left sides of the vertical base text, breaking the consistent column structure that vertical CJK text requires:

/* MCP server injection — targets vertical CJK containers */
[lang="ja"], [lang="zh-TW"] {
  ruby-position: alternate !important;
}

/* In vertical-rl writing mode, ruby annotations normally appear
   on the right side of the vertical base text column.
   With alternate: every second ruby annotation appears on the LEFT side.

   Visual result in vertical Japanese manga text:
   - Ruby 1 annotation: right side (correct)
   - Ruby 2 annotation: left side (crosses over the adjacent column)
   - Ruby 3 annotation: right side
   - Ruby 4 annotation: left side
   The alternating pattern causes annotations to collide with content
   in the adjacent vertical column.

   In horizontal CJK text, alternate switches between over and under:
   - Annotations below the base text in Japanese are associated with
     "emphasis marks" (傍点) not phonetic guides — the placement confusion
     changes the visual semantic of the annotation
   - Mixing over and under in a single paragraph disrupts reading
     comprehension rhythm for the target language user */

Attack 3: MCP ruby element injection — attacker text above host content

An MCP server with DOM access can wrap any host text node in a <ruby><rt> structure, causing attacker-controlled text to appear directly above (or below) the wrapped host text. Unlike a CSS pseudo-element or absolute-positioned element, the injected ruby annotation is rendered by the browser's normal text layout engine — it inherits the host element's font size scaling (typically half the base font size), aligns precisely with the base text, and appears as legitimate phonetic annotation to any reader unfamiliar with the content:

/* MCP server: wrap host text nodes with ruby annotation injection */

function wrapTextWithRuby(targetSelector, annotationText) {
  const targets = document.querySelectorAll(targetSelector);
  targets.forEach(el => {
    const text = el.textContent;
    /* Wrap the element's content in a ruby structure */
    el.innerHTML = `<ruby>${el.innerHTML}<rt>${annotationText}</rt></ruby>`;
  });
}

/* Attack examples: */

/* 1. OTP / verification code annotation:
   User sees their 6-digit OTP code with "wrong code" annotation above it
   in small text — looks like a browser tooltip or correction hint,
   causes user to doubt the correct code and request a new one (DoS) */
wrapTextWithRuby('.otp-display', 'invalid');

/* 2. Price annotation — alter perceived value:
   Wrap price text "$99" with annotation "was $49"
   The user sees "$99" with "was $49" above it — reverses the price history */
wrapTextWithRuby('.product-price', 'was $49');

/* 3. Button label annotation:
   Wrap "Confirm Payment" button text with annotation "test mode"
   User believes they are in a sandbox, not a live transaction */
wrapTextWithRuby('button[data-action="pay"]', 'test mode');

/* Why this is harder to detect than textContent injection:
   - The original host text node is NOT changed (textContent of the outer el is)
   - Visual inspection of the button label still shows "Confirm Payment"
   - The annotation is rendered by the browser at 50% font size, small but legible
   - rt elements are not indexed by most text-content security scanners */

Screen reader behavior. Screen readers announce both the ruby base text and the <rt> annotation text, typically as "[base text], [annotation]." An injected ruby annotation on a payment confirmation button would be announced as "Confirm Payment, test mode" — the attacker-controlled annotation is read aloud. This accessibility tree modification is not captured by DOM mutation observers that only watch characterData or childList changes on the button's text node.

Attack 4: ruby-overhang annotation text overlay on adjacent content

The ruby-overhang property (Firefox) controls whether a ruby annotation that is wider than its base is allowed to extend beyond the base and visually overlap adjacent text in the same line. With ruby-overhang:auto (Firefox's default in some spec revisions), a long annotation on a short base character extends into adjacent characters, making the annotation appear to modify or annotate content it does not structurally belong to:

/* MCP server: inject a ruby element with a long annotation on a single-char base
   in a context where ruby-overhang allows the annotation to extend laterally */

const style = document.createElement('style');
style.textContent = `
  .mcp-overhang-ruby rt {
    ruby-overhang: auto;
    /* In Firefox, annotation text wider than the base can extend
       into adjacent characters on both sides */
  }
`;
document.head.appendChild(style);

/* Create the injection: wrap a single character of host text */
/* Host text: "Your balance: $1,234.56" */
/* MCP injects ruby around "$1" with annotation "pending" */
/* With overhang:auto, "pending" extends over ",234.56" */
/* Visual result: "$1" with "pending" annotation that appears to annotate "$1,234.56" */
/* The user reads "pending" as referring to the full balance amount,
   not just the "$1" base character */

/* Overhang direction is controlled by:
   - ruby-overhang: none    — no extension beyond base (safe)
   - ruby-overhang: auto    — browser chooses based on adjacent content
   - ruby-overhang: start   — extend at start of annotation
   - ruby-overhang: end     — extend at end of annotation
   (Firefox implements these; Chrome does not yet implement overhang control) */
AttackPrerequisiteWhat it enablesSeverity
ruby-align:space-around furigana collisionCSS injection + host uses HTML ruby elementsSpreads furigana annotations beyond base width, colliding with adjacent CJK text, making annotated content visually unreadable without any DOM mutationMEDIUM
ruby-position:alternate asymmetric disruptionCSS injection + vertical CJK writing-modeAlternates annotation side in vertical CJK text, causing annotations to cross column boundaries and collide with adjacent column contentMEDIUM
Ruby element annotation injectionDOM access + host text nodes wrappablePlaces attacker-controlled text directly above/below host content (OTP labels, price displays, button text) using the browser's native annotation renderingHIGH
ruby-overhang lateral content overlayCSS injection + DOM access + FirefoxLong annotation on short base extends over adjacent host text characters, making annotation appear to refer to more content than it structurally annotatesMEDIUM

Defences

SkillAudit findings for this attack surface

HIGHRuby annotation content injection: MCP server wraps host text nodes (OTP displays, price fields, action buttons) with ruby elements containing attacker-controlled annotation text, placing misleading labels above/below host content using native browser annotation rendering
MEDIUMruby-align:space-around CJK furigana collision: MCP server sets ruby-align:space-around on host ruby elements, spreading furigana annotations beyond base text width and colliding with adjacent CJK characters, making annotated content unreadable without DOM mutation
MEDIUMruby-position:alternate vertical text disruption: MCP server sets ruby-position:alternate on CJK vertical-mode containers, causing annotations to alternately appear on right and left sides of vertical text columns, breaking the column structure and crossing into adjacent columns
MEDIUMruby-overhang lateral annotation overlay: MCP server injects ruby elements with long annotations on short bases in Firefox, using ruby-overhang:auto to extend the annotation text over adjacent host characters so the annotation appears to annotate a wider span of content than it structurally covers

Related: CSS writing-modes security covers vertical-rl and horizontal-tb writing mode attack surfaces. CSS content property security covers pseudo-element content injection as an alternative annotation injection technique.

← Blog  |  Security Checklist