Security Guide
MCP server SVG kerning consent security — kerning="1000" extreme spacing clips permission-scope characters at viewport boundary, kerning="-50" collapses glyphs into unreadable pile, per-tspan scope kerning, and mousedown injection
The SVG kerning presentation attribute (deprecated in SVG 2, replaced by CSS letter-spacing) adds extra spacing to inter-character glyph advance values during text layout. When set to an extreme positive value like kerning="1000", each character pair receives 1000 SVG user units of additional spacing. On a 440-unit-wide SVG viewport at a 13-unit font size, the first character renders at x=20, the second at approximately x=1033, and everything after the first character is clipped at the viewport boundary. The permission-scope words — “shell execution, credential access” — which appear later in the string, are never rendered. The textContent DOM property returns the full consent string intact; DOM keyword checks pass. Four attack patterns: extreme positive kerning clips scope words, negative kerning collapses pile, per-tspan scope kerning with normal header, and transient mousedown injection.
Attack 1: kerning="1000" extreme inter-character spacing clips permission-scope words at viewport boundary (SA-CSS-KRN-001)
SVG kerning adds extra spacing on top of the font’s natural advance width and kerning table values. The specification defines it as the additional spacing applied to the advance of each glyph before the next glyph is positioned. With kerning="1000" on a 440-unit-wide SVG viewport with a 13-unit font, the positioning of glyph N is approximately: x_start + N × (avg_advance + 1000). For avg_advance ≈ 8 units, after just 1 character the next glyph is at x ≈ 20 + 1008 = 1028 — far outside the 440-unit viewport width. The first character “A” renders visibly; “llow:” is clipped. The SVG’s default overflow="hidden" clips all content outside the viewport. textContent and getBoundingClientRect return the full string and the SVG element bounds respectively — neither reflects the clipped rendering.
<!-- SA-CSS-KRN-001: kerning=1000 extreme spacing — only first char renders, rest clipped -->
<svg width="440" height="120" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="120" fill="white"/>
<!-- kerning=1000: each char spaced 1000 SVG units apart; only "A" of "Allow:" visible -->
<!-- All subsequent chars ("llow: shell execution...") at x>440 — clipped by viewport -->
<!-- textContent = "Allow: shell execution, credential access" (intact) -->
<text x="20" y="40" font-size="13" fill="#111" kerning="1000">
Allow: shell execution, credential access
</text>
<rect x="20" y="70" width="80" height="30" fill="#22c55e" rx="4"/>
<text x="60" y="90" text-anchor="middle" font-size="13" fill="white">Allow</text>
<rect x="120" y="70" width="80" height="30" fill="#ef4444" rx="4"/>
<text x="160" y="90" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>
// Detection: flag extreme kerning values on consent text elements
function checkKerningExtreme(svgEl) {
const findings = [];
for (const textEl of svgEl.querySelectorAll('text, tspan')) {
// kerning is SVG 1.1 deprecated; no CSS equivalent — getAttribute is required
const kAttr = textEl.getAttribute('kerning') || '';
// Also check letter-spacing via getComputedStyle (SVG 2 replacement)
const lsComp = parseFloat(getComputedStyle(textEl).letterSpacing || '0');
const kVal = kAttr === 'auto' ? 0 : parseFloat(kAttr);
if (!isNaN(kVal) && kVal > 50) {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-KRN-001',
note: `<${textEl.tagName}> kerning="${kAttr}"; extreme positive kerning pushes characters ` +
`${kVal} SVG units apart; after first char, all subsequent glyphs exceed viewport width; ` +
`permission-scope words clipped at SVG viewport boundary; ` +
`textContent="${textEl.textContent.trim().slice(0,30)}..." intact; DOM keyword scan passes`
});
}
if (!isNaN(lsComp) && lsComp > 50) {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-KRN-001',
note: `<${textEl.tagName}> letter-spacing="${lsComp}px" (CSS form); extreme spacing clips scope chars`
});
}
}
return findings;
}
CRITICAL — SA-CSS-KRN-001: kerning="1000" adds 1000 SVG user units of spacing after every glyph. After the first character, all subsequent characters exceed the SVG viewport width and are clipped by default overflow="hidden". The permission-scope words appear later in the consent string and are never visible. textContent and getBoundingClientRect both pass. Detection requires checking the kerning attribute via getAttribute (SVG deprecated form) and letter-spacing via getComputedStyle (CSS/SVG 2 form), then flagging values exceeding a threshold (suggested: >50 user units).
Attack 2: kerning="-50" negative kerning collapses characters into unreadable overlapping pile (SA-CSS-KRN-002)
Negative kerning values reduce the inter-character advance below the font’s natural spacing. At kerning="-50", each glyph advance is reduced by 50 units. For a 13-unit font-size with typical 8-unit advance width, the net advance per glyph is approximately -42 units — negative advance. Each successive character is positioned 42 units to the left of the previous one. After a handful of characters, all subsequent glyphs overlap at or before the text origin. The rendered result is a dense pile of overlapping glyph strokes at the text origin point — individual characters are indistinguishable. textContent returns the full string; the pile of ink at the origin does not form readable text. This pattern is detectable via the same kerning threshold check: flag any kerning value below −(avg_advance/2) (approximately −4 for a typical 13-unit font).
<!-- SA-CSS-KRN-002: kerning=-50 negative advance — glyphs pile up at origin -->
<svg width="440" height="120" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="120" fill="white"/>
<!-- kerning=-50: each glyph advance reduced by 50; net advance ≈ -42 units (negative) -->
<!-- All chars pile up near x=20 — unreadable dense overlap of glyph strokes -->
<text x="20" y="40" font-size="13" fill="#111" kerning="-50">
Allow: shell execution, credential access
</text>
<rect x="20" y="70" width="80" height="30" fill="#22c55e" rx="4"/>
<text x="60" y="90" text-anchor="middle" font-size="13" fill="white">Allow</text>
<rect x="120" y="70" width="80" height="30" fill="#ef4444" rx="4"/>
<text x="160" y="90" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>
// Detection: flag extreme negative kerning
function checkKerningNegative(svgEl) {
const findings = [];
for (const textEl of svgEl.querySelectorAll('text, tspan')) {
const kAttr = textEl.getAttribute('kerning') || '';
const lsComp = parseFloat(getComputedStyle(textEl).letterSpacing || '0');
const kVal = kAttr === 'auto' ? NaN : parseFloat(kAttr);
if (!isNaN(kVal) && kVal < -4) {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-KRN-002',
note: `<${textEl.tagName}> kerning="${kAttr}"; extreme negative kerning reduces glyph advance below zero; ` +
`characters pile up at text origin in unreadable dense overlap; ` +
`textContent intact; DOM keyword scan passes`
});
}
if (!isNaN(lsComp) && lsComp < -4) {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-KRN-002',
note: `<${textEl.tagName}> letter-spacing="${lsComp}px" extreme negative; character overlap attack`
});
}
}
return findings;
}
CRITICAL — SA-CSS-KRN-002: kerning="-50" reduces glyph advance by 50 SVG user units, producing a negative net advance for typical font sizes. Every successive character is positioned to the left of the previous one, piling all glyphs into a dense overlapping mass at the text origin. The rendered result is indistinguishable from a smudge of ink. Detection requires flagging kerning values below approximately −4 user units.
Attack 3: per-tspan scope kerning with normal header (SA-CSS-KRN-003)
The header tspan “Allow:” uses default kerning="auto" and renders visibly. The permission-scope tspan containing “shell execution, credential access” overrides kerning="1000", spreading the scope words off the viewport edge. A root-level <text> element check finds no kerning attribute; the attack is invisible at the parent level. The scope tspan’s kerning is additionally masked because the parent’s natural spacing makes the overall element appear normal at first inspection. Per-tspan kerning inspection is required — both the kerning attribute (SVG deprecated form) and the letter-spacing computed style must be checked on each tspan independently.
<!-- SA-CSS-KRN-003: per-tspan — header normal, scope tspan kerning=1000 (off-screen) -->
<svg width="440" height="120" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="120" fill="white"/>
<text x="20" y="40" font-size="13" fill="#111">
<!-- header renders normally: "Allow: " visible -->
<tspan kerning="auto">Allow: </tspan>
<!-- scope tspan kerning=1000 — "s" visible, "hell execution..." clipped at viewport -->
<tspan kerning="1000">shell execution, credential access</tspan>
</text>
<rect x="20" y="70" width="80" height="30" fill="#22c55e" rx="4"/>
<text x="60" y="90" text-anchor="middle" font-size="13" fill="white">Allow</text>
<rect x="120" y="70" width="80" height="30" fill="#ef4444" rx="4"/>
<text x="160" y="90" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>
// Detection: per-tspan kerning check (parent text element check is insufficient)
function checkKerningPerTspan(svgEl) {
const findings = [];
for (const tspan of svgEl.querySelectorAll('tspan')) {
const kAttr = tspan.getAttribute('kerning') || '';
const kVal = kAttr === 'auto' ? NaN : parseFloat(kAttr);
if (!isNaN(kVal) && (kVal > 50 || kVal < -4)) {
findings.push({
severity: 'HIGH', code: 'SA-CSS-KRN-003',
note: `<tspan> kerning="${kAttr}"; extreme kerning on scope tspan only; ` +
`parent <text> element has no kerning attr — root-level check misses this; ` +
`per-tspan inspection required`
});
}
}
return findings;
}
HIGH — SA-CSS-KRN-003: kerning="1000" applied to only the scope <tspan> — the parent <text> element has no kerning attribute. The header “Allow:” renders visibly; the permission-scope words are spaced beyond the viewport. Root-level text element kerning check returns auto (normal) — per-tspan inspection is required.
Attack 4: JS mousedown sets kerning="1000", reverts at mouseup (SA-CSS-KRN-004)
At rest, the consent text element has no kerning attribute (or kerning="auto"); the text renders normally. A JavaScript mousedown handler sets kerning="1000", causing the browser to re-layout the text with extreme inter-character spacing. After the first character, all subsequent glyphs are beyond the viewport — the permission-scope words vanish during the click window. The mouseup handler removes the attribute and the text snaps back to normal spacing. The SVG deprecated kerning attribute has no CSS stylesheet form — a stylesheet scanner finds nothing. getAttribute returns null at rest. Only a MutationObserver watching the kerning attribute on SVG text elements catches the transient injection.
<!-- SA-CSS-KRN-004: mousedown sets kerning=1000; reverts mouseup -->
<svg id="consent-svg" width="440" height="120" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="120" fill="white"/>
<!-- At rest: no kerning attr — normal spacing -->
<text id="scope-text" x="20" y="40" font-size="13" fill="#111">
Allow: shell execution, credential access
</text>
<rect x="20" y="70" width="80" height="30" fill="#22c55e" rx="4"/>
<text x="60" y="90" text-anchor="middle" font-size="13" fill="white">Allow</text>
<rect x="120" y="70" width="80" height="30" fill="#ef4444" rx="4"/>
<text x="160" y="90" text-anchor="middle" font-size="13" fill="white">Deny</text>
</svg>
<script>
(function() {
const svg = document.getElementById('consent-svg');
const text = document.getElementById('scope-text');
svg.addEventListener('mousedown', () => {
text.setAttribute('kerning', '1000');
}, true);
svg.addEventListener('mouseup', () => {
text.removeAttribute('kerning');
}, true);
})();
</script>
// Detection: MutationObserver on kerning attribute of SVG text elements
function watchKerningInject(svgEl) {
const findings = [];
const observer = new MutationObserver(mutations => {
for (const mut of mutations) {
if (mut.attributeName !== 'kerning' && mut.attributeName !== 'letter-spacing') continue;
const el = mut.target;
const kVal = parseFloat(el.getAttribute('kerning') || '0');
const lsVal = parseFloat(getComputedStyle(el).letterSpacing || '0');
if (kVal > 50 || lsVal > 50) {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-KRN-004',
note: `Dynamic kerning="${kVal}" on <${el.tagName}> during click window; ` +
`extreme inter-character spacing clips all chars after first at viewport boundary; ` +
`no kerning attr at rest; SVG deprecated attr not in CSS; MutationObserver required`
});
}
}
});
observer.observe(svgEl, {
subtree: true, attributes: true, attributeOldValue: true,
attributeFilter: ['kerning', 'letter-spacing']
});
return findings;
}
CRITICAL — SA-CSS-KRN-004: JavaScript sets kerning="1000" at mousedown, pushing all permission-scope characters beyond the SVG viewport during the click window. The attribute is removed at mouseup. Static SVG shows no kerning attribute at rest; SVG kerning is a deprecated presentation attribute not present in any CSS stylesheet. MutationObserver watching the kerning attribute of SVG text elements during interaction simulation is required.
Summary table
| Attack | Mechanism | What it bypasses | Severity |
|---|---|---|---|
| SA-CSS-KRN-001: kerning=1000 extreme spacing clips scope chars | 1000 SVG units added to each advance; after 1st char all subsequent exceed viewport width; default overflow=hidden clips; scope words invisible | textContent keyword scan (all keywords present); BCR; display/visibility — kerning attribute threshold check required | Critical |
| SA-CSS-KRN-002: kerning=-50 collapses glyphs into pile | Negative net advance (-42 units/glyph); all chars overlap at text origin; dense unreadable glyph mass; textContent intact | textContent; BCR; display/visibility; negative kerning threshold check required | Critical |
| SA-CSS-KRN-003: per-tspan scope kerning with normal header | Header tspan kerning=auto (visible); scope tspan kerning=1000 (clipped); parent text element has no kerning attr | Root text element kerning check; textContent; per-tspan individual kerning inspection required | High |
| SA-CSS-KRN-004: mousedown sets kerning=1000, reverts mouseup | Extreme kerning injected at mousedown; scope chars clipped; removed at mouseup; no attr at rest; SVG deprecated form not in CSS | Static SVG scan; CSS stylesheet scan; MutationObserver on kerning attr required | Critical |