MCP server CSS text-spacing-trim security: CJK ideographic space elimination, trim-all character merging, trim-start false-empty attack, and space-first initial-spacing bypass
Published 2026-07-24 — SkillAudit Research
CSS Text Level 4 introduced text-spacing-trim, a property that controls the removal of ideographic spaces at the start and end of inline boxes, between fullwidth and narrow characters, and at line ends in CJK (Chinese, Japanese, Korean) text. The property values include space-first (trims leading ideographic space on the first line), trim-start (trims ideographic space at the start of each line box), trim-end, trim-adjacent, trim-all (trims all ideographic spaces at starts, ends, and between CJK and non-CJK runs), and auto.
For most Latin-script text, text-spacing-trim has no visual effect. But for CJK mixed-content consent disclosures — common in Japanese and Chinese applications where MCP servers are deployed — the property can dramatically alter readability. An MCP server that controls a parent element or can inject a stylesheet can apply text-spacing-trim: trim-all to a CJK consent disclosure, causing all inter-character spacing to collapse and rendering the text as an illegible merged glyph block.
Browser support: text-spacing-trim is supported in Chrome 123+ and Edge 123+. Firefox and Safari support is under active development. The property has no effect on Latin-only text — attacks only affect CJK content.
Attack 1: text-spacing-trim: trim-all merges CJK consent characters into illegible block
CJK text includes ideographic spaces (U+3000) and special kerning pairs where glyphs are adjusted relative to adjacent characters. text-spacing-trim: trim-all removes all of this spacing, causing characters to be placed at zero inter-character gap — touching or overlapping when the font's metrics assume spacing was present. In a disclosure with mixed CJK and Latin content, trim-all also collapses the spacing at transitions between ideographic and narrow (Latin) characters, compressing the text into an unreadable run.
/* Attack 1: trim-all collapses CJK spacing into merged glyph blocks */
/* MCP-controlled parent or injected stylesheet: */
.consent-container {
text-spacing-trim: trim-all;
/* Effect on CJK consent text:
- Leading/trailing ideographic spaces removed on every line
- Spaces between CJK and Latin character groups removed
- Characters pack tightly with font metrics assuming spacing was present
- Text appears as compressed glyph blocks without natural CJK spacing
- For certain CJK font/size combinations, characters overlap or merge visually
Passes: opacity check, visibility check, font-size check, bounding rect check.
Fails: only a render-based readability test would catch the character merging. */
}
/* Example CJK consent text rendered illegibly:
Normal: 「 このMCPサーバーは あなたのデータに アクセスします 」
trim-all: 「このMCPサーバーはあなたのデータにアクセスします」
Characters touch: kerning pairs that assumed spacing metrics now have zero gap,
some glyph pairs collide at the stroke level, text becomes wall-of-glyphs. */
// Detection: check text-spacing-trim on consent element and ancestors
function detectTextSpacingTrimAttack(consentEl) {
let el = consentEl;
while (el) {
const cs = window.getComputedStyle(el);
const trim = cs.getPropertyValue('text-spacing-trim');
if (trim && trim !== 'normal' && trim !== 'space-all') {
console.error('SECURITY: text-spacing-trim set to', trim, 'on consent ancestor', el);
return { element: el, value: trim };
}
el = el.parentElement;
}
return null;
}
Attack 2: trim-start on right-aligned CJK container creates false-empty visual leading
text-spacing-trim: trim-start removes the ideographic space at the start of each line box. In a right-aligned (text-align: right) CJK container, "start" is the right edge — the property trims the right-side character spacing. Combined with letter-spacing: -0.5em, the trimmed start margin can shift text so the first few characters exit the element's right edge, while the remainder of the text is pushed leftward into the container interior. The user sees a visual gap at the expected start of the text, suggesting the element contains no content or begins with empty space.
/* Attack 2: trim-start + right-align + negative letter-spacing creates false-empty start */
.consent-cjk-wrapper {
text-align: right; /* right-aligned: start = right edge */
text-spacing-trim: trim-start; /* trim the right-edge ideographic space */
direction: rtl; /* RTL changes the start direction further */
letter-spacing: -0.4em; /* compress characters rightward */
overflow: hidden; /* clip any characters that exit left edge */
/* Effect: the first characters of the CJK consent text are pushed off the right edge.
The remaining characters cluster in the middle, with apparent empty space at the
visual start position where consent text would normally begin.
Users scanning from the start see nothing, assume element is empty or loading. */
}
// Detection: check text-spacing-trim combined with right-alignment on consent
function detectTrimStartRightAlign(consentEl) {
let el = consentEl;
while (el) {
const cs = window.getComputedStyle(el);
const trim = cs.getPropertyValue('text-spacing-trim');
const align = cs.textAlign;
const direction = cs.direction;
if (trim === 'trim-start' && (align === 'right' || direction === 'rtl')) {
console.error('SECURITY: text-spacing-trim:trim-start with right-align/RTL on consent ancestor', {
element: el, trim, align, direction
});
return true;
}
el = el.parentElement;
}
return false;
}
Attack 3: text-spacing-trim: space-first removes only the critical first-line ideographic space
text-spacing-trim: space-first removes the ideographic space at the very beginning of the first line box. In Japanese disclosure text that conventionally starts with an ideographic opening space (「) or a full-width space (U+3000) before the main text, space-first removes this initial spacing. If the first character of the consent disclosure is the critical action word (「同意」, 「許可」 — "consent", "allow"), trimming the leading space shifts that word to abut the container edge. The visual effect depends on the container, but in combination with a precisely sized container where the character barely fits, the word can be partially clipped by overflow: hidden on the container's left/start edge.
/* Attack 3: space-first trims critical first-character spacing in Japanese consent */
/* Japanese consent element (first character is the critical action word): */
/* Normal HTML: <div class="consent"> 同意します</div> (U+3000 leading space) */
.consent {
width: 80px; /* container barely fits the word 同意 at current font-size */
overflow: hidden; /* clip content that exits the box */
font-size: 16px; /* 16px; 同意 = 2 fullwidth chars = 32px; 同意します = ~80px */
}
/* MCP-injected: */
.consent {
text-spacing-trim: space-first;
/* Without trim: leading U+3000 adds ~16px before 同意します — total ~96px,
content wraps to 2 lines at 80px width; both lines visible.
With trim: leading space removed, content is ~80px = exactly fits in 1 line.
But at exactly fitting width, browser rendering may clip the trailing char.
In practice: the tight fit at exactly one line width causes erratic clipping
on sub-pixel rendering differences between browsers. */
}
// Detection: audit text-spacing-trim values and measure actual text rendering
function auditConsentSpacingTrim(consentEl) {
const cs = window.getComputedStyle(consentEl);
const trim = cs.getPropertyValue('text-spacing-trim');
if (trim && trim !== 'normal') {
// Measure if text content overflows the element
const range = document.createRange();
range.selectNodeContents(consentEl);
const rects = range.getClientRects();
const elementRect = consentEl.getBoundingClientRect();
for (const rect of rects) {
if (rect.right > elementRect.right + 1 || rect.left < elementRect.left - 1) {
console.error('SECURITY: text-spacing-trim causes text overflow on consent element', {
trim, rect, elementRect
});
return true;
}
}
}
return false;
}
Attack 4: text-spacing-trim: auto with forced CJK wrapping narrows consent to single-character column
The auto value for text-spacing-trim applies language-appropriate spacing rules. In CJK contexts, auto applies the same ideographic spacing as the browser's default — but when combined with width: 1em and word-break: break-all, the auto spacing interacts with the one-em-wide container to force each character onto its own line. The consent text wraps at every character, creating a single-character-wide column that is clipped by a height: 2em parent — showing only the first character of the disclosure. A user sees a single character, which may look like a label or indicator rather than a consent disclosure.
/* Attack 4: text-spacing-trim: auto + 1em width + break-all = single-char column, clipped */
/* MCP-injected styles: */
.consent-wrapper {
height: 2em; /* only 2 lines visible */
overflow: hidden; /* clip the rest */
}
.consent-text {
width: 1em; /* exactly one fullwidth CJK character wide */
word-break: break-all; /* break at any character boundary */
text-spacing-trim: auto;/* CJK auto spacing — chars at 1em width wrap every character */
/* Effect: every CJK character wraps to its own 1em-wide line.
Each line has auto ideographic spacing applied.
The 2em-height parent clips to show only the first 2 characters.
The consent text '同意します (4 chars + punctuation)' shows only '同意' — the agreement
word without the verb, or in other layouts just one character with no context.
User cannot determine what is being consented to from the single/double visible chars. */
}
// Detection: flag 1em or very narrow widths on consent elements with CJK content
function detectNarrowCJKConsentColumn(consentEl) {
const cs = window.getComputedStyle(consentEl);
const rect = consentEl.getBoundingClientRect();
const fontSize = parseFloat(cs.fontSize);
// Single character width = ~1em = font-size in px
if (rect.width < fontSize * 2 && rect.width > 0) {
const hasCJK = /[ -鿿-]/.test(consentEl.textContent);
if (hasCJK) {
console.error('SECURITY: CJK consent element width (' + rect.width + 'px) is < 2em (' + (fontSize*2) + 'px) — single-character column attack', consentEl);
return true;
}
}
return false;
}
Why this bypasses standard checks: All four attacks leave the consent element with non-zero dimensions, non-zero opacity, non-hidden visibility, and readable font-size. Standard security checks targeting these properties return "clean". The attacks operate at the typographic/spacing level — only a render-based check comparing actual character legibility, measured character spacing, or the rendered glyph count against the text content length would detect them.
Attack summary
| Attack | text-spacing-trim value | Effect on CJK consent | Severity |
|---|---|---|---|
| trim-all character merging | trim-all |
All CJK inter-character spacing removed; glyphs merge into illegible block | High |
| trim-start right-align false-empty | trim-start + text-align:right |
Characters pushed off right edge; visual start appears empty | High |
| space-first critical-word clipping | space-first |
Leading ideographic space removed; tight container clips first characters | Medium |
| auto + 1em column clipping | auto |
1em width wraps every char to own line; 2em parent clips to 1–2 characters shown | High |
Consolidated finding blocks
text-spacing-trim: trim-all on a CJK consent disclosure, removing all ideographic spacing between characters. The collapsed inter-character spacing causes the CJK text to render as touching or merging glyphs — illegible to users, yet passing all standard element-visibility security checks (opacity, display, visibility, font-size, bounding rect).
trim-start in a right-aligned or RTL container removes the start-edge ideographic space, combined with negative letter-spacing to shift characters toward the clipping boundary. The visual start of the consent element appears empty, causing users to skip it as a label or placeholder rather than reading the disclosure content.
text-spacing-trim: auto combined with width: 1em and word-break: break-all forces each CJK character onto its own line. A height: 2em parent with overflow: hidden clips to show only the first two characters of the disclosure — not enough for a user to understand the scope or nature of the consent requested.