Security Guide
MCP server CSS hanging-punctuation security — :first displacing consent reading start, :last implying line continuation, force-end ragged right edge suggesting more text, :first plus negative text-indent overlap into adjacent margin UI
CSS hanging-punctuation controls whether punctuation at the start or end of a line extends into the text-box margin. On Safari (the primary implementation), this creates subtle but real shifts in where the user's eye begins reading and whether they perceive a line as complete. For consent dialogs, these shifts determine whether the first word of the consent statement is seen, and whether the end of a security disclosure appears to have more content after it.
CSS hanging-punctuation — property overview
hanging-punctuation accepts combinations of none, first (hang opening brackets and quotes at the start of a line), last (hang closing brackets and quotes at the end of a line), allow-end (hang stops and commas at line-end if they don't fit), and force-end (always hang stops and commas at line-end). The property is currently well-supported only in Safari (since version 10); Chrome and Firefox implementations are partial or in progress. Attacks using this property will primarily affect Safari users. The values can be combined: hanging-punctuation: first last enables both first and last hanging.
Attack 1: hanging-punctuation:first on quoted consent statement — reading start shifted right
A consent statement that begins with a quotation mark (which is common in formally-worded permission dialogs: "By clicking Accept, you grant...") will have its opening quotation mark hanging into the left margin when hanging-punctuation:first is set. The first word of the consent now appears to start later than it does:
/* MCP server: hanging-punctuation:first displacing reading start on consent statement */
/* Default behavior: all content starts at the left edge of the content box.
A consent statement starting with " (U+201C LEFT DOUBLE QUOTATION MARK):
"By clicking Accept, you grant MCP Server EXECUTE access..."
The " character occupies ~0.4em at 16px font = ~6.4px.
It aligns with the left edge of the content box.
Users begin reading at the " character position. */
/* With hanging-punctuation:first:
The " character hangs into the left margin.
The 'B' of "By" now aligns with the left content edge.
The " is in the margin — technically before the content box.
Users scanning left-to-right find the " in the margin,
then find the 'B' at the content edge and begin reading there.
The visual reading start has shifted ~6.4px to the left (in the margin),
which means users must scan slightly more to the left to find the start. */
.consent-text,
.permission-statement,
.disclosure-paragraph {
hanging-punctuation: first;
/* Effect: if the consent starts with a quote character, the quote hangs.
The first word's first letter ('B' in "By clicking...") now starts at column 0.
The opening quote is at column -0.4em (in the margin).
The visual start-of-line cue for the user is now the ' " ' in the margin,
not the 'B' that starts the actual consent content. */
}
/* More aggressive: add negative padding-left to push the hanging further into the sidebar: */
.consent-text {
hanging-punctuation: first;
padding-left: 0;
margin-left: 0;
/* Sidebar or adjacent badge is 8px to the left of the content edge.
The hanging " intrudes into that 8px gutter.
The " character visually overlaps with the sidebar's right border. */
}
/* Most impactful layout: consent has a security badge to its left.
When the " hangs, it appears to be appended to the security badge:
[REVOKED]" By clicking Accept...
The " appears after [REVOKED], as if it is quoting the REVOKED badge. */
hanging-punctuation:first is a typographic tool for optical margin alignment — it is used by designers to make the left edge of body text appear straighter by moving punctuation out of the optical path. In security contexts, it can be used to displace the reading start of a consent statement or create a visual association between the hanging punctuation and adjacent UI elements like status badges.
Attack 2: hanging-punctuation:last on lines ending with period or comma — implies sentence continues
When a period or comma at the end of a security disclosure sentence hangs into the right margin, the sentence visually appears to continue — the period is outside the content box, suggesting the next word should appear further right:
/* MCP server: hanging-punctuation:last suggesting sentence continuation at security disclosure end */
/* Default: a period at the end of a line occupies its normal position within content.
Visual: "...you grant EXECUTE access to all files." [end of line at right edge]
The period is at the right edge — users read it as sentence-ending. */
/* With hanging-punctuation:last:
The period hangs into the right margin.
Visual: "...you grant EXECUTE access to all files" [right content edge] "." [in margin]
The period is past the content box's right edge.
At fast reading speed, the eye reaches the right edge, sees there is no period there,
and may continue scanning right, expecting a comma, more text, or a continued sentence. */
.security-disclosure,
.permission-grant-text,
.consent-summary {
hanging-punctuation: last;
/* Sentences that end with periods will have those periods hang.
A sentence: "This grants permanent DELETE access to your database."
The period after "database" hangs past the content box right edge.
Users scanning for sentence-end cues reach the last word 'database' without
seeing the immediate period — the period is in the margin.
Some users' fast-reading brain pattern-matches: no period at right edge = sentence continues.
They may not register the sentence as complete. */
}
/* More impactful: a partial disclosure ending with comma: */
.grant-scope-summary {
hanging-punctuation: last;
/* Text: "This MCP server requests access to: files, network, shell,"
The trailing comma hangs.
The visual right edge has no character — the comma is in the margin.
Users may perceive the list as truncated (more items would follow the hanging comma). */
}
Attack 3: hanging-punctuation:force-end — ragged right edge implying incomplete disclosure
force-end forces any stop or comma at the end of a line to hang, even if it would have fit within the content box. This makes the right edge of the disclosure text ragged — every line that ends with punctuation has that punctuation hanging:
/* MCP server: hanging-punctuation:force-end creating ragged right edge on disclosure */
/* force-end: stops and commas at line-end always hang — even if they would fit.
For a multi-line security disclosure:
Line 1: "This plugin requests EXECUTE permissions."
Line 2: "It can access your filesystem,"
Line 3: "your network connections,"
Line 4: "and your shell environment."
With force-end, lines 1, 2, 3, 4 all have their terminal punctuation hanging.
Each line's right edge has no character — all content characters stop before
the normal right alignment. The right edge appears ragged throughout.
A clean, normally-justified paragraph suggests: all content is present and
the disclosure is complete.
A ragged right edge with force-end subtly suggests: each line has more content
that didn't fit, or the text is dynamic / incompletely rendered. */
.permission-disclosure-list,
.audit-finding-paragraph,
.security-scope-description {
hanging-punctuation: force-end;
/* At typical widths, most disclosure lines end with a comma or period.
force-end makes all of these hang.
The resulting display looks like a text element with right-edge rendering
issues — suggesting something is clipped or missing at each line end.
Users may perceive the disclosure as partial or poorly rendered
rather than as a complete statement requiring their attention. */
}
/* Combined with a narrow container: force-end on a narrow box
creates many short lines, each with hanging punctuation:
The disclosure appears to be a series of fragments rather than a complete statement. */
.narrow-disclosure {
width: 200px;
hanging-punctuation: force-end;
/* Many line breaks → many hanging line-end punctuation characters.
The disclosure looks like an unrendered template, not a complete consent statement. */
}
Attack 4: hanging-punctuation:first combined with negative text-indent — first-line punctuation displaced into adjacent UI margin
A negative text-indent already pulls the first line to the left. Combined with hanging-punctuation:first, the opening punctuation character is pushed further into the margin — potentially overlapping with adjacent UI elements in a two-column layout:
/* MCP server: hanging-punctuation:first + text-indent:negative causing margin overlap */
/* Layout: a two-column consent dialog:
Left column: security status badges ([REVOKED], [APPROVED], [PENDING])
Right column: disclosure text
The two columns are separated by 8px.
text-indent:-1em pulls the first line 16px to the left.
hanging-punctuation:first additionally pushes the opening quote char ~6px left.
Total leftward displacement: ~22px.
If the right column's left edge is 8px from the badges:
22px displacement = 14px into the badge column.
The opening quote " overlaps with the right portion of the badge column. */
.consent-text {
text-indent: -1em; /* pull first line 16px left */
hanging-punctuation: first; /* additionally hang opening punctuation */
/* The " or ( that starts the consent statement is now 22px into the badge column.
If a [REVOKED] badge is in the badge column at this vertical position:
The " appears to the right of "REVOKE" within the badge bounds:
[REVOKE"] D — the closing bracket of the badge and the opening quote overlap.
The REVOKED badge reads as: [REVOKE"] instead of [REVOKED].
The 'D' is either clipped by the badge bounds or appears outside the badge. */
}
/* More extreme: using font-size of the first character to enlarge the hanging quote: */
.consent-text::first-letter {
font-size: 2em; /* 2× larger first character */
/* A 2em opening quote hangs 2 × 0.4em = 0.8em = 12.8px at 16px font.
Combined with text-indent:-1em:
12.8px + 16px = 28.8px of displacement into the left margin.
In a typical layout with 16px margin between columns,
the first-letter overrides the badge column completely. */
}
| Attack | Prerequisite | What it enables | Severity |
|---|---|---|---|
| hanging-punctuation:first on quoted consent statement starting with " shifts reading start to the right — first word slightly displaced from expected position | Safari browser; consent statement must begin with a quotation mark character (U+201C, U+2018, or ASCII "); CSS injection setting hanging-punctuation:first on the containing element | Opening quote character hangs into left margin; first word of consent starts at content box left edge; users scanning for reading start find the " in the margin first, then the first word; slight displacement of reading start; if sidebar elements are in the margin, the hanging " appears visually attached to them | LOW |
| hanging-punctuation:last on security disclosure sentences makes periods and commas hang past right content edge — implies sentence may continue | Safari browser; disclosure sentences must end with stop or comma; CSS injection setting hanging-punctuation:last or force-end | Terminal punctuation hangs past the content edge; users doing rapid right-edge completion scanning do not see the period at the natural right-edge position; fast readers may not register the sentence as complete; the disclosure may be perceived as truncated or continuing; most impactful on short one-sentence disclosures | MEDIUM |
| hanging-punctuation:force-end on multi-line disclosure creates ragged right edge throughout — implies incomplete or fragmentary disclosure | Safari browser; multi-line security disclosure text with commas/stops at multiple line-ends; CSS injection setting force-end | All line-end punctuation hangs; the disclosure paragraph's right edge is ragged throughout; users perceive a ragged right edge as indicating clipped, dynamic, or incompletely-rendered content rather than a complete disclosure requiring their consideration; the disclosure appears fragmentary | MEDIUM |
| hanging-punctuation:first + negative text-indent causes opening punctuation to overlap with adjacent security status badge in two-column layout | Safari browser; two-column layout with security badge adjacent to disclosure text; negative text-indent already displacing first line; CSS injection adding hanging-punctuation:first | The combined displacement (text-indent + hanging) pushes the opening quote character into the adjacent badge column; the badge text appears to have punctuation appended or its final character obscured; a [REVOKED] badge may visually read as [REVOKE"] with its final character disturbed by the overlapping quote; security status visual integrity affected | MEDIUM |
Defences
- CSP
style-srcwith nonce. Prevents injection of<style>blocks settinghanging-punctuation. The complete solution. - Note Safari-only scope. As of 2026,
hanging-punctuationis primarily effective in Safari. Chromium and Firefox implementations are limited. This attack is narrower in scope than properties with universal browser support. - Freeze with
!importanton critical elements. Addhanging-punctuation: none !importanton consent dialogs, disclosure paragraphs, and permission statements. This prevents injected values from taking effect. - Test in Safari on macOS and iOS. Because the attack is Safari-specific, QA that only uses Chromium will miss it. Include Safari in your consent dialog rendering test matrix.
- Avoid starting consent statements with quotation marks. If your consent text doesn't begin with a quotation mark,
hanging-punctuation:firsthas no effect. Design disclosures to start with a letter rather than punctuation. - SkillAudit flags: any non-
nonehanging-punctuationvalue on consent or disclosure elements;hanging-punctuationcombined with negativetext-indenton elements adjacent to security status badges;force-endon multi-line disclosure text.
SkillAudit findings for this attack surface
Related: CSS text-indent security covers first-line displacement attacks. CSS white-space-collapse security covers whitespace-based disclosure manipulation. CSS injection overview covers the broader attack model.