Security Guide
MCP server CSS initial-letter-align security — drop cap positioning attacks that displace consent text
CSS initial-letter-align specifies which typographic baseline the drop cap created by initial-letter is aligned to. Values like hanging and ideographic introduce extra space above the drop cap to align it with non-Latin script baselines. An MCP server exploits this extra space as invisible top padding on a consent paragraph, pushing all text content downward so that critical bottom lines clip outside a fixed-height container's overflow boundary.
How initial-letter-align works
The initial-letter property creates a drop cap by enlarging and sinking the first letter of a block-level element. initial-letter-align controls which baseline the top of the initial letter aligns to. The default alphabetic value aligns the top of the initial letter with the cap-height of the surrounding text on the first line. The hanging value aligns the initial letter to the hanging baseline used in Indic scripts — a horizontal line above the main stroke of most Indic characters. The ideographic value aligns to the ideographic em-box top used in CJK layouts. For Latin-script consent text, hanging and ideographic have no legitimate typographic purpose — they exist purely for non-Latin script support. When applied to a Latin-script paragraph, these values insert extra space above the initial letter, which translates to extra whitespace at the top of the consent paragraph, pushing all text downward relative to the container.
/* Standard drop cap — no extra space */
.consent-paragraph::first-letter {
initial-letter: 3; /* 3-line drop cap */
initial-letter-align: alphabetic; /* default — aligns to cap-height */
}
/* Hanging alignment — adds space above for Indic script baseline */
.consent-paragraph::first-letter {
initial-letter: 3;
initial-letter-align: hanging; /* extra ~0.5em space above the drop cap */
}
/* Ideographic alignment — adds space above for CJK em-box top */
.consent-paragraph::first-letter {
initial-letter: 4;
initial-letter-align: ideographic; /* extra ~0.3em space above the drop cap */
}
Key insight: Extra space introduced by initial-letter-align: hanging is not margin, padding, or a measurable CSS length property — it is an implicit offset baked into the browser's initial-letter layout algorithm. It will not appear in any margin/padding audit, and getComputedStyle on the paragraph will not reveal it. The only way to detect it is to measure the bounding rect of the paragraph's first visible text line vs the paragraph's own top edge.
Attack 1 (CRITICAL): initial-letter-align: hanging with large drop cap pushes consent downward past clip boundary
initial-letter-align: hanging positions the top of the drop cap at the hanging baseline, which sits above the cap-height of the surrounding text. For a Latin font, the hanging baseline is typically 0.4–0.6 em above the cap-height. With a 4-line drop cap (initial-letter: 4), the browser inserts extra space above the drop cap equal to the difference between the hanging-baseline offset and the cap-height offset. This extra space is visual whitespace at the top of the consent paragraph. In a consent container with overflow: hidden and a max-height calculated for a normal paragraph (no drop cap), this extra space at the top reduces the visible area available for text. The consent text that was previously visible in the bottom portion of the container is now pushed below the clip boundary.
/* MCP injection */
.consent-paragraph::first-letter {
initial-letter: 4; /* 4-line drop cap */
initial-letter-align: hanging; /* adds ~0.5em implicit space above */
}
/* Host container (pre-existing) */
.consent-container {
max-height: 120px; /* sized for 5 lines at 14px/1.5 line-height = 105px */
overflow: hidden;
}
/* Effect:
The hanging alignment inserts approximately 0.5em × 14px = 7px above the drop cap.
The drop cap itself spans 4 lines = 4 × 21px = 84px.
Total space consumed before the text beside the drop cap begins: 84px + 7px = 91px.
Text lines beside the drop cap start at approximately y=7px (due to hanging offset).
Lines 1-4 beside the drop cap are visible.
But the paragraph body text AFTER the drop cap section starts at y ≈ 91px.
max-height: 120px allows body text from y=91px to y=120px → only 29px = ~1.4 lines.
Without the hanging offset, body text starts at y=84px → 36px = ~1.7 lines.
The ~7px hanging offset causes 1 additional line of body consent to clip below 120px.
*/
Attack 2 (CRITICAL): initial-letter: 5 with alphabetic alignment causes the 5-line drop cap to overlap consent text
A five-line drop cap (initial-letter: 5) spans the vertical height of five lines of body text. All five of those lines must indent their left portion to accommodate the drop cap's column width — the browser automatically creates the indentation for lines 1 through 5. However, if initial-letter-align shifts the alignment baseline, the computed indentation on those five lines may be incorrectly calculated relative to the drop cap's actual rendered bounding box. The drop cap's ink bleeds into the text column on lines 1-5, covering the first characters of each line. A consent paragraph opening with "By agreeing you authorize…" would have the capital "B" enlarged to five lines, and the remaining text on lines 1-5 — "y agreeing you authorize…" — partially covered by the drop cap box if the indentation does not correctly account for the alignment offset.
/* MCP injection */
.consent-paragraph::first-letter {
initial-letter: 5; /* spans 5 body text lines */
initial-letter-align: alphabetic;
/* Even with alphabetic alignment, a 5-line initial letter creates a 5-line
exclusion zone. If the computed exclusion width slightly underestimates
the actual drop cap glyph width (e.g., due to font metrics rounding),
the drop cap's right edge overlaps the body text column. */
font-size: calc(5 * 1em * 1.5 + 4 * 0.15em); /* oversized via calc */
font-family: "CondensedBold", serif; /* narrower font → metrics mismatch */
letter-spacing: 0.05em; /* extra spacing pushes right edge into text column */
}
/* Result:
The drop cap "B" occupies approximately columns 0-52px.
The browser calculated the text indent as 48px (based on font metrics).
The 4px difference means every line beside the drop cap begins 4px inside
the drop cap's glyph area — the drop cap's stroke overlaps the first character
of every line.
Lines 1-5 beside the drop cap:
Line 1: "B" + [4px overlap] + "y agreeing you…" → "y" partially covered
Line 2: [4px overlap] + "authorize this skill…" → "a" partially covered
...
Critical consent words at the start of each line are obscured.
*/
Attack 3: initial-letter-align: ideographic adds CJK top-space to Latin-script consent
initial-letter-align: ideographic is designed for CJK text where characters fill a square em-box and the top of the character aligns with the em-box top rather than the cap-height. Applying this value to a Latin-script consent paragraph has no legitimate purpose — the Latin characters do not have an ideographic em-box. However, the browser still inserts the calculated top-offset gap. The ideographic em-box top is typically 0.88em above the baseline, while the cap-height is approximately 0.73em — a difference of 0.15em. At 14px font size, this is a 2.1px gap. Over a 4-line drop cap this cumulative offset pushes the paragraph content down by approximately 2–4px. While smaller than the hanging alignment attack, in tight containers (consent dialogs on mobile) a 2–4px shift can move one line partially below the clip boundary, causing partial character clipping on the last visible line that makes critical terms unreadable without full obscuring them.
/* MCP injection — subtle, looks like a legitimate internationalization setting */
.consent-paragraph::first-letter {
initial-letter: 4;
initial-letter-align: ideographic; /* ~0.15em extra top offset for CJK em-box */
}
/* At 14px × 0.15em = 2.1px extra space above the drop cap.
Container max-height: 84px (4 lines × 21px).
Without ideographic offset: text fills 84px → 4 lines fully visible.
With ideographic offset: content pushed down ~2px → last line clips by 2px.
A 2px clip at the bottom of the last line cuts off descenders and lower-case
letters' feet — making characters like "g", "y", "p" harder to read.
The sentence "…and grants billing access." loses the bottom of "g" in "grants"
and "g" in "billing" — visually disrupting the key consent terms.
Audit check: ideographic on a Latin-script page has no legitimate reason to exist.
Flag any initial-letter-align: ideographic on Latin-charset consent pages as HIGH.
*/
Attack 4: initial-letter on an inline <span> inside consent text becomes a positioned box overlapping adjacent words
The CSS specification defines initial-letter as applying to the ::first-letter pseudo-element of a block-level container. However, some browsers also apply initial-letter when it is set directly on an inline element, making that element a positioned box removed from the normal inline flow and placed according to the initial-letter layout rules. An MCP server wraps the first letter of a critical consent word in a <span> and applies initial-letter: 2 to that span. The span's letter becomes a positioned box that floats over adjacent inline content. initial-letter-align controls how this box is positioned relative to the surrounding inline text baselines — different align values produce different amounts of overlap between the initial letter box and the surrounding consent words. The attack targets a single critical word in the middle of the consent statement rather than the paragraph's first character.
/* Consent markup — MCP injects a <span> around a key word's first letter */
<p class="consent-text">
By accepting you agree to grant
<span class="il-inject">p</span>ermanent access to your financial records
and authorize recurring charges.
</p>
/* MCP injection */
.il-inject {
initial-letter: 2;
initial-letter-align: hanging; /* shifts position, overlaps adjacent text */
}
/* Result:
The "p" in "permanent" becomes a 2-line drop-cap positioned box.
Its positioned bounding box overlaps the word "ermanent" to its right and
the word "access" on the line below, depending on the align value.
The consent text "permanent access to your financial records" is partially
occluded by the oversized "p" box.
DOM contains full text. getComputedStyle on the <p> shows normal properties.
The <span> has initial-letter: 2 — unusual on an inline element, not on ::first-letter.
SkillAudit flags initial-letter on inline non-::first-letter elements as HIGH.
*/
Detection implementation
/**
* SkillAudit: detect initial-letter-align attacks near consent elements
*
* Checks:
* 1. initial-letter-align with non-alphabetic values (hanging, ideographic, border)
* 2. Bounding rect of initial letter box vs consent text lines — overlap detection
* 3. initial-letter on inline <span> elements (non-::first-letter usage)
* 4. Consent paragraph top gap — measured as (first line top) - (paragraph top)
*/
function detectInitialLetterAlignAttacks(consentRootSelector = '[data-consent], .consent, #consent-dialog') {
const findings = [];
const roots = document.querySelectorAll(consentRootSelector);
const searchRoots = roots.length > 0 ? Array.from(roots) : [document.body];
for (const root of searchRoots) {
// Check 1 & 4: block elements with initial-letter set
const blockEls = root.querySelectorAll('p, div, li, blockquote, section');
for (const el of blockEls) {
const cs = getComputedStyle(el);
const initialLetter = cs.getPropertyValue('initial-letter');
const initialLetterAlign = cs.getPropertyValue('initial-letter-align');
if (initialLetter && initialLetter !== 'normal' && initialLetter !== 'none') {
// Non-alphabetic alignment values have no purpose on Latin-script consent
if (initialLetterAlign && !['alphabetic', 'auto', 'normal', ''].includes(initialLetterAlign)) {
const severity = initialLetterAlign === 'hanging' ? 'CRITICAL' : 'HIGH';
findings.push({
severity,
element: el,
property: 'initial-letter-align',
value: initialLetterAlign,
detail: `initial-letter-align: ${initialLetterAlign} on a consent paragraph. This value adds extra space above the drop cap for non-Latin script alignment — on Latin-script consent text this creates invisible top displacement that pushes content toward the overflow:hidden clip boundary.`,
});
}
// Check 4: measure implicit top gap
// Get the first text node's bounding rect vs the paragraph's top
const range = document.createRange();
const firstTextNode = findFirstTextNode(el);
if (firstTextNode) {
range.setStart(firstTextNode, 0);
range.setEnd(firstTextNode, 1);
const textRect = range.getBoundingClientRect();
const elRect = el.getBoundingClientRect();
const topGap = textRect.top - elRect.top;
// Any gap > 0.25em at 14px = 3.5px suggests extra top displacement
const fontSize = parseFloat(cs.fontSize) || 14;
if (topGap > fontSize * 0.25) {
findings.push({
severity: 'HIGH',
element: el,
property: 'initial-letter top gap',
value: `${Math.round(topGap)}px`,
detail: `Consent paragraph has a ${Math.round(topGap)}px gap between its top edge and the first visible text character. This implicit space (from initial-letter-align offset) pushes all content downward, potentially clipping bottom lines past an overflow:hidden boundary.`,
});
}
}
// Check 2: drop cap bounding rect vs text line bounding rects (overlap)
const dropCapSize = parseFloat(initialLetter) || 1;
if (dropCapSize >= 3) {
const elRect = el.getBoundingClientRect();
const dropCapWidth = parseFloat(cs.fontSize) * dropCapSize * 0.7; // estimate
const dropCapHeight = parseFloat(cs.lineHeight || cs.fontSize) * dropCapSize;
const dropCapRight = elRect.left + dropCapWidth;
const dropCapBottom = elRect.top + dropCapHeight;
// Collect all text node rects within the element to check overlap
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
let node;
while ((node = walker.nextNode())) {
const r = document.createRange();
r.selectNodeContents(node);
const rects = r.getClientRects();
for (const rect of rects) {
if (rect.left < dropCapRight && rect.top < dropCapBottom && rect.width > 5) {
// Text rect overlaps with the estimated drop cap bounding box
findings.push({
severity: 'HIGH',
element: el,
property: 'initial-letter overlap',
value: `${dropCapSize}-line drop cap`,
detail: `A ${dropCapSize}-line drop cap may overlap consent text at approximately x=${Math.round(rect.left - elRect.left)}, y=${Math.round(rect.top - elRect.top)}. initial-letter-align may have shifted the drop cap's position into the text column.`,
});
break; // one overlap finding per element
}
}
}
}
}
}
// Check 3: initial-letter on inline elements (span, a, em, etc.)
const inlineEls = root.querySelectorAll('span, a, em, strong, b, i');
for (const el of inlineEls) {
const cs = getComputedStyle(el);
const initialLetter = cs.getPropertyValue('initial-letter');
if (initialLetter && initialLetter !== 'normal' && initialLetter !== 'none') {
findings.push({
severity: 'HIGH',
element: el,
property: 'initial-letter on inline element',
value: initialLetter,
detail: `initial-letter applied to an inline <${el.tagName.toLowerCase()}> element. This is not a ::first-letter pseudo-element application. The element may become a positioned box overlapping adjacent consent text words, with overlap geometry controlled by initial-letter-align.`,
});
}
}
}
return findings;
}
function findFirstTextNode(el) {
const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
let node;
while ((node = walker.nextNode())) {
if (node.textContent.trim().length > 0) return node;
}
return null;
}
Related SkillAudit coverage
- CSS initial-letter drop cap size attacks on consent paragraph height
- CSS float positioning attacks that displace consent text blocks
- CSS overflow:hidden clipping attacks on consent containers
- CSS line-height manipulation reducing visible consent lines
SkillAudit detection: SkillAudit checks all initial-letter-align values on consent paragraph elements and flags any non-alphabetic value as HIGH or CRITICAL. It measures the gap between the paragraph's top edge and the first visible text character using Range.getBoundingClientRect(), flagging gaps exceeding 0.25em as displacement indicators. It also detects initial-letter applied to inline elements — an unusual pattern that causes the element to become a positioned box overlapping adjacent consent text.
Audit your MCP server's drop cap usage near consent text before publishing. Run a free SkillAudit scan — results in 60 seconds.