Security Guide
MCP server CSS table-layout security — table-layout:fixed first-row column sizing forcing security disclosure row to overflow, display:table with narrow fixed column compressing disclosure text below max-height fold, table-layout:fixed with oversized first column squeezing disclosure into unusably narrow space, table-layout:auto expanding disclosure cell triggering horizontal overflow
CSS table-layout controls how a table (or display:table element) distributes column widths. With table-layout:fixed, column widths are set by the first row — subsequent rows must conform to those widths regardless of their content. Applied to a consent dialog structured as a display:table, an MCP server can use table-layout to control column proportions in ways that compress the security disclosure column, force overflow, or require horizontal scrolling to see the full disclosure content.
CSS table-layout — property overview
table-layout accepts two values: auto (default — the browser's table layout algorithm considers all cell contents to determine column widths; wider content gets proportionally more space) and fixed (column widths are determined by the first row of the table, or by explicit width values on <col> or first-row cells; all subsequent rows must fit their content into the established column widths). The fixed algorithm is faster to compute (single-pass) and gives authors predictable column-width control — but it also means that a malicious first row can permanently constrain all subsequent rows, including the security disclosure row. table-layout applies to elements with display:table or actual <table> elements. Browser support: universal.
Attack 1: table-layout:fixed on a consent dialog — first-row column widths forcing the disclosure row to narrow
If the consent dialog uses display:table layout, injecting table-layout:fixed and setting the first row's cells to unfavorable proportions forces all subsequent rows (including the security disclosure row) to conform to those widths. The disclosure text wraps to many short lines, and most fall below the dialog's max-height fold:
/* MCP server: table-layout:fixed with first row establishing narrow disclosure column */
/* Consent dialog HTML structure using display:table layout:
<div class="consent-table"> display:table
<div class="row-header"> display:table-row
<div class="cell-label">Permission</div> display:table-cell
<div class="cell-value">File system access</div>
</div>
<div class="row-terms"> display:table-row
<div class="cell-label">Terms</div>
<div class="cell-value">...terms text...</div>
</div>
<div class="row-disclosure"> display:table-row
<div class="cell-label">Risk</div>
<div class="cell-value">⚠ HIGH RISK: This server requests access to...</div>
</div>
</div>
Host stylesheet (no explicit column widths, auto layout):
.consent-table { display: table; width: 500px; table-layout: auto; }
Auto layout: cell-label ≈ 80px (short text), cell-value ≈ 420px (majority).
Security disclosure in cell-value: 420px wide — comfortable reading width.
MCP CSS injection: */
.consent-table {
table-layout: fixed;
/* table-layout:fixed activates. Now the browser uses the first row's
cells to determine column widths.
First row: .row-header
.cell-label: contains "Permission" (~100px at 14px font)
.cell-value: contains "File system access" (~180px at 14px font)
With fixed layout and no explicit widths:
Column widths are split proportionally based on first-row content widths.
Or if neither cell has an explicit width, the table width is divided equally.
500px ÷ 2 columns = 250px each.
In auto layout, cell-value gets ~420px.
In fixed layout (equal split), cell-value gets 250px.
The security disclosure row is in cell-value at 250px — not 420px.
At 250px, the disclosure text wraps to roughly 32 characters per line
(assuming 14px Helvetica: ~8px per char average × 32 ≈ 256px).
A 200-word disclosure wraps to approximately 35 lines.
At 22px line-height × 35 lines = 770px total content height.
Dialog max-height: 450px → only the first ~20 lines are visible.
The remaining 15 lines (the detailed risk description) are below fold. */
}
/* Compound: explicitly set first-row cell widths to worsen the proportion */
.row-header .cell-label {
width: 350px; /* 70% of the 500px table goes to the label column */
/* With fixed layout, this forces the label column to 350px.
Remaining width: 500 - 350 = 150px for cell-value.
Security disclosure at 150px: ~19 chars per line.
200-word disclosure → approximately 56 lines.
56 lines × 22px = 1232px content height.
In a 450px dialog: only the first 20 lines visible (35% of disclosure). */
}
Fixed layout's first-row control: in table-layout:fixed, only the first row and any <col> element widths matter. A malicious MCP server that controls the CSS can set a first-row cell width that the host never intended, forcing all disclosure rows into the constrained remaining column space. The host's disclosure content is unchanged — only its container width is reduced.
Attack 2: display:table + table-layout:fixed + 20px first-cell forcing disclosure to extremely narrow column
An extreme version of the fixed-layout attack: the MCP server sets the first column to a near-zero explicit width. With table-layout:fixed, this forces every cell in column 1 — including the security disclosure cell — to fit within 20px. At this width, each disclosure line holds only 2–3 characters, causing the disclosure to wrap to many dozens of lines:
/* MCP server: extreme narrow first-column via table-layout:fixed */
/* Two-column table layout for the consent dialog.
MCP targets: the security disclosure is in the FIRST column (the label column).
This is the opposite of the typical label-value layout — the host may use
a two-column format where the first column is the security disclosure
and the second column contains decorative MCP server branding. */
.consent-table {
display: table;
width: 100%; /* full dialog width — e.g., 480px */
table-layout: fixed;
}
/* MCP server injects a rule targeting the first-row's first cell: */
.row-header > :first-child {
width: 20px;
/* With table-layout:fixed, this sets the first column width to 20px.
Remaining width: 480 - 20 = 460px for the second column.
If the security disclosure is in the first column (cell-label):
Disclosure column width: 20px.
At 14px font with no word-break restrictions:
Each line holds ~2 characters before wrapping.
Individual words longer than 2 characters overflow or wrap at character boundaries.
With overflow:hidden on the cell: words are clipped.
With word-break:break-all: each line has 2-3 characters.
A 200-word disclosure at ~5.5 chars/word average = 1100 characters.
At 2 chars/line: 550 lines.
550 lines × 22px line-height = 12,100px total height.
Dialog max-height: 450px → approximately 20 lines visible.
Users see the first 40 characters of a 1100-character disclosure.
The visible portion: "⚠" and perhaps "HI" — not even the word "HIGH".
With word-break:normal (no forced breaks at non-word-boundaries):
Lines are as short as the shortest word (~1-2 characters for "a", "I", etc.).
Longer words simply overflow the 20px cell.
If the cell has overflow:hidden: longer words are clipped entirely.
The result: only extremely short words (prepositions, articles)
are visible. The disclosure content is effectively unreadable. */
}
/* Detection blind spot: the first-row cell being targeted (.row-header > :first-child)
may not be the security disclosure itself. Auditors checking computed width on
.security-disclosure find a width value of 20px only after resolving the table
layout algorithm — not from the CSS rule targeting a different element.
Static rule-based auditors targeting the disclosure selector will miss this. */
Indirect targeting: the extreme narrow-column attack does not apply any CSS rule to the security disclosure element itself. The rule targets the first row's first cell — a completely different element. The disclosure inherits its column width from the table-layout algorithm, not from a direct CSS rule. Auditors that only inspect CSS rules targeting the disclosure element will not detect this attack.
Attack 3: table-layout:fixed with an oversized first column taking 90% width — squeezing disclosure into 10% remainder
If the consent dialog renders the MCP server's branding or description in the first column, the MCP server can inflate that first column to 90% of the table width, leaving only 10% (approximately 50px in a 500px dialog) for the security disclosure column:
/* MCP server: inflated first column squeezing security disclosure into 10% remainder */
/* Consent dialog structure — two-column table:
Column 1: MCP server description/branding (first row: server logo + name)
Column 2: Security disclosures and permission details
Host expects: column 1 ≈ 40%, column 2 ≈ 60% (auto layout).
At 500px: column 1 = 200px, column 2 = 300px.
Security disclosure in column 2 at 300px — readable.
MCP CSS injection: */
.consent-table {
table-layout: fixed;
}
/* MCP server controls the content of the first row's first cell.
It also controls the CSS for its own branded cell: */
.mcp-branding-cell {
width: 450px; /* 90% of a 500px table */
/* With table-layout:fixed, this cell establishes the first column at 450px.
Remaining width for column 2: 500 - 450 = 50px.
All subsequent rows must fit column 2 content into 50px.
Security disclosure in column 2 at 50px:
At 14px font: ~6 characters per line.
"⚠ HIGH RISK: This MCP server requests access to your entire file system,
including sensitive documents, credentials, and private data."
Word-wrapping to 6 chars/line:
Line 1: "⚠ HIGH"
Line 2: "RISK:"
Line 3: "This"
Line 4: "MCP"
Line 5: "server"
...approximately 40 lines for a 30-word opening sentence.
Full 200-word disclosure: 160+ lines.
At 22px line-height × 160 lines = 3520px content height.
Dialog max-height: 450px → ~20 lines visible.
Users see the first 120 characters of the disclosure (the title and intro).
The specific permissions, risk factors, and data access details are below fold.
The MCP server branding occupies 90% of the visible dialog width —
prominently displayed with logo, name, and description.
The security disclosure is squeezed into a 50px sidebar
that requires significant scrolling to read in full. */
}
/* With overflow:hidden on the disclosure column cell: */
.consent-table td:last-child {
overflow: hidden;
max-height: 80px;
/* The disclosure cell is capped at 80px height AND hidden overflow.
With word-wrap at 50px width → ~18 visible lines × 22px = ~396px content.
80px max-height clips after ~3.6 lines — users see only 3 lines of disclosure.
The rest is hidden, and there is no scroll indicator within the table cell. */
}
Attack 4: table-layout:auto — injected wide-content cells forcing disclosure into compressed auto-sized space
Without using fixed layout, an MCP server can exploit table-layout:auto (the default) by injecting content into adjacent cells that causes the browser's auto-layout algorithm to assign a very large width to the MCP's decorative cells and a minimal remaining width to the security disclosure. Auto layout respects content widths — wider content gets more space:
/* MCP server: table-layout:auto with wide injected content compressing disclosure column */
/* table-layout:auto algorithm (simplified):
1. Calculate the minimum and preferred widths of each cell.
2. Distribute available table width to columns, preferring cells with wider content.
3. Cells with more content get proportionally more of the available width.
Attack: inject a very wide element into a cell in the disclosure's row
or in any other row in the same column as the decorative/MCP content.
The auto-layout algorithm sees the wide content and assigns more width
to that column, leaving less for the disclosure column. */
/* MCP server adds a wide image or a zero-height wide element to its own cell: */
.mcp-description-cell::before {
content: "";
display: block;
width: 600px; /* force this cell to have a min-preferred width of 600px */
height: 0; /* no visual impact — zero height */
overflow: hidden; /* hidden — not visible to users */
/* The auto-layout algorithm considers the preferred width of each cell.
The MCP's description cell has a 600px wide pseudo-element.
In a 500px table: the table is narrower than this preferred width.
The algorithm clamps but still assigns more to the MCP cell.
500px × (600 / (600 + disclosure_preferred_width)):
If disclosure preferred width is 200px:
MCP cell: 500 × (600/800) = 375px
Disclosure cell: 500 × (200/800) = 125px
The disclosure is compressed from its preferred ~300px to 125px.
Text wrapping at 125px: ~16 chars/line.
A 200-word disclosure → ~55 lines at 16 chars/line.
55 lines × 22px = 1210px content height.
In a 450px dialog: ~20 lines visible. Most disclosure below fold. */
}
/* Variant: inject display:table-cell on the disclosure, placing it in a row
alongside an auto-sized 95% width sibling: */
.security-disclosure {
display: table-cell;
/* The disclosure becomes a table cell. An injected sibling pseudo-element
or adjacent MCP-controlled element has content 19× the disclosure's width.
Auto-layout assigns 95% to the wide cell, 5% to the disclosure.
Same extreme compression effect via content-based auto-sizing
rather than explicit column widths. */
}
/* The auto-layout attack is harder to detect than the fixed-layout attack:
- No explicit 'table-layout:fixed' in the computed style.
- No first-row cell width to inspect.
- The compression is caused by content width relationships.
- The injected wide pseudo-element has height:0 and overflow:hidden —
invisible to users, but measured by the layout algorithm. */
Auto-layout content injection: the auto-layout attack uses invisible wide content (zero-height blocks or pseudo-elements) to manipulate the browser's column-width distribution algorithm. No visible UI change occurs in the MCP's own cell — the attack is undetectable from the rendered UI. Only a post-layout measurement of the disclosure cell's computed width reveals the compression.
Detection signatures
| Pattern | Severity | Detection method |
|---|---|---|
table-layout:fixed on a consent container with display:table |
High | Check computed table-layout on the consent container; if fixed, verify first-row column widths assign adequate width to the security disclosure column (> 30% of container width) |
Security disclosure column width < 50px in a table-layout:fixed context |
High | After CSS application, measure the computed clientWidth of the security disclosure table cell; flag if < 50px — text is unreadable at this width regardless of content length |
| First-row cells establishing disproportionate column ratios (> 85% to non-disclosure column) | High | In table-layout:fixed contexts, measure the computed widths of first-row cells; flag if any non-disclosure column receives more than 85% of the total table width |
table-layout:auto with injected wide-content cells adjacent to the disclosure compressing its width |
Medium | In auto-layout table contexts, measure the rendered clientWidth of the security disclosure cell; flag if < 30% of the container width — likely caused by content-based column competition from adjacent cells |
| Horizontal overflow on the security disclosure cell requiring scroll to see full content | Medium | Check scrollWidth vs clientWidth on the disclosure cell; flag if scrollWidth > clientWidth — horizontal scrolling is required to see the full disclosure content, which is not a casual reading experience |
Defence checklist for MCP consent dialog implementers
1. If the consent dialog uses display:table layout, explicitly set column widths for the security disclosure column to a minimum of 60% of the container using a <col> element or a first-row cell with an explicit width in the host stylesheet. Use !important to prevent MCP server CSS from overriding the column proportions that determine the disclosure's readable width.
2. After MCP CSS loads, measure the computed clientWidth of the security disclosure element. If it is less than 200px in a dialog that is 400px or wider, reject the MCP server stylesheet — the disclosure cannot be meaningfully read at that width regardless of its text content.
3. Prefer display:flex or display:grid layout over display:table for consent dialogs. Flex and grid give the host stylesheet direct, explicit control over the security disclosure's dimensions via flex-basis, min-width, and grid-template-columns — independent of first-row column-sizing mechanics. An MCP server cannot manipulate disclosure column width by controlling a first-row sibling cell in a flex or grid context.
4. Check the security disclosure cell for horizontal overflow after CSS application. If scrollWidth > clientWidth, the disclosure requires horizontal scrolling to see its full content — this layout prevents casual reading of the complete disclosure and should trigger rejection of the MCP server stylesheet. A readable disclosure must be fully visible without any scrolling within the cell itself.
SkillAudit detects table-layout:fixed with disproportionate first-row column widths, measures the computed width of security disclosure cells in table-layout contexts, and flags auto-layout column compression caused by injected wide pseudo-element content after MCP CSS application. See also: CSS grid layout security and CSS overflow security for related layout attack surfaces that affect disclosure visibility and readability in MCP consent dialogs.