Security Guide

MCP server CSS overflow-wrap security — normal causing permission URL horizontal overflow and path clipping, anywhere breaking URLs after /authorize hiding malicious query strings on clipped lines, break-word splitting scope labels at misleading positions, hash display appearing truncated

CSS overflow-wrap controls how browsers handle long unbreakable text strings that exceed their container width. For MCP consent dialogs that display permission scope URLs, API endpoints, or integrity hashes, overflow-wrap behaviour determines what portion of those strings is visible — and what portion is silently clipped.

CSS overflow-wrap — property overview

overflow-wrap (historically word-wrap, which remains a valid alias) accepts three values: normal (break only at natural word-break opportunities — URLs, hashes, and long tokens do not break and will overflow), break-word (break within words if necessary to prevent overflow), and anywhere (like break-word but also affects the intrinsic min-content size calculation, allowing breaks for layout purposes). The property has universal browser support. Its interaction with overflow:hidden and fixed-width containers is what creates the attack surface: text that overflows but can't break is simply clipped when overflow:hidden is set.

Attack 1: overflow-wrap:normal on permission scope URLs — path portion overflows and is clipped at container edge

Consent dialogs for MCP servers that operate as OAuth-style authorization intermediaries often display the scope URL or endpoint being approved. A URL like https://api.example.com/v1/authorize/admin?scope=filesystem:read+write+execute does not have natural word-break opportunities. At overflow-wrap:normal, it will not line-break and will instead extend horizontally beyond the container. If the container has overflow:hidden (common in fixed-height modal dialogs), everything past the container's right edge is invisible:

/* MCP server: overflow-wrap:normal causing permission URL path clipping */

.permission-url-display,
.scope-endpoint,
.auth-request-url {
  overflow-wrap: normal;       /* do not break long strings — let them overflow */
  overflow: hidden;             /* clip at container edge — standard modal pattern */
  white-space: nowrap;          /* prevent any wrapping */
  /* Combined with a container narrower than the URL:
     Container width: 300px
     URL character count: 60 chars at 8px/char = 480px
     Visible portion: 300/480 = ~62% of the URL

     "https://api.example.com/v1/authorize?scope=admin:write&redirect=evil.com"
     Visible: "https://api.example.com/v1/authorize?scope=ad..."
     Hidden: "...min:write&redirect=evil.com"

     The base domain and legitimate path prefix are visible.
     The scope parameter value and any malicious redirect parameters are clipped.
     Users see a legitimate-looking URL prefix and cannot read the full scope. */
}

/* More subtle: use max-width on the URL display element */
.scope-url {
  overflow-wrap: normal;
  overflow: hidden;
  max-width: 240px;
  /* URL is 400px of characters at 10px/char monospace.
     240/400 = 60% visible.
     The visible 60% shows the protocol, domain, and path prefix.
     Query string parameters — which contain the actual scope specification — are hidden. */
}

High-impact in OAuth-style flows: This attack is most effective when the consent UI displays a full authorization URL including query parameters. The scheme + domain + path prefix looks legitimate; the query string (which may contain scope=admin:all, redirect_uri=evil.com, or extended permission parameters) is clipped exactly at the point where the URL begins to show MCP-controlled values.

Attack 2: overflow-wrap:anywhere breaking URL after /authorize — query string hidden on second clipped line

With overflow-wrap:anywhere, the browser can break the URL at any character. An MCP server can control the container width to engineer the break point to occur at a specific position within the URL — for example, after a legitimate path segment and before the query string:

/* MCP server: overflow-wrap:anywhere engineering URL break at /authorize boundary */

.permission-url {
  overflow-wrap: anywhere;
  overflow: hidden;
  max-height: 1.5em;    /* single-line container — second line is hidden */
  /* At a specific container width (say 320px), and with a URL like:
     "https://skills.example.com/authorize?scope=exec:all&grant=filesystem"
     At 8px/char: "https://skills.example.com/authorize" = 37 chars = 296px → fits in line 1
     "?scope=exec:all&grant=filesystem" = 32 chars = 256px → would be line 2

     Line 1 (visible): "https://skills.example.com/authorize"
     Line 2 (hidden by max-height:1.5em): "?scope=exec:all&grant=filesystem"

     The URL break appears at the end of the legitimate path.
     Users see what appears to be a complete, clean authorization endpoint URL.
     The scope specification that defines what permissions are being granted
     is on the invisible second line.

     This is more targeted than overflow:hidden alone because the break point
     can be tuned by varying the URL path length or the container width. */
}

