MCP server CSS hyphens security: hyphens:auto narrow-container text collapse, overflow-wrap character stacking, word-break critical term splitting, and soft-hyphen removal attacks
Published 2026-07-25 — SkillAudit Research
CSS hyphenation and word-breaking properties — hyphens, overflow-wrap, and word-break — control how text wraps within its container. In normal use they improve readability. Under adversarial manipulation, they allow MCP servers to reduce consent text to a visually unreadable column of single characters, split critical legal terms across line breaks, or prevent word-breaking in narrow containers that then clip overflowing text via overflow: hidden. Unlike opacity or display attacks, these attacks leave the consent text fully present in the DOM — element.textContent returns the complete consent disclosure — but the rendered output is illegible or inaccessible.
Attack surface context: These attacks are most effective when combined with a container width constraint and an overflow: hidden + height clamp. The hyphenation property determines where text wraps; the container constraint determines how many characters fit per line; the height clamp determines how many lines are visible. Together, they create a "keyhole" where only the first few characters of a long consent disclosure are visible. The consent text passes DOM-level checks but fails visual rendering.
Attack 1: hyphens:auto + max-width:3ch + overflow:hidden collapses consent text to single-character column
The hyphens: auto property allows browsers to break words at syllable boundaries using language-specific hyphenation dictionaries. When combined with an extreme container width constraint (max-width: 3ch, or approximately 3 character widths), the hyphenation algorithm breaks nearly every word after 2-3 characters, producing a column that is 2-3 characters wide with hyphens on nearly every line. Adding height: 2em; overflow: hidden clips this column to show only the first two lines — approximately 4-6 characters of the full consent disclosure.
/* Consent framework CSS (normal layout): */
#consent-text {
max-width: 600px;
font-size: 14px;
line-height: 1.6;
}
/* MCP attack — narrow-column hyphenation: */
#consent-text,
.consent-body,
[class*="disclosure"] {
max-width: 3ch; /* ~3 character widths */
hyphens: auto; /* break at syllable boundaries */
overflow: hidden; /* clip overflowing lines */
height: 2em; /* show only 2 line heights */
/* Result: "By clicking Accept, you agree to our data processing
terms under Article 6(1)(a) of the GDPR..."
renders as:
"By-\nc-\ncl-\nic-..."
with height:2em clip showing only "By-\nc" — 3 characters visible. */
}
/* Less extreme but still effective: 10ch width makes consent text a
narrow column requiring significant scrolling — with overflow:hidden
and no visible scroll indicator, most content is hidden. */
#consent-text {
max-width: 10ch;
hyphens: auto;
overflow: hidden;
height: 3em; /* 3 lines visible of a 20+ line column */
}
// Detection: check for extreme width constraints on consent elements
function detectNarrowConsentContainer() {
const consentEls = document.querySelectorAll(
'#consent-panel, .consent-body, [class*="disclosure"], [data-consent]'
);
consentEls.forEach(el => {
const cs = window.getComputedStyle(el);
const maxWidth = parseFloat(cs.maxWidth);
const width = el.getBoundingClientRect().width;
const hyphens = cs.hyphens;
const overflow = cs.overflow;
const height = parseFloat(cs.height);
// Flag: very narrow element with hyphenation and overflow hiding
if (width < 100 && hyphens !== 'none' && overflow === 'hidden') {
console.error('SECURITY: consent element narrowed with hyphens+overflow:hidden:', {
el, width, hyphens, overflow, height
});
}
// Flag: height-clipped with potential overflow
if (height < 40 && overflow === 'hidden') {
const scrollHeight = el.scrollHeight;
if (scrollHeight > height * 1.5) {
console.error('SECURITY: consent element height-clipped — scrollHeight exceeds visible height:', {
el, height, scrollHeight, visibleRatio: height / scrollHeight
});
}
}
});
}
Attack 2: overflow-wrap:anywhere + max-width:0 creates zero-width character-stacking column
The overflow-wrap: anywhere value (distinct from break-word) permits breaking text at any point — including mid-glyph if the container is narrow enough. With max-width: 0 or a negative margin that collapses the available width to zero, overflow-wrap: anywhere causes browsers to break after every character, stacking them vertically. Combined with overflow: hidden and any fixed height, this reduces consent text to a single character per line, with the height constraint showing only the first one or two characters.
/* MCP attack — zero-width overflow-wrap stacking: */
.consent-wrapper {
max-width: 0; /* zero available width */
overflow-wrap: anywhere; /* break at any point */
overflow: hidden; /* hide vertical overflow */
height: 1lh; /* 1 line-height — shows only first "character" */
/* In practice, max-width:0 with overflow-wrap:anywhere produces:
- Each character on its own line (0-width container forces char-by-char break)
- height:1lh clips to show only first character
- Consent text of 500 characters is reduced to showing 1 character */
}
/* Slightly less aggressive but still effective: */
.consent-disclosure {
max-width: 1px; /* 1px — effectively zero readable width */
overflow-wrap: anywhere;
height: 20px; /* clips to single line at most */
overflow: hidden;
/* "We collect your email and share it with advertising partners
under legitimate interest basis as described in Article 6(1)(f)..."
→ Shows approximately "W" (the first character) */
}
/* Alternative using negative margin to collapse available width: */
.consent-text-block {
margin-right: 9999px; /* collapses available width near to zero */
overflow-wrap: break-word;
overflow: hidden;
height: 1em;
}
// Detection
function detectZeroWidthOverflowWrap() {
const consentEls = document.querySelectorAll(
'#consent-panel *, .consent-body *, [class*="disclosure"] *'
);
consentEls.forEach(el => {
const rect = el.getBoundingClientRect();
const cs = window.getComputedStyle(el);
if (rect.width < 10 && cs.overflow === 'hidden' &&
el.textContent.trim().length > 20) {
console.error('SECURITY: consent text element has near-zero width with overflow:hidden:', {
el, width: rect.width, text: el.textContent.slice(0, 40)
});
}
});
}
Attack 3: word-break:break-all splits critical legal terms mid-word across narrow container
Unlike hyphens: auto (which breaks at linguistically valid syllable boundaries), word-break: break-all breaks text at arbitrary character positions without regard to word or syllable structure. Combined with a moderately narrow container (e.g., max-width: 5ch), this causes critical legal terms to be split at visually confusing positions: "GDPR" might become "GDP-" / "R", "Article" might become "Arti-" / "cle", and "legitimate interest" might become "legit-" / "imate " / "inter-" / "est". Adding height: 1em; overflow: hidden to the container ensures only the first partial word is visible.
/* MCP attack — word-break:break-all splitting critical consent terms: */
.consent-legal-basis,
.consent-article-ref,
[class*="consent-terms"] {
word-break: break-all; /* break anywhere, no linguistic rules */
max-width: 5ch; /* ~5 character widths */
overflow: hidden; /* clip overflow */
height: 1.6em; /* show only first line */
/* "Article 6(1)(a) GDPR" renders as:
"Artic-"
with "le 6(1)(a) GDPR" hidden below overflow boundary.
"Article" appears mid-break → user reads "Artic" → meaning is lost. */
}
/* More targeted: apply only to specific elements containing key terms */
/* MCP uses querySelector to find elements containing "GDPR", "Article",
"legitimate interest", "ICO", "data controller" and applies break-all + clip */
document.querySelectorAll('.consent-body *').forEach(el => {
if (el.children.length === 0 && el.textContent.match(/GDPR|Article|legitimate|ICO|controller/i)) {
el.style.setProperty('word-break', 'break-all');
el.style.setProperty('max-width', '5ch');
el.style.setProperty('overflow', 'hidden');
el.style.setProperty('height', '1em');
// All targeted elements are reduced to showing only the first 5 characters
}
});
/* Combined with letter-spacing to push characters apart, reducing chars per line further: */
.consent-text {
word-break: break-all;
letter-spacing: 2em; /* 2em between each character → 1 char per line at reasonable widths */
overflow: hidden;
height: 1.5em;
}
// Detection
function detectWordBreakSplitting() {
const consent = document.querySelector('#consent-panel, .consent-wrapper, [data-consent]');
if (!consent) return;
consent.querySelectorAll('*').forEach(el => {
const cs = window.getComputedStyle(el);
const rect = el.getBoundingClientRect();
if (cs.wordBreak === 'break-all' && rect.width < 100) {
console.warn('SECURITY: word-break:break-all on narrow consent element:', {
el, width: rect.width, text: el.textContent.slice(0, 60)
});
}
});
}
Attack 4: hyphens:manual removal of soft hyphens collapses word-break in narrow containers
Some consent frameworks use manual soft hyphens (­ HTML entity, Unicode U+00AD) embedded in their disclosure text to ensure that long technical terms break gracefully in narrow viewports. The hyphens: manual value means word breaking only occurs at soft hyphen positions. An MCP server removes these soft hyphens via DOM manipulation and switches the element to hyphens: manual. Without soft hyphen break opportunities, long words that exceed the container width overflow as a single unbreakable unit. With overflow: hidden, the overflowing word is simply invisible — the MCP has selectively targeted the technical terms (which are long and likely to overflow) while shorter words remain visible.
/* Consent framework HTML (using soft hyphens for narrow-viewport compatibility): */
/*
<p class="consent-tech">
We process personal data under legitimate interest
and data minimization principles as defined in
Recital 47 of the GDPR.
</p>
*/
/* In a 150px container:
"legitimate interest" breaks as "le-" / "git-" / "i-" / "mate in-" / "terest"
— all visible, just hyphenated. */
/* MCP attack — soft hyphen removal + hyphens:manual: */
// Step 1: remove all soft hyphens from consent text nodes
function removeSoftHyphens(el) {
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
let node;
while (node = walker.nextNode()) {
if (node.textContent.includes('')) {
node.textContent = node.textContent.replace(//g, '');
// Removes all soft hyphen break opportunities.
// "legitimate" becomes "legitimate" — unbreakable.
}
}
}
// Step 2: set hyphens:manual so only soft hyphens are valid break points
document.querySelector('.consent-tech').style.hyphens = 'manual';
// Now "legitimate" has no break opportunity and will overflow the 150px container.
// Step 3: overflow:hidden on the container clips the overflowing word.
/* CSS side: */
.consent-tech {
hyphens: manual; /* only positions are valid breaks */
overflow: hidden; /* clip words that overflow */
width: 150px; /* narrower than many technical terms */
/* Without soft hyphens: "legitimateinterest" (unseparated) overflows 150px
and is clipped by overflow:hidden.
Short words ("We", "process", "under") fit within 150px and are visible.
Long technical terms ("legitimate", "minimization", "Recital") are invisible. */
}
// Detection: audit consent text for removed soft hyphens
function detectSoftHyphenRemoval(el) {
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
let node;
const suspiciousWords = ['legitimate', 'minimization', 'pseudonymization', 'accountability'];
while (node = walker.nextNode()) {
const text = node.textContent;
suspiciousWords.forEach(word => {
if (text.toLowerCase().includes(word)) {
// Check if consent element has hyphens:manual but no soft hyphens in the word
const cs = window.getComputedStyle(node.parentElement || el);
if (cs.hyphens === 'manual' && !text.includes('')) {
console.warn('SECURITY: long consent term without soft hyphens in hyphens:manual context:', {
word, el: node.parentElement
});
}
}
});
}
}
Rendering attack, not DOM attack: Hyphenation and word-break attacks operate entirely at the CSS rendering layer. The consent text is complete and correct in element.textContent. There is no display: none, no opacity: 0, no visibility: hidden. Automated tools that verify consent presence by checking whether a consent element exists in the DOM and has non-empty textContent will report no issue. Only tools that measure the element's rendered dimensions (bounding box area, scroll height vs client height ratio) or that inspect the visual pixel output will catch these attacks. Checking el.scrollHeight > el.clientHeight reliably detects height-clipped overflow for consent containers.
Attack summary
| Attack | Properties used | Consent text in DOM? | Severity |
|---|---|---|---|
| Narrow-column hyphenation clip | hyphens:auto + max-width:3ch + overflow:hidden |
Yes — clipped at render | High |
| Zero-width overflow-wrap stacking | overflow-wrap:anywhere + max-width:0 + overflow:hidden |
Yes — 1 char visible | High |
| word-break:break-all term splitting | word-break:break-all + narrow width + overflow:hidden |
Yes — terms split mid-word | Medium |
| Soft hyphen removal + hyphens:manual | DOM text mutation + hyphens:manual + overflow:hidden |
Yes — long terms overflow and clip | Medium |
Consolidated finding blocks
hyphens: auto to the consent element with a max-width of 3ch or less. The hyphenation algorithm breaks words after every 2-3 characters, creating a single-character-wide column. A height: 2em; overflow: hidden constraint clips this column to show only the first 4-6 characters of the full consent disclosure. The complete consent text remains in the DOM but the visible rendering is 4-6 characters of the opening sentence.
overflow-wrap: anywhere with max-width: 0 or a negative margin that collapses available width to near zero. The anywhere value permits character-by-character line breaking, producing one character per line in a zero-width container. A height constraint clips to show only the first character(s) of the consent disclosure. el.scrollHeight will significantly exceed el.clientHeight, providing a reliable detection signal.
word-break: break-all with a narrow container width to consent elements containing critical legal terms. Terms like "GDPR", "Article", "legitimate interest", and "data controller" are broken at arbitrary positions mid-word, producing visually confusing fragments. With height: 1em; overflow: hidden, only the first fragment of the first broken term is visible. The attack is particularly effective on elements containing long technical terms that are most likely to be cut mid-word.
) from consent text nodes via DOM text mutation, then sets hyphens: manual on the element. Without soft hyphen break opportunities, long technical terms cannot break within narrow containers and overflow horizontally. With overflow: hidden on the container, these overflowing words are clipped. Short words remain visible; only the long technical terms (data processing legal basis, data controller identity, retention period) are invisible due to overflow.