Security Guide
MCP server font-display security — FOIT flash-of-invisible-text, font swap layout shift, and symbol font substitution attacks on consent disclosures
The CSS font-display descriptor inside @font-face rules controls how the browser handles the interval between page load and web font availability. It defines a block period — during which text may render invisibly or with a fallback — and a swap period — during which the browser will substitute the loaded web font once it arrives. For MCP servers where an attacker controls injected CSS, these timing mechanisms create four exploitable attack surfaces: a font-display:block rule that silences disclosure text for up to three seconds, a never-resolving font URL that extends that silence, a metric-mismatch between the loaded font and its fallback that causes visible text to overflow a hidden container, and an inline symbol font that maps ASCII permission text to unreadable glyphs while leaving the DOM's textContent intact. All four attacks pass naive DOM and accessibility checks.
How font-display works — and why it is an attack surface
When a browser encounters a @font-face rule and begins downloading the font file, it must decide what to show the user for text that depends on that font. Two periods govern this decision:
- Block period: The interval immediately after the font is first needed. During this period, text using the web font is rendered with invisible glyphs — the text occupies its layout space and affects document flow, but its pixels are painted transparently. If the font loads before the block period expires, the browser swaps immediately and text becomes visible. If not, the block period ends and the browser applies the fallback font.
- Swap period: The interval after the block period ends. During this period, if the font finally loads, the browser will swap the fallback text to the web font (causing a layout shift if the metrics differ). After the swap period expires, the fallback font is kept permanently even if the web font eventually loads.
The five font-display values configure the duration of these two periods differently:
auto— browser default; typically similar toblockblock— block period up to 3 seconds (text invisible); swap period infiniteswap— block period approximately 0ms (immediate fallback); swap period infinitefallback— block period approximately 100ms; swap period 3 seconds, then permanent fallbackoptional— block period approximately 100ms; swap period 0ms (if font not cached, never swaps)
The critical detail for attackers: during the block period, the disclosure element's textContent, the DOM structure, and the accessibility tree all return the correct permission text. Only the rendered pixels are invisible. No standard DOM property, no getComputedStyle() call, and no screen reader check distinguishes "text invisibly rendered during FOIT" from "text normally rendered in a loaded font." An audit that does not simulate the font-loading lifecycle will not detect the attack window.
Attack 1 — font-display:block creating a 3-second FOIT window
The most direct attack uses font-display:block and an artificially slow-loading font server. The block period — up to 3 seconds — creates a Flash of Invisible Text (FOIT) window during which the disclosure text is present in the DOM but invisible on screen. If the consent dialog presents an Accept button during this window, a user can click Accept without ever seeing the disclosure text.
@font-face {
font-family: 'ConsentFont';
src: url('https://fonts.attacker.com/slow-font.woff2'); /* artificially slow load */
font-display: block; /* up to 3-second block period: text is invisible */
}
.mcp-disclosure {
font-family: 'ConsentFont', sans-serif;
/* during block period: disclosure text is invisible (FOIT) */
/* textContent, DOM, accessibility tree all return correct permission text */
/* only the rendered pixels show nothing — the disclosure appears blank */
}
Why this is dangerous: The block period duration is a browser-controlled upper bound, not a guaranteed duration. On a slow connection or with a rate-limited font server, the font may take 4–10 seconds to load — and the block period may expire before the font arrives, causing the fallback to render. But on fast connections where the font server deliberately throttles only the initial connection, the browser may hold the block period open for the full 3 seconds while the font eventually trickles in. In either case, the Accept button is active during the invisible window.
Detection requires more than checking font-display values in the stylesheet: the audit must also verify whether the font source URL responds quickly. A font that loads in 50ms poses no practical FOIT risk. A font that takes 4 seconds — whether due to server throttling, geographic latency, or deliberate delay — creates a real attack window. SkillAudit checks both the declared font-display value and the network response time of each font src URL in audit sandbox conditions.
Attack 2 — Never-loading font URL keeping disclosure invisible
A more reliable variant of the block-period attack uses a font source URL that returns HTTP 404 or that never responds at all. The browser enters the block period at page load and waits for the font. On fast networks, the 404 response arrives quickly, the font fails to load, and the block period ends after its nominal 3 seconds, swapping to the system sans-serif fallback. But on slow connections, the 404 response itself may take longer than 3 seconds to arrive — extending the FOIT window beyond what the font-display:block nominal limit suggests.
@font-face {
font-family: 'ConsentText';
src: url('https://cdn.example.com/nonexistent-font.woff2'); /* 404 */
font-display: block;
}
.mcp-disclosure {
font-family: 'ConsentText', sans-serif;
}
Most dangerous in slow network environments: In a simulated 3G environment, a 404 response from a geographically distant server can take 4–6 seconds, during which the browser has not yet confirmed the font failed to load. The block period countdown begins when the font is first needed, but the browser's font-loading state machine does not transition to "failed" until the network response completes. Users on slow mobile connections — exactly the users least likely to notice a blank disclosure box — experience the longest FOIT windows.
Detection: for every @font-face rule applied to a consent disclosure element, SkillAudit fetches the font src URL from the audit sandbox and checks the HTTP response status. URLs returning 4xx responses or connection timeouts on fonts applied to disclosures are flagged immediately. The audit also simulates the disclosure rendering in a throttled-network sandbox to measure the actual FOIT duration in slow-network conditions.
Attack 3 — font-display:fallback + metric-mismatch overflow
This attack targets the swap period rather than the block period, and exploits the difference in character metrics between the loaded web font and its fallback. With font-display:fallback, the block period is only 100ms — too short to be an effective FOIT window. Instead, the attack relies on the fact that the fallback font and the web font have very different character widths. When the disclosure is rendered in the fallback font (Arial Black), the text fits within the container. When users have the web font cached from a prior page load, the wider web font causes the disclosure text to overflow an overflow:hidden container — making it invisible for users who have visited the page before.
@font-face {
font-family: 'FallbackTrap';
src: url('https://very-slow-server.invalid/font.woff2'); /* connection refused / timeout */
font-display: fallback;
/* block period: 100ms — invisible text for 100ms */
/* swap period: 3 seconds — if font loads within 3s, swap; otherwise use fallback forever */
}
.mcp-disclosure {
font-family: 'FallbackTrap', 'Arial Black', sans-serif;
/* 'Arial Black' is the fallback — metrics very different from web font metrics */
}
TOCTOU attack pattern: This is a time-of-check to time-of-use attack. The audit sandbox fetches the page fresh, with no cached fonts — so the fallback renders, the disclosure is visible, and the audit passes. But a real user who has visited the MCP server's consent page once before will have the web font cached. On their second visit, the browser loads the cached web font immediately (no block period, no swap delay) — and the wider font metrics push the disclosure text outside the overflow:hidden container. The disclosure is invisible for precisely the users who have already interacted with the server and are most likely to click Accept quickly.
Detection: SkillAudit checks whether the fallback font metrics differ significantly from the loaded web font metrics for fonts applied to disclosure elements. The audit simulates rendering the disclosure element with both the fallback font and the loaded font, measures the resulting text dimensions, and flags cases where the text overflows an overflow:hidden container in one scenario but not the other. A character width ratio of more than 1.3× between the web font and its fallback on disclosure elements is flagged as a metric-mismatch risk.
Attack 4 — Symbol font substituting permission text with glyphs
This attack does not rely on font loading delays at all. Instead, it uses a @font-face rule with an inline base64 data URI to load a custom font in which standard ASCII characters are mapped to Private Use Area symbol glyphs — a technique equivalent to applying Wingdings to the disclosure text. The disclosure element's textContent contains the correct English permission text. The rendered output is a sequence of unreadable dingbat symbols.
@font-face {
font-family: 'PermissionFont';
src: url('data:font/woff2;base64,...'); /* inline base64 — a Wingdings-style symbol font */
font-display: swap;
}
.mcp-disclosure {
font-family: 'PermissionFont';
/* "READ EXECUTE DELETE access permissions requested" renders as */
/* "♔♖♘♙ ♕♗♞♙♔♕ ♘♙♔♕♗♘ ♙♔♙♕♔♗♙ ♕♙♔♕♗♙♙♔♔♗♙♘♙♕ ♔♙♕♚♙♔♗♕♙♘" */
/* the DOM has the correct ASCII text; the symbol font renders unreadable glyphs */
}
Why this bypasses both DOM and accessibility checks: The data URI font is embedded inline — there is no network request, no loading delay, and no FOIT. The symbols render immediately at page load. The element's textContent returns the correct English permission text because the DOM stores Unicode code points, not glyph indices. Screen readers read textContent — so accessibility checks announce the correct permission warning. Only a sighted user looking at the rendered consent dialog sees dingbat symbols instead of readable text. A security audit that reads textContent or uses a screen reader will not detect this attack.
Detection requires loading the font and inspecting its character-to-glyph mapping. SkillAudit extracts inline data URI fonts and loads them in a font analysis sandbox. For each font applied to a consent disclosure element, the audit checks whether the glyphs for common ASCII characters (Unicode range 0x41–0x7A: uppercase A through lowercase z, plus space and digits) are alphabetic letterforms or non-alphabetic symbol glyphs. Fonts where more than 20% of the standard ASCII range maps to symbol or Private Use Area glyphs are flagged as symbol substitution risks. Additionally, SkillAudit renders the disclosure element in the audit sandbox and compares the pixel output against a reference rendering using system sans-serif — significant visual divergence triggers a visual mismatch alert.
Summary table
| Attack | font-display value |
Disclosure appearance | Severity |
|---|---|---|---|
| Slow font server block period | block |
Invisible for up to 3 seconds during block period; fallback appears after | High |
| Never-loading font URL | block |
Invisible until block expires (up to 3s+); on slow networks, 404 response delays extend FOIT beyond nominal limit | High |
| Metric-mismatch overflow | fallback |
Visible with fallback; overflows overflow:hidden container when web font is cached — invisible on repeat visits |
High |
| Symbol font substitution | swap |
Renders as unreadable symbol glyphs immediately; DOM and accessibility tree return correct text | High |
Detection checklist
Detecting all four attack variants requires a multi-layer audit that covers both static CSS analysis and dynamic font-loading simulation. SkillAudit's methodology covers the following controls:
- Enumerate all
@font-facerules applied to consent disclosure elements. Walk the CSS cascade for each disclosure element and collect every@font-facerule in the matched font family list, including fallback entries. This includes rules in stylesheets, inline style attributes, and injected style tags. - Fetch font src URLs and check HTTP status. For each
@font-facesrc URL (excluding data URIs, which are handled separately), issue a network request from the audit sandbox and record the HTTP response status and response time. Flag 4xx responses and connection timeouts on fonts applied to disclosure elements. Record response time for all 2xx responses — slow responses (over 1 second) on disclosure fonts are flagged as FOIT-risk candidates. - Flag
font-display:blockon fonts applied to disclosures. Any@font-facerule withfont-display:blockapplied to a consent disclosure element is a potential FOIT attack surface. Flag the finding and correlate with the font src URL response time to assess actual risk duration. - Extract and analyze inline data URI fonts. For
@font-facerules withsrc: url('data:font/...'), extract the base64 payload, decode it, and load the font in a character analysis sandbox. Map ASCII code points 0x20–0x7E to their rendered glyphs and check whether the glyph outlines match alphabetic letterforms. Flag fonts where common ASCII characters map to non-alphabetic symbol glyphs. - Render the disclosure element and compare pixel output. In the audit sandbox, render the disclosure element with all
@font-facefonts loaded and capture a pixel snapshot. Render the same element'stextContentusing the system sans-serif font and capture a reference snapshot. Compute the visual similarity score between the two. Flag significant visual divergence (similarity below 70%) as a potential symbol substitution attack. - Check fallback font metrics versus web font metrics. For each
@font-faceapplied to a disclosure, measure the character advance widths for the web font and its declared CSS fallback. Simulate rendering the full disclosure text in both fonts at the element's computed width. If the web font causes overflow where the fallback does not (or vice versa) and the element hasoverflow:hidden, flag the metric-mismatch as a TOCTOU overflow risk.
SkillAudit findings
@font-face { font-display:block } applied to consent disclosure. Font source URL https://slow.example.com/font.woff2 returned HTTP 200 after 4.2 seconds in audit sandbox. 3-second block period: disclosure text was invisible (FOIT) from load until t=3s. Accept button was available during FOIT window — user could click Accept without seeing disclosure text.
@font-face with font-display:block and font source URL returning HTTP 404. Block period started at t=0. After 3 seconds, browser swapped to system sans-serif fallback. During the 3-second block window on slow connections, the 404 response had not completed, extending the FOIT beyond the nominal 3-second block period. Disclosure was invisible for 4.8 seconds in simulated 3G environment.
font-display:fallback with font-family:'CustomWide', 'Arial Black'. Fallback renders correctly (textContent visible in Arial Black). Loaded font character width is 1.8× Arial Black for uppercase characters. With loaded font, disclosure at 280px container width overflows by 340px. Container has overflow:hidden. Audit run with fallback passes; users with cached font see no disclosure.
@font-face font applied to disclosure. Character analysis: ASCII range 0x41–0x7A (A-z) maps to Private Use Area glyphs (dingbat symbols). textContent returns "HIGH RISK: This server requests DELETE access to your file system." Rendered output: unreadable symbol sequence. Screen reader announces correct text. Sighted user sees dingbat symbols only.
Related security guides: font-display attacks are one dimension of font-based consent disclosure abuse. See also Font Loading API attack surface (programmatic font loading via the CSS Font Loading API), font-feature-settings attacks (OpenType feature abuse for glyph substitution), and font-variant attacks on disclosure text (small-caps and ligature-based obfuscation).