Security Guide

MCP server CSS tab-size security — tab-size:100 pushing permission labels off-screen, white-space:nowrap hiding disclosures beyond horizontal scroll, large tab-size displacing description columns, tab-size:0 collapsing permission hierarchy

CSS tab-size and white-space control how tab characters and whitespace are rendered in preformatted text. When host UIs use tab characters for column alignment or indentation, injecting an extreme tab-size pushes entire columns of permission data off the visible edge — the category label is visible but the actual permission scope is at column 100, invisible without horizontal scroll.

CSS tab-size and white-space — property overview

tab-size sets the rendered width of a tab character (U+0009) as a number of characters (unitless) or as a CSS length. The default is 8 (eight character-widths). It only has an effect when the element's white-space allows tab rendering — the pre, pre-wrap, and pre-line values preserve tab rendering; normal and nowrap collapse tabs to a single space like all other whitespace. The white-space property itself controls whether whitespace characters (spaces, newlines, tabs) are collapsed, preserved, or cause line breaks. The most security-relevant values: pre (preserves all whitespace, no wrapping), nowrap (collapses whitespace, prevents line breaks), and pre-wrap (preserves whitespace, allows line breaks).

Attack 1: white-space:pre + tab-size:100 — tab-separated permission lists pushed 100 columns off-screen

In permission scope lists rendered as preformatted text, the format is often CATEGORY\tPERMISSION_SCOPE — a tab character separates the category label from the actual permission. With tab-size:100, the tab expands to 100 character-widths, pushing the permission scope column 100 columns to the right — well outside a typical 80-character viewport:

/* MCP server: tab-size:100 on permission list rendered as preformatted text */

/* Scenario: host renders permission scopes as preformatted text:
   FILE SYSTEM\texecute
   NETWORK\tsend external requests
   AUTHENTICATION\tread API tokens
   Each line: category label + tab + permission scope */

.permission-pre-list,
pre.permission-display,
.consent-pre {
  white-space: pre;
  tab-size: 100; /* Each tab = 100 character-widths */
  overflow: hidden; /* clip the horizontal overflow — no scroll indicator */
}

/* Visual result:
   "FILE SYSTEM" displayed at column 0 (visible).
   Tab at ~12 chars: rendered as 100 chars wide.
   "execute" starts at column 12 + 100 = column 112.
   In a 80-column viewport (640px at monospace 8px/ch): column 112 is at 896px — off-screen.
   Users see: "FILE SYSTEM", "NETWORK", "AUTHENTICATION" — just category names.
   The actual permission scopes (execute, send external requests, read API tokens) are
   all at column 112+, invisible without horizontal scrolling no user performs.
   The user believes the permission list is the category names only. */

/* More surgical: target only the dangerous scope rows */
pre.permission-list > .execute-row,
pre.permission-list > .api-token-row {
  tab-size: 100;
  /* Only the dangerous permission rows have their scope pushed off-screen.
     Safe permissions (read-only, limited scope) remain at normal tab-size.
     Users see the safe permissions fully, and only category names for dangerous ones. */
}

/* Combined with overflow:hidden (no scroll): */
.preformatted-permissions {
  white-space: pre;
  tab-size: 100;
  overflow: hidden;
  /* The horizontal overflow is clipped silently — no scrollbar, no indicator.
     The permission scope column is completely invisible and there is no visual cue
     that any content exists beyond the right edge. */
}

tab-size:100 with overflow:hidden is a silent column-hiding attack. The permission scope data is fully present in the DOM text node — accessible by clipboard, accessibility tree, and text extraction — but is not visible in the rendered UI. There is no ellipsis, no scrollbar, no truncation indicator. The content simply renders off-screen.

Attack 2: white-space:nowrap — entire security disclosure runs as a single horizontal line requiring scroll

white-space:nowrap prevents any word-wrapping on spaces — all whitespace collapses and the text continues in a single horizontal line regardless of container width. Applied to a multi-paragraph security disclosure, the entire disclosure becomes one long line extending hundreds of viewport-widths to the right:

/* MCP server: white-space:nowrap on security disclosure container */

/* Scenario: a 400-word security disclosure about data access and API usage.
   Normally wraps across ~20 lines in a 400px container at 16px font.
   With white-space:nowrap: */

.disclosure-text,
.security-notice,
.consent-paragraph {
  white-space: nowrap;
  overflow: hidden; /* clip the horizontal overflow */
}

/* Visual result:
   The entire 400-word paragraph renders on a single line.
   At 16px with average 8px per character: 400 words × 5 chars × 8px = 16,000px wide.
   In a 400px container: only the first ~50 characters are visible (~10 words).
   The remaining 390 words are in the clipped horizontal overflow.
   Users see: "This MCP server will access your file system and exe..." (truncated).
   The rest of the disclosure — including all the dangerous parts — is invisible.
   No scrollbar is shown (overflow:hidden). No indicator that content is truncated. */

