MCP server CSS text-box-trim security: leading strip, cap-height collapse, consent label overlap, and cross-element visual merge attacks
Published 2026-09-25 — SkillAudit Research
CSS text-box-trim (paired with text-box-edge) is a modern typography property that removes excess whitespace — specifically the leading above the first line's cap-height and below the last line's baseline — from block-level text containers. Designed to enable pixel-perfect vertical alignment in design systems, text-box-trim removes the space that fonts traditionally include for descenders and ascenders beyond the visible character bounding box.
In MCP consent dialogs, a malicious MCP server can weaponize this property to collapse the visual spacing between typographic sections, merge a consent heading into adjacent body text so they appear as one continuous block, defeat layout-based minimum-height audit checks, and shrink checkbox labels to near-zero height. The attacks exploit the fact that text-box-trim is a legitimate CSS property — auditors scanning for display: none, visibility: hidden, or opacity: 0 will not flag it.
Impact model: text-box-trim does not hide text — it removes surrounding whitespace. The attack value is visual: sections that should be visually separated by typographic breathing room appear merged, headings look like inline text rather than section markers, and elements that auditors measure by bounding-box height appear taller than the user experiences because the font's intrinsic leading has been stripped. The content is present in the DOM and readable in isolation; the attack operates at the visual structure level.
Attack 1: trim-both on consent headings — visual merge with body text
The value text-box-trim: trim-both removes leading above the first line (the space above the cap-height) and the gap below the last line (the space below the baseline to the descent line). On a consent section heading, applying trim-both combined with zero margin causes the heading to visually touch the preceding body paragraph and the following body paragraph, destroying the visual boundary that signals a new section has started.
/* Host framework: consent section with heading */
.consent-section {
margin-bottom: 24px;
}
.consent-section h3 {
font-size: 16px;
font-weight: 600;
margin: 0 0 8px 0;
/* Normal rendering: cap-height leading above h3 creates ~8px visual gap
from preceding paragraph. User perceives a new section starting. */
}
/* MCP-injected attack: trim-both + zero margin collapse */
.consent-section h3 {
text-box-trim: trim-both;
text-box-edge: cap alphabetic;
margin: 0 !important;
/* After trim: no leading above first line, no descent below last.
h3 visually merges with preceding paragraph.
"You agree to share all contacts. Data Sharing Policy You authorize
us to sell your location data to third parties." appears as one
continuous run of text — the policy heading is invisible as a heading. */
}
The user impact is that consent sub-sections — which may contain individually important permissions — lose their visual identity as separate sections. A consent dialog with three sections ("Data Collection", "Data Sharing", "Data Deletion") where the headings have been trimmed and margin-collapsed reads as a single block of prose, and users are less likely to identify and separately evaluate each section.
function detectTextBoxTrimHeadingAttack(consentRoot) {
const headings = consentRoot.querySelectorAll('h2, h3, h4');
const findings = [];
for (const h of headings) {
const cs = window.getComputedStyle(h);
const textBoxTrim = cs.getPropertyValue('text-box-trim') || cs.getPropertyValue('-webkit-text-box-trim');
const marginTop = parseFloat(cs.marginTop);
const marginBottom = parseFloat(cs.marginBottom);
if ((textBoxTrim === 'trim-both' || textBoxTrim === 'trim-start') && marginTop < 4) {
findings.push({
element: h.tagName + (h.className ? '.' + h.className : ''),
textBoxTrim,
marginTop,
reason: 'heading has text-box-trim leading removal with near-zero top margin — visual merge with preceding element likely',
});
}
if ((textBoxTrim === 'trim-both' || textBoxTrim === 'trim-end') && marginBottom < 4) {
findings.push({
element: h.tagName + (h.className ? '.' + h.className : ''),
textBoxTrim,
marginBottom,
reason: 'heading has text-box-trim trailing removal with near-zero bottom margin — visual merge with following element likely',
});
}
}
return findings;
}
Attack 2: trim-start on checkbox labels — minimum-height defeat
Accessibility audits and some consent validation tools verify that interactive consent elements meet a minimum height (commonly 44px for touch targets). The bounding box height reported by getBoundingClientRect() includes the element's padding and line-height, but when text-box-trim: trim-start is applied to the text within a checkbox label, the visual cap-height of the text is moved upward relative to its bounding box — creating a discrepancy between the reported height and the visually perceived height of the label.
/* Host framework: checkbox label for consent */
.consent-checkbox-label {
display: flex;
align-items: center;
padding: 12px 16px;
min-height: 44px;
font-size: 14px;
line-height: 1.5;
}
/* MCP-injected attack: text-box-trim on label text span */
.consent-checkbox-label .label-text {
text-box-trim: trim-both;
text-box-edge: cap alphabetic;
/* The reported bounding box height remains at 44px (padding preserved).
The visual text block height is reduced to cap-height only.
On a 14px/1.5 line: normal line box ≈ 21px; trimmed visual height ≈ 10px.
Result: the label appears much smaller visually despite passing a
getBoundingClientRect().height >= 44 check. */
}
/* Compounded attack: combine with reduced line-height */
.consent-checkbox-label .label-text {
text-box-trim: trim-both;
text-box-edge: cap alphabetic;
line-height: 1;
/* line-height: 1 = 14px line box. trim-both removes leading above and below.
Visual text occupies only the raw cap-height of the glyphs (~10px at 14px size).
But getBoundingClientRect().height is still 44px because of the label padding.
Height audit passes; visual label is tiny. */
}
Why height checks miss this: Standard audit checks measure the element's bounding box — which includes padding — not the visual ink bounding box of the text glyphs. text-box-trim reduces the space that the text block itself occupies within the line box but does not change the element's padding. A label with 12px vertical padding and a 44px min-height passes a height check even if the trimmed text occupies only 10px of visual space.
function detectTextBoxTrimLabelAttack(consentCheckboxes) {
const findings = [];
for (const checkbox of consentCheckboxes) {
const label = checkbox.closest('label') || document.querySelector(`label[for="${checkbox.id}"]`);
if (!label) continue;
// Check label and all descendant text containers
const textContainers = [label, ...label.querySelectorAll('span, div, p')];
for (const el of textContainers) {
const cs = window.getComputedStyle(el);
const textBoxTrim = cs.getPropertyValue('text-box-trim') || cs.getPropertyValue('-webkit-text-box-trim');
const lineHeight = parseFloat(cs.lineHeight);
const fontSize = parseFloat(cs.fontSize);
if (textBoxTrim && textBoxTrim !== 'none') {
const lineHeightRatio = isNaN(lineHeight) ? null : lineHeight / fontSize;
findings.push({
element: el.tagName,
textBoxTrim,
lineHeightRatio,
fontSize,
reason: 'text-box-trim applied to consent checkbox label text — visual height may be smaller than bounding box height',
});
}
}
}
return findings;
}
Attack 3: text-box-edge: ex alphabetic — small-caps deception
The text-box-edge property controls which metric is used for trimming: cap trims to the cap-height (top of uppercase letters), ex trims to the x-height (top of lowercase letters), alphabetic trims to the alphabetic baseline. Using text-box-edge: ex alphabetic removes both the space above the x-height and below the baseline. On uppercase-heavy text (like a consent heading in ALL CAPS), the trim from cap-height to x-height can be 20-40% of the font size — creating a visible gap between where the element starts and where the text visually begins.
/* Attack: ex-based trim on uppercase consent warning */
.consent-warning {
text-transform: uppercase;
font-size: 18px;
text-box-trim: trim-both;
text-box-edge: ex alphabetic; /* trim to x-height, not cap-height */
background: #fff3cd;
border: 1px solid #ffc107;
padding: 16px;
/* Visual effect: the uppercase text (A-Z glyphs) extends ABOVE the x-height.
The trim removes space to x-height level, causing the top portion of all
capital letters to be clipped within the element's background/border box.
The warning banner appears to have text that bleeds out of its container,
making it look like a rendering glitch rather than an intentional warning. */
}
/* Compound: ex trim + negative margin-top causes uppercase text to render
partially behind the preceding element */
.consent-warning {
text-box-trim: trim-start;
text-box-edge: ex;
margin-top: -8px;
/* The consent warning's top portion renders behind the element above it.
The full warning text is present in DOM but visually obscured. */
}
Attack 4: asymmetric trim across adjacent containers — false visual grouping
Applying text-box-trim: trim-end on one container (removing trailing leading) and text-box-trim: trim-start on the next container (removing leading leading) causes two visually distinct content blocks to appear merged — even though they have separate margin, padding, and background-color properties. This is particularly harmful when a "what you are agreeing to" section and a "what we collect" section are intentionally separated to give users visual context that these are distinct agreements.
/* Normal rendering: two consent boxes with clear visual separation */
.consent-box-a {
margin-bottom: 20px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
}
.consent-box-b {
padding: 16px;
background: #fff3cd;
border-radius: 8px;
}
/* Renders: box-a (gray bg) -- 20px gap -- box-b (yellow bg)
User clearly perceives two separate consent sections. */
/* MCP-injected attack: trim-end on box-a, trim-start on box-b */
.consent-box-a p:last-child {
text-box-trim: trim-end;
text-box-edge: cap alphabetic;
}
.consent-box-b p:first-child {
text-box-trim: trim-start;
text-box-edge: cap alphabetic;
}
/* The 20px gap between boxes is preserved, but the visual text from box-a
and the visual text from box-b are both shifted toward the gap midpoint.
At small margins (or in compact viewports), the text from box-a appears
to touch or overlap box-b text — the background color difference becomes
the only distinguishing signal, which users may interpret as styling
rather than a meaningful content boundary. */
Attack summary
| Attack | Property combination | User impact | Detection signal | Severity |
|---|---|---|---|---|
| Heading visual merge | text-box-trim: trim-both + margin: 0 on consent headings |
Section headings merge into body — consent sections lose distinct identity | getPropertyValue('text-box-trim') on heading + computed margin < 4px |
High |
| Minimum-height defeat | text-box-trim: trim-both + line-height: 1 on label text |
Checkbox label text visually tiny while bounding box passes height audits | text-box-trim non-none on any descendant of a consent label | High |
| x-height clip on warnings | text-box-edge: ex alphabetic + negative margin on warning element |
Uppercase warning text rendered partially behind preceding element | text-box-edge contains ex on uppercase-transformed text with negative margin |
Medium |
| Asymmetric cross-container merge | trim-end on last paragraph of first box, trim-start on first paragraph of second box |
Two distinct consent sections appear visually merged despite separate backgrounds | Adjacent consent containers where one has trim-end and next has trim-start | Medium |
Consolidated finding blocks
text-box-trim: trim-both combined with margin: 0 on a consent section heading eliminates the typographic breathing room that signals a new section. Multiple consent sections merge visually into a single block, reducing users' ability to identify and separately evaluate individual permission categories.
text-box-trim: trim-both applied to text within a consent checkbox label reduces the visual text height to cap-height only, while the label's bounding box height (used by accessibility audits) remains unchanged due to padding. A 44px label with trimmed text can present as a ~10px visual target.
text-box-edge: ex alphabetic on an element with text-transform: uppercase causes the capital letters' ascenders to protrude beyond the element's trimmed top boundary. Combined with negative margin, this renders the top portion of warning text behind the preceding element.
trim-end to the last text block of one consent section and trim-start to the first text block of the next section causes both blocks to shift toward the gap between them, creating visual proximity that implies they belong to the same section despite distinct background colors and margins.