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. */
}
AttackPrerequisiteWhat it enablesSeverity
hanging-punctuation:first on quoted consent statement starting with " shifts reading start to the right — first word slightly displaced from expected positionSafari 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 elementOpening 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 themLOW
hanging-punctuation:last on security disclosure sentences makes periods and commas hang past right content edge — implies sentence may continueSafari browser; disclosure sentences must end with stop or comma; CSS injection setting hanging-punctuation:last or force-endTerminal 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 disclosuresMEDIUM
hanging-punctuation:force-end on multi-line disclosure creates ragged right edge throughout — implies incomplete or fragmentary disclosureSafari browser; multi-line security disclosure text with commas/stops at multiple line-ends; CSS injection setting force-endAll 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 fragmentaryMEDIUM
hanging-punctuation:first + negative text-indent causes opening punctuation to overlap with adjacent security status badge in two-column layoutSafari browser; two-column layout with security badge adjacent to disclosure text; negative text-indent already displacing first line; CSS injection adding hanging-punctuation:firstThe 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 affectedMEDIUM

Defences

SkillAudit findings for this attack surface

MEDIUMhanging-punctuation:last on "This grants permanent DELETE access to your database." — period hangs into right margin, sentence appears incomplete: MCP server injects hanging-punctuation:last on the security disclosure paragraph; the terminal period after "database" hangs past the right content edge; in Safari, fast-reading users scanning for sentence-end cues at the right edge do not immediately see the period; disclosure may not be perceived as a complete, conclusive statement
MEDIUMhanging-punctuation:force-end on multi-line permission scope list creates ragged right edge — disclosure appears fragmentary rather than complete: MCP server injects force-end on the permission scope description list; all line-end commas hang; the paragraph's right edge is uniformly ragged; in Safari, users perceive the disclosure as having rendering issues or incomplete content rather than as a definitive statement requiring their informed consent
MEDIUMhanging-punctuation:first + text-indent:-1em on quoted consent statement — opening quote displaces 22px into badge column, overlapping with adjacent [REVOKED] badge in Safari: MCP server combines negative text-indent with hanging-punctuation:first on a consent statement starting with "; combined displacement places the " 22px into the left column where the [REVOKED] security badge is positioned; the badge's final 'D' is visually disturbed by the overlapping " character
LOWhanging-punctuation:first on consent statement starting with opening quote — reading start shifted, first word not at expected left-edge position in Safari: MCP server injects hanging-punctuation:first on the consent text; opening " hangs 6px into left margin; first word "By" starts at content box left edge rather than at the visual starting point where the " is; slight reading start displacement — minimal impact alone, but amplified in combination with other layout manipulations

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.

← Blog  |  Security Checklist