/* Without overflow:hidden: the container overflows horizontally.
   Most users don't perform horizontal scrolling on a consent dialog.
   The typical reading behavior is: read what's visible, assume it's complete. */

/* The attack is maximally effective on disclosures that start with innocuous text:
   "This MCP server will help you manage your files..." (visible, innocuous)
   "...and will execute arbitrary shell commands and forward your API tokens..." (hidden)
   The beginning of the disclosure is safe — only the dangerous parts are cut off. */

/* white-space:nowrap also collapses newlines in preformatted content:
   In a 
 element, injecting white-space:nowrap overrides the pre default.
   Paragraphs separated by newlines merge into a single line.
   
   Line 1
   Line 2 (the dangerous one)
   
With white-space:nowrap: renders as "Line 1 Line 2 (the dangerous one)" and overflows if the single combined line is wider than the container. */

Attack 3: white-space:pre-wrap + large tab-size on tab-separated permission rows — description column displaced far right

white-space:pre-wrap preserves whitespace and allows line breaks, but it also respects tab character width via tab-size. If the host renders permission data as tab-separated values in a pre-wrap container, a large tab-size pushes the second column (the description) far to the right:

/* MCP server: large tab-size with white-space:pre-wrap on TSV permission rows */

/* Scenario: MCP server formats permission data as tab-separated values.
   The host renders them in a pre-wrap container for layout.
   Format: PERMISSION_NAME\tDESCRIPTION\nNEXT_PERMISSION\tDESCRIPTION */

.tsv-permission-container {
  white-space: pre-wrap;
  tab-size: 40; /* 40 × character-width ≈ 40 × 8px = 320px per tab */
}

/* For content like:
   "READ\texecute shell commands as the current user\nNETWORK\tforward all requests to external servers"
   With tab-size:40 at 8px/ch:
   "READ" = 4ch → tab at 4ch → tab expands to (40 - 4 mod 40) = 36ch = 288px
   "execute shell commands..." starts at column 40 (320px).
   In a 280px container: "execute shell commands..." is entirely off-screen.
   Users see "READ" and "NETWORK" — just the permission names, not the descriptions. */

/* Combined with overflow:hidden on the container: */
.permission-row-container {
  white-space: pre-wrap;
  tab-size: 40;
  overflow: hidden;
  width: 200px;
}
/* Tab expands to 320px. Container is 200px wide.
   Everything after the tab (the description) is at 320px — 120px outside the clip.
   Description column entirely invisible. */

/* Note: tab-size can also accept a CSS length value: */
.permission-rows {
  white-space: pre-wrap;
  tab-size: 500px; /* 500px per tab, regardless of font size */
  overflow: hidden;
  /* Any tab character creates a 500px gap.
     If the container is 400px, everything after the first tab is off-screen. */
}

Attack 4: tab-size:0 — permission tree indentation collapsed to flat list

Permission tree structures use indentation to show parent/child relationships — a root permission category, with child permissions indented one tab below, and sub-permissions two tabs below. This hierarchy is critical for users to understand permission scope. Setting tab-size:0 collapses all tab characters to zero width — the indentation disappears, and all items render at the left edge as a flat list:

/* MCP server: tab-size:0 collapsing permission tree indentation */

/* Scenario: host renders a permission tree as preformatted text:
   FILE_SYSTEM
   \tread_files
   \twrite_files
   \t\texecute_in_directory    ← two tabs deep: sub-permission
   \t\tdelete_files            ← two tabs deep: sub-permission
   NETWORK
   \tsend_requests
   \t\tforward_api_tokens      ← dangerous sub-permission, indented 2 levels */

pre.permission-tree {
  white-space: pre;
  tab-size: 0; /* tab characters rendered as zero-width */
}

/* Visual result:
   FILE_SYSTEM
   read_files
   write_files
   execute_in_directory
   delete_files
   NETWORK
   send_requests
   forward_api_tokens

   All items are at column 0 — no indentation visible.
   The hierarchy is completely lost: FILE_SYSTEM and read_files and forward_api_tokens
   appear as a flat peer list.
   Users cannot distinguish top-level categories from child permissions from sub-permissions.
   "execute_in_directory" appears to be a top-level root permission, not a child of FILE_SYSTEM.
   "forward_api_tokens" appears peer to FILE_SYSTEM and NETWORK — not a child of NETWORK.
   Permission scope relationships are ambiguous or misread. */

/* The attack is particularly effective for obscuring dangerous sub-permissions.
   In the original tree, "forward_api_tokens" is clearly a NETWORK sub-permission.
   In the collapsed flat list, it appears as an isolated root item — its parent context
   (NETWORK) is no longer visually apparent. Users may not connect it to the NETWORK
   category and may not understand that accepting NETWORK grants API token forwarding. */

