MCP server SVG pattern security
The SVG <pattern> element defines a tiling brush applied to other SVG elements as their fill or stroke. When applied as fill to a consent text element, the pattern tiles its content across every glyph pixel. A pattern whose tiles are the same color as the page background renders the text as invisible while getComputedStyle(el).fill returns a URL reference — not a transparent color — passing most consent auditors' fill-transparency checks.
Finding SA-PAT-001: near-white pattern tile makes consent text invisible against white background
<pattern> element defines a single tile containing a rectangle filled with fill="#fefefe" — a near-white color perceptually indistinguishable from a white background. The pattern is applied as the fill of a consent text element. Each character glyph is rendered in #fefefe against a #ffffff background — invisible to human vision. getComputedStyle(el).fill returns url(#near-white-pattern) — not transparent, not none, not rgba(0,0,0,0). Standard fill-color checks pass; only following the URL reference and checking the tile's fill color reveals the attack.<defs>
<pattern id="near-white-pattern" x="0" y="0" width="10" height="10"
patternUnits="userSpaceOnUse">
<rect width="10" height="10" fill="#fefefe"/> <!-- near-white -->
</pattern>
</defs>
<text fill="url(#near-white-pattern)">
I authorize access to my calendar, contacts, and email.
</text>
Detection gap: getComputedStyle(el).fill returns url(#near-white-pattern). A check for fill !== 'none' and fill !== 'transparent' passes. A check for fill !== 'rgba(0,0,0,0)' passes. Only pattern URL detection followed by tile fill color analysis catches this attack. The color #fefefe is 254/255 on each channel — a 0.4% difference from white that is invisible at display resolution.
Finding SA-PAT-002: animated patternTransform repositions visible region off glyph pixels
animateTransform on the patternTransform attribute shifts the tile position by exactly 1px at button mousedown, moving the dark stripe off the glyph edges and covering all glyph pixels with the near-white region. The text is invisible at the interaction moment.<defs>
<pattern id="shift-pattern" x="0" y="0" width="10" height="1"
patternUnits="userSpaceOnUse">
<rect width="1" height="1" fill="#333"/> <!-- 1px dark stripe -->
<rect x="1" width="9" height="1" fill="#fefefe"/> <!-- 9px near-white -->
<animateTransform attributeName="patternTransform" type="translate"
from="0 0" to="1 0"
begin="submitBtn.mousedown" dur="indefinite" fill="freeze"/>
</pattern>
</defs>
Finding SA-PAT-003: patternContentUnits=objectBoundingBox with full-coverage tile
patternContentUnits="objectBoundingBox", the pattern tile contents are specified in object-bounding-box coordinates (0–1 range). A tile containing a rectangle with width="1" height="1" in object-bounding-box units covers the entire element — creating a single non-tiling fill equivalent to a solid color. Combined with a near-white fill color, this is structurally equivalent to setting fill="#fefefe" but expressed as a pattern reference, evading fill-color checks. The pattern tile geometry requires evaluation in the correct coordinate system to detect the full-coverage attack.<defs>
<pattern id="full-cover" patternContentUnits="objectBoundingBox"
width="1" height="1">
<!-- In objectBoundingBox: 0–1 = full element extent -->
<rect x="0" y="0" width="1" height="1" fill="#fefefe"/>
</pattern>
</defs>
<text fill="url(#full-cover)">Consent text...</text>
Finding SA-PAT-004: sub-glyph micro-tile pattern creates gray blur
<defs>
<pattern id="noise-pattern" x="0" y="0" width="2" height="2"
patternUnits="userSpaceOnUse">
<!-- Checkerboard at 1px: renders as uniform gray at display -->
<rect width="1" height="1" fill="black"/>
<rect x="1" width="1" height="1" fill="white"/>
<rect y="1" width="1" height="1" fill="white"/>
<rect x="1" y="1" width="1" height="1" fill="black"/>
</pattern>
</defs>
<text fill="url(#noise-pattern)">Consent text...</text>
Detection: following pattern fill references and checking tile colors
function checkSVGPatternFills(consentEl) {
const risks = [];
const fillAttr = consentEl.getAttribute('fill')
|| getComputedStyle(consentEl).fill;
if (!fillAttr || !fillAttr.includes('url(')) return risks;
const idMatch = fillAttr.match(/url\(#([^)]+)\)/);
if (!idMatch) return risks;
const patternEl = document.getElementById(idMatch[1]);
if (!patternEl || patternEl.tagName.toLowerCase() !== 'pattern') return risks;
// Check tile shapes for near-background fill colors
const tileShapes = patternEl.querySelectorAll('rect, circle, ellipse, path');
let allNearWhite = true;
let tileCount = 0;
for (const shape of tileShapes) {
tileCount++;
const computedFill = getComputedStyle(shape).fill || shape.getAttribute('fill') || '';
if (!isNearBackground(computedFill)) {
allNearWhite = false;
}
// Check for SMIL animation on patternTransform
const animEls = shape.querySelectorAll('animateTransform[attributeName="patternTransform"]');
for (const anim of animEls) {
const begin = anim.getAttribute('begin') || '';
if (/click|mousedown|focus/.test(begin)) {
risks.push({ finding: 'SA-PAT-002', severity: 'high',
note: 'patternTransform animated at interaction — may shift visible region off glyphs' });
}
}
}
if (tileCount > 0 && allNearWhite) {
risks.push({ finding: 'SA-PAT-001', severity: 'critical',
note: 'All pattern tile fills are near-background color — text rendered invisible' });
}
// Check for sub-pixel tile size (SA-PAT-004)
const tileW = parseFloat(patternEl.getAttribute('width') || '10');
const tileH = parseFloat(patternEl.getAttribute('height') || '10');
if (tileW <= 2 && tileH <= 2 && tileCount > 1) {
risks.push({ finding: 'SA-PAT-004', severity: 'medium',
note: 'Sub-pixel pattern tile — may render as gray noise at display resolution' });
}
return risks;
}
function isNearBackground(colorStr) {
if (/^(white|#fff|#ffffff)$/i.test(colorStr)) return true;
const m = colorStr.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
if (!m) return false;
return parseInt(m[1]) > 240 && parseInt(m[2]) > 240 && parseInt(m[3]) > 240;
}
Remediation
| Control | How it helps |
|---|---|
Detect any fill or stroke attribute on consent text elements that contains a url() reference; follow the reference to the <pattern> element; compute the luminance of all tile shapes via getComputedStyle; flag if all tiles are near-background luminance (> 0.9) |
SA-PAT-001 requires following the fill URL reference. getComputedStyle(el).fill returns the URL — not the color — passing all standard non-transparent checks. Pattern fill URL detection must trigger pattern tile analysis. |
For pattern fills on consent elements, check for animate and animateTransform children of the pattern element with begin= referencing interaction events; evaluate the post-animation tile positions for coverage against the consent glyph bounding box |
SA-PAT-002 is a SMIL timing attack specific to patternTransform. The safe tile alignment at load time does not guarantee safe alignment at interaction time. Only animate child enumeration with begin= inspection reveals the timing attack. |
Do not apply SVG pattern fills to consent text elements; use solid color fills with explicit color values for all consent text |
Preventive: pattern fills on consent text have no legitimate use case. An unconditional flag on any pattern fill URL reference on a consent text element eliminates all four vectors without requiring tile geometry analysis. |
SkillAudit follows SVG pattern fill URL references, computes tile fill luminance via computed styles, detects patternTransform animation at interaction, and flags sub-pixel tile patterns that render as gray noise. Run a free audit on your MCP server GitHub URL.