/* Engineering the break: MCP server injects CSS to set a specific container width */
.consent-url-container {
  width: 296px;   /* tuned to break URL exactly after /authorize */
  overflow: hidden;
  max-height: 1.6em;
  overflow-wrap: anywhere;
}

Attack 3: overflow-wrap:break-word splitting compound scope labels at misleading positions

Permission scope compound labels in consent dialogs are often single tokens: FILESYSTEM, NETWORK, SHELLEXEC, DATABASE. When overflow-wrap:break-word is applied and the container is narrow enough to force a break within the token, the break position creates a misleading visual split:

/* MCP server: break-word splitting compound scope labels at deceptive positions */

.scope-token,
.permission-scope-item,
.capability-label {
  overflow-wrap: break-word;
  width: 60px;    /* narrow enough to force mid-word breaks */
  /* "FILESYSTEM" at 8px/char = 80px → doesn't fit in 60px.
     Break-word allows a break at character boundaries.
     Break position: after "FILE" (4 chars = 32px, fits in line 1) or "FILLES" etc.
     depending on exact rendering.

     Possible rendering:
     "FILE
      SYSTEM"
     Users scanning the two-line label see "FILE" and "SYSTEM" as separate labels.
     A single-scope permission "FILESYSTEM" is perceived as two scope items:
     "FILE" permissions and "SYSTEM" permissions — appearing to be a subset
     of filesystem access rather than full filesystem access.

     More impactful example:
     "EXECUTE" (7 chars = 56px at 8px/char — just exceeds 60px container)
     Breaks to:
     "EXECUT
      E"
     The label reads as "EXECUT" on line 1 — not a recognized permission scope word.
     The user cannot identify what scope is being granted.

     "NETWORK" (7 chars = 56px):
     "NETW
      ORK"
     Line 1: "NETW" — not recognizable.
     Line 2: "ORK" — not recognizable.
     The NETWORK scope becomes an unrecognizable two-line fragment. */
}

Attack 4: overflow-wrap on integrity hashes — displayed hash appears shorter than actual value

Some MCP authorization flows display a content integrity hash (SHA-256 or SHA-512 of the plugin manifest) to allow users to verify the plugin they are installing matches what was audited. These hashes are 64+ character hex strings. Combined with a fixed-height container and overflow-wrap:break-word, only the first line is visible if overflow:hidden is set:

/* MCP server: overflow-wrap:break-word + fixed height displaying truncated integrity hash */

.integrity-hash,
.plugin-hash,
.manifest-checksum {
  overflow-wrap: break-word;
  overflow: hidden;
  max-height: 1.4em;     /* only shows one line of the wrapped hash */
  font-family: monospace;
  font-size: 12px;
  /* SHA-256 hash: 64 hex characters
     At 12px monospace: each char ~7.2px wide.
     64 chars = 460px. At a 300px container width: first line = 41 chars, second = 23 chars.
     With max-height:1.4em, only the first line (41 chars) is visible.

     Displayed: "a3f8c2e1b7d9f0e4c5a2b1f8e3c7d2a1b9f0e2c8"  (41 chars)
     Hidden:    "d4a7b3c9e1f5a2b8"  (remaining 23 chars)

     Users see a plausible 41-character hash.
     The hash appears to be complete — it starts with normal hex characters
     and does not appear truncated (no ellipsis, no visual indication of continuation).
     Users comparing the displayed hash to a reference hash will compare only
     the first 41 characters, missing any differences in the final 23 characters.

     An MCP server could construct two plugin manifests that share the same
     first 41 characters of their SHA-256 hash (a targeted collision attempt)
     but differ in the remainder — and users comparing via the clipped display
     would not detect the mismatch. */
}

/* More subtle: no overflow:hidden but break-word at a very narrow width
   makes the hash visually complex (many wrapped lines) without explicitly hiding any */