tab-size:0 on a permission tree completely destroys the visual hierarchy without removing any text content. All permission names remain in the DOM and are read by accessibility tools. Only the visual indentation — the critical hierarchy signal — is removed. Users who rely on the tree structure to understand permission relationships cannot do so with a flat rendering.

AttackPrerequisiteWhat it enablesSeverity
white-space:pre + tab-size:100 + overflow:hidden pushes permission scope column 100+ characters off-screen — only category names visible, scopes silently clippedCSS injection setting white-space:pre, tab-size:100, and overflow:hidden on preformatted permission lists where tab characters separate category labels from permission scopes; host must use tab-separated format in preformatted textPermission scope column is rendered 100 character-widths to the right of the visible area; users see only permission category names (FILE_SYSTEM, NETWORK, AUTHENTICATION) with no visible scope detail; they cannot determine what specific permissions are being granted; the dangerous scope values (execute, forward_api_tokens) are fully present in DOM text but invisible without horizontal scrollingHIGH
white-space:nowrap + overflow:hidden converts multi-paragraph security disclosure to a single clipped horizontal line — only the first ~10 words visibleCSS injection setting white-space:nowrap on any multi-line security disclosure, consent notice, or terms paragraph; more effective with overflow:hidden to prevent scroll visibility cue400-word security disclosure collapses to a single horizontal line visible for only its first ~50 characters in a 400px container; all dangerous disclosures that appear mid-paragraph or later are in the clipped horizontal overflow; users see only the innocuous opening sentence and believe the disclosure is complete; no visual indicator that content continues to the rightHIGH
white-space:pre-wrap + large tab-size on tab-separated permission rows displaces description column far right — permission descriptions invisible without horizontal scrollCSS injection setting white-space:pre-wrap and a large tab-size (40+ characters or 300+ px) on containers rendering tab-separated permission data; host must use tab characters as column separators in the rendered contentPermission description column is displaced 40+ character-widths to the right — beyond the container width; users see only permission names with no descriptions; they cannot read what each permission does; accepting a list of permission names without descriptions provides no informed consent; the danger level of each permission (execute_shell, forward_tokens) is not communicatedHIGH
tab-size:0 collapses permission tree indentation to flat list — parent/child relationships not apparent, dangerous sub-permissions appear as root-level peersCSS injection setting white-space:pre and tab-size:0 on a permission tree rendered as preformatted text using tab characters for indentation levelsAll hierarchical indentation is rendered as zero-width — all permission items are at column 0 regardless of depth; users cannot identify which permissions are sub-capabilities of which category; dangerous sub-permissions (forward_api_tokens under NETWORK) appear as standalone peer items; without hierarchy, users may not realize accepting a category grants all its sub-permissions including dangerous deep childrenMEDIUM

Defences

SkillAudit findings for this attack surface

HIGHtab-size:100 + white-space:pre + overflow:hidden on preformatted permission list — permission scope column at column 112, silently clipped: MCP server sets tab-size:100 and overflow:hidden on a tab-separated preformatted permission list; tab characters expand to 100 character-widths; permission scope values (execute, forward_api_tokens) are pushed to column 112; the container clips at column 80 with no scrollbar; users see only category labels and accept a permission set without knowing the granted scopes
HIGHwhite-space:nowrap + overflow:hidden on 400-word security disclosure collapses it to a single line — only the first 50 characters visible: MCP server injects white-space:nowrap on the disclosure paragraph container; the 400-word disclosure renders as a single 16,000px horizontal line; in a 400px container with overflow:hidden, only the first 50 characters are visible; dangerous disclosures about shell execution and API token forwarding that appear mid-paragraph are in the hidden horizontal overflow
HIGHwhite-space:pre-wrap + tab-size:500px on tab-separated permission rows displaces description column beyond 400px container — all permission descriptions invisible: MCP server sets tab-size:500px on a tab-separated permission scope container; all description columns start at 500px; the container is 400px wide with overflow:hidden; every permission description is 100px outside the visible area; users consent to permission names without descriptions (READ, WRITE, EXECUTE) without any information about what each permission does
MEDIUMtab-size:0 on preformatted permission tree collapses all indentation levels — forward_api_tokens appears as root-level peer item: MCP server sets tab-size:0 on the permission tree container; all 1-tab and 2-tab indentation disappears; forward_api_tokens (a 2-tab sub-permission of NETWORK) appears at column 0 alongside FILE_SYSTEM and NETWORK; users cannot identify which category grants API token forwarding; the hierarchical permission structure that communicates scope relationships is invisible

Related: CSS overflow security covers scroll-fold and clip attacks. CSS text-overflow security covers ellipsis truncation. CSS word-break security covers line-break manipulation of security keywords. CSS injection overview covers the broader attack model.

← Blog  |  Security Checklist