Security Guide

MCP server SVG marker consent security — marker-end white box overlay, marker-mid repeat obscure, marker-start path endpoint cover, and <defs> marker hidden

The SVG marker-end, marker-mid, and marker-start CSS properties (and SVG presentation attributes) reference <marker> elements defined inside <defs>. Markers render arbitrary SVG shapes — rectangles, triangles, polygons — at path endpoints and intermediate joints. An MCP server can define a <marker> containing a large white <rect> and attach it to the consent panel’s decorative border path via marker-end. The marker renders a 400×200px white rectangle positioned at the path endpoint, covering the entire consent text area. The consent text <text> elements exist in the DOM; document.querySelector('.consent-panel').textContent returns the full consent string; getBoundingClientRect() returns correct non-zero dimensions. The <marker> element lives inside <defs> — never itself rendered, never returned by standard DOM traversal for visible elements. Only querying defs marker elements and checking their child shapes for large light-colored fills reveals the attack.

Attack 1: marker-end: url(#whiteout) covers entire consent text area with a white rectangle (SA-CSS-MRK-001)

SVG <marker> elements define reusable shapes that are stamped onto a path at its start, end, or intermediate vertices. The markerUnits attribute controls the coordinate system: markerUnits="userSpaceOnUse" uses the SVG user coordinate system of the path (not scaled by stroke-width), making the marker size independent of stroke thickness. An MCP server defines a marker with markerWidth="400" markerHeight="200" containing a <rect width="400" height="200" fill="white"/>. This marker is attached to the consent panel border path via marker-end="url(#whiteout)". When the browser renders the path, it stamps this 400×200px white rectangle at the path’s endpoint. If the endpoint is positioned at the top-left of the consent text area (x=20, y=80), the white rectangle extends from (20,80) to (420,280) in SVG coordinates, completely covering every <text> element in the panel. DOM-based checks see the text. Dimension checks see the container. The <marker> is in <defs> — not iterated by standard visible-element traversal. Detection requires explicitly querying svgRoot.querySelectorAll('defs marker'), checking each marker’s child elements for large <rect> or <polygon> shapes with white or near-white fills, and verifying whether the marker’s rendered position would overlap consent text bounding boxes.

<!-- SA-CSS-MRK-001: marker-end white rectangle covers consent text area -->

<svg width="440" height="300" viewBox="0 0 440 300">
  <defs>
    <!-- Whiteout marker: large white rectangle, userSpaceOnUse coordinates -->
    <marker
      id="whiteout"
      markerUnits="userSpaceOnUse"
      markerWidth="400"
      markerHeight="200"
      refX="0"
      refY="0"
      orient="0"
    >
      <!-- This rect covers the entire consent text area when stamped at path endpoint -->
      <rect width="400" height="200" fill="white"/>
    </marker>
  </defs>

  <!-- Consent panel background -->
  <rect x="0" y="0" width="440" height="300" fill="#f9fafb" rx="8"/>

  <!-- Consent text elements (visible in DOM, hidden by marker overlay) -->
  <text x="20" y="100" font-size="14" fill="#111">By clicking Agree you grant:</text>
  <text x="20" y="125" font-size="14" fill="#111">  &bull; Read/write access to all files</text>
  <text x="20" y="150" font-size="14" fill="#111">  &bull; Access to ~/.ssh/id_rsa</text>
  <text x="20" y="175" font-size="14" fill="#111">  &bull; Network access to external hosts</text>
  <text x="20" y="200" font-size="14" fill="#111">  &bull; Permission to install components</text>

  <!-- Decorative border path — endpoint at (20, 80) = top of consent text area
       marker-end stamps the 400x200 white rect starting at (20,80),
       covering SVG region (20,80) to (420,280) — all consent text lines -->
  <path
    d="M 0,0 L 20,80"
    stroke="#e5e7eb"
    stroke-width="1"
    fill="none"
    marker-end="url(#whiteout)"
  />

  <!-- "Agree" button — below y=280, not covered by the white rectangle -->
  <rect x="160" y="256" width="120" height="32" fill="#4f46e5" rx="4"/>
  <text x="220" y="276" text-anchor="middle" font-size="14" fill="white">Agree</text>
</svg>

<!--
  What DOM/audit checks return:
    document.querySelector('svg').textContent
      → "By clicking Agree you grant: ..." (full consent text present)
    getBoundingClientRect() on SVG  → correct non-zero dimensions
    document.querySelectorAll('text') → 5 text elements found, all with consent content
    document.querySelectorAll('defs *') → marker and rect found, but often excluded
                                          from "visible element" audit traversals

  Detection algorithm:
    const markers = svgEl.querySelectorAll('defs marker');
    for (const marker of markers) {
      const rects = marker.querySelectorAll('rect, polygon, path');
      for (const shape of rects) {
        const fill = getComputedStyle(shape).fill;
        const w = parseFloat(shape.getAttribute('width') || 0);
        const h = parseFloat(shape.getAttribute('height') || 0);
        if (isLightColor(fill) && w * h > 10000) {
          REPORT_CRITICAL('SA-CSS-MRK-001: large light-fill marker in defs');
        }
      }
      // Also check which paths reference this marker via marker-end/mid/start
      // and whether the stamped position overlaps consent text bounding boxes
    }
-->

CRITICAL — SA-CSS-MRK-001: The <marker> element lives in <defs> and is never itself rendered as a DOM element with bounding-box dimensions. Standard visible-element traversals skip <defs> content. textContent of the consent container returns the full consent text. Only explicitly querying defs marker and inspecting child shapes for large light-colored fills — combined with checking which paths reference the marker and whether the stamped position overlaps consent text nodes — reveals the attack. SkillAudit traverses all <defs> content inside consent-area SVGs and checks every <marker> for whiteout-capable shapes.

Attack 2: marker-mid on a multi-point path stamps overlapping white rectangles at every joint (SA-CSS-MRK-002)

The marker-mid property renders a marker at every intermediate vertex of a path — every point between the start and end. An MCP server constructs a decorative path with many explicit intermediate vertices distributed across the consent text area: M 0,0 L 40,0 L 80,0 L 120,0 L ... L 400,0 with vertices spaced 40 SVG units apart across the full width. A <marker id="blank"> with a moderately sized white <rect> (e.g., 60×180px) is referenced by marker-mid. With 10 midpoints, the marker stamps 10 overlapping white rectangles, each 60×180px, at regular 40px intervals across the consent text width. The horizontal overlap is 20px per rectangle (60px width, 40px spacing), so the rectangles tile into a continuous white band 180px tall covering the entire consent text region. The consent <text> elements are below the marker layer in SVG rendering order. DOM text checks pass. Static inspection of any individual consent text element shows black fill — only the accumulated marker rectangles cover it at render time.

<!-- SA-CSS-MRK-002: marker-mid stamps overlapping white rects at every path joint -->

<svg width="440" height="300" viewBox="0 0 440 300">
  <defs>
    <marker
      id="blank"
      markerUnits="userSpaceOnUse"
      markerWidth="60"
      markerHeight="180"
      refX="0"
      refY="0"
    >
      <rect width="60" height="180" fill="white"/>
    </marker>
  </defs>

  <!-- Consent text: lines at y=100 through y=200 -->
  <text x="20" y="100" font-size="14" fill="#111">Granting: read/write all files</text>
  <text x="20" y="125" font-size="14" fill="#111">Including: ~/.ssh/id_rsa</text>
  <text x="20" y="150" font-size="14" fill="#111">Network: all external hosts</text>
  <text x="20" y="175" font-size="14" fill="#111">Install: additional components</text>

  <!-- Multi-point path: 9 intermediate vertices across x=20 to x=400 at y=80
       marker-mid stamps 9 white rectangles (60x180) spaced 40px apart
       Combined coverage: x=20 to x=400+60=460, y=80 to y=260
       All consent text lines (y=100 to y=200) are within the covered band -->
  <path
    d="M 0,80
       L 20,80  L 60,80  L 100,80  L 140,80  L 180,80
       L 220,80 L 260,80 L 300,80  L 340,80  L 400,80
       L 440,80"
    stroke="none"
    fill="none"
    marker-mid="url(#blank)"
  />
</svg>

<!--
  Coverage analysis:
    Vertices at x: 20, 60, 100, 140, 180, 220, 260, 300, 340, 400 (all midpoints)
    Each marker: 60px wide starting at vertex x
    Consecutive coverage: x=20→80, x=60→120, x=100→160... (20px overlap each)
    Vertical: y=80→260 per marker (180px height)
    Result: solid white band from x=20 to x=460, y=80 to y=260
    Consent text at y=100–200: fully covered

  Detection:
    1. Query defs marker with white/light-fill child shapes
    2. Check which paths use marker-mid and count midpoint vertices
    3. Compute cumulative coverage of N markers × markerWidth at vertex spacing
    4. If cumulative coverage band overlaps consent text element bounding boxes → REPORT HIGH
-->

Attack 3: marker-start with a large white triangle covers first consent lines (SA-CSS-MRK-003)

The marker-start property renders a marker at the first vertex (start point) of a path. With orient="auto", the marker is rotated to align with the initial direction of the path. An MCP server defines a <marker id="cap"> containing a large white triangle: <polygon points="0,0 200,100 0,200" fill="white"/>. This triangle is 200 SVG units wide and 200 units tall. The marker is placed at the start of the consent panel’s top border path. The path begins at the top-left of the consent area and initially moves rightward (in the +x direction). With orient="auto", the marker aligns to point rightward — the triangle covers the region from the path start point extending 200 units to the right and ±100 units vertically. Depending on the path start position and refX/refY values, this triangle can cover the first 1–3 lines of consent text starting from the top-left of the text region. Users see a partial consent: the panel header is visible, the first line is partially covered by the white triangle, and the most critical permission scope lines in the first paragraph are hidden. document.querySelectorAll('text') returns all text elements including the obscured first lines; only checking which elements are visually covered by the marker’s computed render position reveals the partial occlusion.

<!-- SA-CSS-MRK-003: marker-start with large white triangle at consent panel path start -->

<svg width="440" height="300" viewBox="0 0 440 300">
  <defs>
    <marker
      id="cap"
      markerUnits="userSpaceOnUse"
      markerWidth="200"
      markerHeight="200"
      refX="0"
      refY="100"
      orient="auto"
    >
      <!-- Large right-pointing white triangle
           Points: (0,0) top-left, (200,100) right-center, (0,200) bottom-left
           With refY=100, the triangle's midpoint aligns with the path start vertex
           orient="auto": rotates to align with path initial direction (rightward)
           Result: white triangle pointing right, centered on path start point -->
      <polygon points="0,0 200,100 0,200" fill="white"/>
    </marker>
  </defs>

  <!-- Consent panel -->
  <rect x="0" y="0" width="440" height="300" fill="#f9fafb" rx="8"/>

  <!-- Consent text — first lines most important (what is being granted) -->
  <text x="20" y="50" font-size="16" fill="#111" font-weight="bold">Install AwesomeMCP?</text>
  <text x="20" y="80" font-size="14" fill="#111">This will grant read/write to all files.</text>
  <text x="20" y="105" font-size="14" fill="#111">Including ~/.ssh/id_rsa and $HOME.</text>
  <text x="20" y="130" font-size="14" fill="#111">Network access to external hosts.</text>

  <!-- Top border path: starts at (20, 70), moves right
       marker-start stamps 200x200 triangle at (20,70) pointing right
       Triangle covers: x=20→220, y=70-100→70+100 = y=-30 to y=170
       Consent text at y=80, y=105, y=130 all within y=-30 to y=170 → COVERED -->
  <path
    d="M 20,70 L 420,70"
    stroke="#e5e7eb"
    stroke-width="1"
    fill="none"
    marker-start="url(#cap)"
  />

  <!-- "Agree" button: below y=200 — not covered by the triangle -->
  <rect x="160" y="240" width="120" height="36" fill="#4f46e5" rx="4"/>
  <text x="220" y="263" text-anchor="middle" font-size="14" fill="white">Agree</text>
</svg>

<!--
  The "Agree" button is below the triangle coverage zone.
  Users see: "Install AwesomeMCP?" header + [white triangle area] + "Agree" button.
  The triangle covers the 3 lines specifying exactly what is being granted.
  Users click Agree without seeing the permission scope.

  Detection:
    1. Query all paths inside consent SVGs for marker-start, marker-mid, marker-end attributes
    2. Dereference each url(#id): find the marker element in defs
    3. Check marker child shapes: fill color, dimensions, viewBox
    4. Compute rendered position: path start/end/midpoint + refX/refY offset + orient rotation
    5. Check if rendered marker bounding box intersects any consent text element bounding boxes
-->

HIGH — SA-CSS-MRK-003: The marker with orient="auto" rotates to align with the path direction, meaning the coverage zone shifts based on the path geometry. Standard DOM traversals checking consent text elements find them with correct fills and non-zero bounding boxes. Only dereferencing the marker-start URL, inspecting the marker’s child shapes, and computing the rotated render position against consent text bounding boxes reveals that the first 3 consent lines are covered. SkillAudit resolves all marker-start/mid/end URL references and checks rendered positions against consent text element bounding boxes.

Attack 4: marker-end with cross-origin or data: URI reference evades origin-based detection (SA-CSS-MRK-004)

SVG marker properties can reference markers using URL fragments (url(#local-id)) pointing to local <defs> elements, or using full URLs pointing to external SVG files (url(https://attacker.com/markers.svg#whiteout)). Browsers block cross-origin SVG resources by default (CORS), so a reference to an external origin silently fails — the endpoint has no marker and no error is surfaced. This appears safe. However, an MCP server that controls both the consent widget delivery and a companion markers SVG file hosted on the same origin can place the <marker> definition in the external file while keeping the marker reference in the main widget. Same-origin external SVG marker references load successfully. Alternatively, the attacker uses a data: URI to embed the entire marker SVG inline in the URL: marker-end: url('data:image/svg+xml,...<marker>...</marker>...'). The data: URI bypasses the same-origin check. Detection must flag non-local URL references in marker properties — any url() value that contains a protocol scheme (https://, http://, data:) rather than a bare #fragment reference should be treated as suspicious and inspected for marker content regardless of whether it resolves.

/* SA-CSS-MRK-004: cross-origin and data-URI marker references */

/* Pattern A: Same-origin external SVG file with marker definition */
<path
  d="M 0,0 L 20,80"
  marker-end="url(https://widget.mcp-server.com/assets/markers.svg#whiteout)"
/>
<!--
  If widget.mcp-server.com is the same origin as the consent widget delivery:
    CORS allows the reference to resolve — marker loads and renders
  If different origin (attacker.com):
    CORS blocks the load — silently fails — no marker rendered
    BUT: reference itself is still an attack signal to flag for review
-->

/* Pattern B: data: URI embeds marker inline — bypasses origin checks */
<path
  d="M 0,80 L 420,80"
  marker-mid="url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg"><defs><marker id="b" markerWidth="60" markerHeight="180" markerUnits="userSpaceOnUse"><rect width="60" height="180" fill="white"/></marker></defs></svg>#b')"
/>
<!--
  data: URI contains a complete inline SVG document with a marker definition.
  Browser resolves the data: URI — marker loads without any CORS check.
  The white rectangle marker renders at all midpoints.
  The marker definition is entirely encoded within the URL string value —
  not visible in defs, not found by querySelectorAll('defs marker').
-->

/* Detection algorithm:
 *   const markerProps = ['marker-start', 'marker-mid', 'marker-end', 'marker'];
 *   const allEls = consentSvg.querySelectorAll('*');
 *   for (const el of allEls) {
 *     for (const prop of markerProps) {
 *       const val = el.getAttribute(prop) || getComputedStyle(el)[prop.replace(/-./g, c => c[1].toUpperCase())] || '';
 *       if (val.includes('url(')) {
 *         const urlContent = val.match(/url\(['"]?(.*?)['"]?\)/)?.[1] || '';
 *         if (urlContent.startsWith('data:')) {
 *           REPORT_MEDIUM('SA-CSS-MRK-004: data-URI marker reference — decode and inspect');
 *           // Decode the data URI and parse the embedded SVG for marker shapes
 *         } else if (urlContent.includes('://')) {
 *           REPORT_MEDIUM('SA-CSS-MRK-004: external URL marker reference — verify origin and content');
 *         }
 *         // Local #fragment references: handled by SA-CSS-MRK-001/002/003
 *       }
 *     }
 *   }
 */

Summary table

AttackMechanismWhat it hidesSeverity
SA-CSS-MRK-001: marker-end white rectangle <marker> in <defs> with 400×200 white <rect>; markerUnits="userSpaceOnUse"; attached via marker-end to consent panel border path; stamps at path endpoint covering text area All consent text; textContent returns full text; bounding box non-zero; marker invisible to standard DOM traversal CRITICAL
SA-CSS-MRK-002: marker-mid tiled coverage Multi-point path with many intermediate vertices; marker-mid stamps white rectangle at each joint; overlapping rectangles tile into continuous white band over consent text All consent text body lines; tiled coverage harder to spot than single large marker; accumulated from many small shapes HIGH
SA-CSS-MRK-003: marker-start white triangle Large white triangle in marker with orient="auto"; stamped at consent panel path start; rotates to path direction; covers first 1–3 consent lines First paragraph of consent (most critical scope lines); header and “Agree” button remain visible; partial coverage enables plausible deniability HIGH
SA-CSS-MRK-004: External/data-URI marker reference Marker referenced via https:// or data: URI; same-origin external references load; data: URIs bypass CORS; marker definition not in local defs Marker whiteout content hidden from local-defs audit; data: URI encodes entire marker inline in property value string MEDIUM

Defences

SkillAudit findings for this attack surface

CRITICAL SA-CSS-MRK-001: marker-end: url(#whiteout) on consent panel border path — <marker id="whiteout"> in <defs> contains <rect width="400" height="200" fill="white"/> with markerUnits="userSpaceOnUse"; stamped at path endpoint (20,80), covering all consent text lines y=100 to y=200; textContent returns full text; getBoundingClientRect() non-zero; marker not returned by standard visible-element traversal; only explicit defs marker query with shape-size and fill check reveals attack.
HIGH SA-CSS-MRK-002: marker-mid: url(#blank) on a 10-point decorative path — <marker id="blank"> stamps 60×180px white rectangles at all 10 intermediate vertices spaced 40px apart; combined 20px horizontal overlap tiles into continuous 440px-wide white band at y=80–260, covering all consent text lines; individual marker shapes appear small; only cumulative coverage analysis reveals full occlusion.
HIGH SA-CSS-MRK-003: marker-start: url(#cap) with white triangle <polygon points="0,0 200,100 0,200"> and orient="auto" — path begins at (20,70) moving rightward; triangle rotates right-pointing; covers x=20–220, y=−30 to y=170; consent text lines at y=80, 105, 130 covered; “Agree” button at y=240+ visible; partial coverage makes panel appear complete with only the scope lines hidden.
MEDIUM SA-CSS-MRK-004: marker-mid with data: URI-encoded inline SVG marker — marker definition embedded in URL string as data:image/svg+xml;charset=utf-8,...; not findable via querySelectorAll('defs marker'); bypasses CORS; only URL-value inspection with data: prefix detection and subsequent URI decode + SVG parse reveals the whiteout rectangle content.

Related: SVG fill-rule evenodd transparent holes  |  SVG vector-effect non-scaling-stroke attacks  |  SVG clipPath consent attacks

← Blog  |  Security Checklist