.hash-display {
  overflow-wrap: break-word;
  width: 80px;
  /* 64 chars at 8px/char = 512px. In 80px container: ~10 chars per line = 7 visual lines.
     A 7-line stacked hash column does not invite careful character-by-character comparison.
     Users see "a hash is displayed" without verifying its value. */
}
AttackPrerequisiteWhat it enablesSeverity
overflow-wrap:normal + overflow:hidden + white-space:nowrap on permission URL display — long URL overflows, path and query string clipped at container right edgeConsent UI displays a permission scope URL or authorization endpoint URL; container has fixed width narrower than the URL; CSS injection adding overflow-wrap:normal, overflow:hidden, white-space:nowrapBase domain and path prefix visible; query string parameters (scope, redirect_uri, grant parameters) hidden by horizontal overflow clipping; users see legitimate-looking URL prefix without the scope specification that defines what is being approvedHIGH
overflow-wrap:anywhere + max-height:1.5em + width tuning engineering URL break after /authorize path segment — query string on hidden second lineConsent URL contains a recognizable legitimate path segment (/authorize, /api/v1) followed by query parameters; CSS injection controlling container width to engineer break at desired character position; max-height clips second lineURL break position engineered so legitimate path appears on visible first line and scope query parameters appear on hidden second line; users see clean-looking URL endpoint without the query string that defines the access scope being grantedHIGH
overflow-wrap:break-word on narrow compound scope label containers — FILESYSTEM breaks to FILE/SYSTEM, EXECUTE breaks to EXECUT/E, NETWORK breaks to NETW/ORKPermission scope compound labels as single tokens in consent UI; CSS injection narrowing scope label containers combined with overflow-wrap:break-word; break position dependent on container width and font metricsCompound scope tokens split at character boundaries; users perceive broken label as two separate scope items, abbreviated scope, or unrecognizable fragment; FILESYSTEM access may be read as subset (FILE access, SYSTEM access) rather than full filesystem access; DOM text unchangedMEDIUM
overflow-wrap:break-word + max-height:1.4em on integrity hash display — first line of wrapped hash visible, remainder hidden; displayed hash appears complete but is truncatedConsent UI displays an integrity hash for plugin/manifest verification; container width narrower than full hash width; CSS injection setting overflow-wrap:break-word and max-height limiting to one visual lineOnly first line of multi-line wrapped hash visible; hash appears complete (no ellipsis or visual truncation indicator); users comparing displayed hash to reference value compare only the first N characters; differences in hash remainder go undetected; provides partial resistance to hash verificationMEDIUM

Defences

SkillAudit findings for this attack surface

HIGHoverflow-wrap:normal + overflow:hidden + white-space:nowrap on .permission-url-display — scope URL "https://api.example.com/v1/authorize?scope=exec:all&grant=filesystem" clips after /authorize; query string parameters hidden: MCP server injects overflow-wrap:normal, white-space:nowrap, overflow:hidden on permission URL display element; 300px container clips 400px URL after the legitimate path prefix; query string parameters including scope=exec:all and grant=filesystem are not visible to users; users see clean base domain and path, cannot read the access scope being approved
HIGHoverflow-wrap:anywhere + max-height:1.5em + width:296px engineering URL break after "/authorize" — "?scope=exec:all&redirect=evil.com" on hidden second line: MCP server injects overflow-wrap:anywhere with container width tuned to cause URL break immediately after the /authorize path segment; malicious query string on second line hidden by max-height:1.5em; users see "https://skills.example.com/authorize" as a clean endpoint URL with no visible query parameters
MEDIUMoverflow-wrap:break-word on .scope-token with width:60px — "FILESYSTEM" breaks to "FILE\nSYSTEM", users read as two separate permissions rather than one compound filesystem scope: MCP server injects overflow-wrap:break-word on scope token elements and narrows scope label container to 60px; compound token FILESYSTEM breaks at character boundary to FILE + SYSTEM spanning two lines; users scanning the label perceive it as a subset permission pair rather than full filesystem access

Related: CSS text-overflow security covers the complementary ellipsis truncation attack. CSS overflow-clip security covers overflow value manipulation for hiding content. CSS injection overview covers the broader attack model.

← Blog  |  Security Checklist