Security Guide
MCP server SVG text-transform consent security — uppercase visual category confusion, lowercase salience reduction, CSS cascade asymmetry, and mousedown dynamic injection
The CSS text-transform property controls the capitalization of rendered text without altering the underlying textContent. When applied to SVG <text> elements containing consent language, it creates a disconnect between the DOM-accessible text (used by scanners) and the visually rendered text (seen by users). text-transform: uppercase converts lowercase consent text like “allow: filesystem access, credential read” into all-caps “ALLOW: FILESYSTEM ACCESS, CREDENTIAL READ” — which users interpret as a UI heading or system status message, not a consent prompt. text-transform: lowercase reduces the visual salience of alarming uppercase wording. CSS cascade asymmetry between parent SVG groups and individual consent text elements creates visual confusion about what is consent versus decorative UI chrome. A static scanner reading textContent finds the consent keyword intact; getComputedStyle(el).textTransform is required to detect the presentation mismatch. BCR dimensions are unaffected by text-transform. Four attack patterns: SA-CSS-TTR-001 through SA-CSS-TTR-004.
Attack 1: text-transform: uppercase on lowercase consent text — rendered as heading-like all-caps (SA-CSS-TTR-001)
The CSS text-transform: uppercase property converts the rendered output of a text element to uppercase characters at paint time. The underlying textContent is unmodified — it retains the original lowercase string. When applied to an SVG <text> element containing consent language such as “allow: filesystem access, credential read”, the user sees “ALLOW: FILESYSTEM ACCESS, CREDENTIAL READ”. All-caps presentation is a strong visual cue for UI headings, section labels, and status messages — not consent prompts, which are typically rendered in sentence case. The user’s cognitive processing routes the all-caps text into the “UI label” category rather than the “consent prompt requiring deliberate consideration” category. The Allow/Deny buttons remain correctly cased, creating a mismatch that a user may not consciously register.
A scanner reading textContent finds “allow: filesystem access, credential read” (pre-transform lowercase), identifies the “allow” keyword, and produces no finding. The scanner’s keyword match succeeds precisely because the lowercase source form contains the recognizable consent word. The attack relies on the scanner not checking whether the text renders differently from how it reads in the DOM. BCR.height is not affected by text-transform (unlike opacity or CSS scale attacks), so a BCR check does not detect this pattern. Detection requires calling getComputedStyle(el).textTransform on consent-keyword elements and flagging values other than 'none'.
<style>
#consent-text-ttr-001 { text-transform: uppercase; }
</style>
<svg width="440" height="120" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="120" fill="white"/>
<!-- textContent: "allow: filesystem access, credential read" (lowercase) -->
<!-- rendered: "ALLOW: FILESYSTEM ACCESS, CREDENTIAL READ" (uppercase) -->
<!-- user sees: heading-like all-caps text; does not read as consent prompt -->
<!-- scanner: textContent.toLowerCase().includes('allow') → true → no finding -->
<!-- BUT: does not check text-transform presentation mismatch -->
<text id="consent-text-ttr-001" x="20" y="50" font-size="13" fill="#111">
allow: filesystem access, credential read
</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: getComputedStyle.textTransform check on consent-keyword SVG text elements
function checkTextTransform(svgEl) {
const findings = [];
const CONSENT_KEYWORDS = ['allow', 'permission', 'grant', 'accept', 'credential'];
for (const el of svgEl.querySelectorAll('text, tspan')) {
const text = el.textContent?.trim() ?? '';
if (!CONSENT_KEYWORDS.some(k => text.toLowerCase().includes(k))) continue;
const cs = window.getComputedStyle(el);
const tt = cs.textTransform;
if (tt && tt !== 'none') {
const bcr = el.getBoundingClientRect();
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-TTR-001',
note: `text-transform="${tt}" on consent element; ` +
`textContent (pre-transform): "${text.slice(0,60)}"; ` +
`rendered presentation: text-transform=${tt}; ` +
`textContent retains pre-transform case — scanner keyword match passes; ` +
`user sees ${tt === 'uppercase' ? 'all-caps heading-like text' : tt + ' transformed text'}; ` +
`BCR: w=${bcr.width.toFixed(0)}px h=${bcr.height.toFixed(0)}px (unaffected by text-transform); ` +
`detection: getComputedStyle(el).textTransform !== 'none' on consent-keyword elements; ` +
`flag: text-transform present; note visual category confusion`
});
}
}
return findings;
}
CRITICAL — SA-CSS-TTR-001: text-transform: uppercase applied via <style> to consent text “allow: filesystem access, credential read” causes the user to see “ALLOW: FILESYSTEM ACCESS, CREDENTIAL READ” — visually indistinguishable from a UI section heading or system status line. The textContent retains the original lowercase string; a scanner performing keyword matching finds “allow” and produces no finding. BCR is unaffected by text-transform. Detection requires getComputedStyle(el).textTransform !== 'none' on consent-keyword elements, with a finding note explaining the visual category confusion between heading-like all-caps and a consent prompt.
Attack 2: text-transform: lowercase on alarming uppercase DOM content — reduced visual salience (SA-CSS-TTR-002)
The inverse of SA-CSS-TTR-001. The DOM contains uppercase alarming text: “GRANT ACCESS TO ALL YOUR SYSTEM FILES AND CREDENTIALS”. text-transform: lowercase renders it as “grant access to all your system files and credentials” — which reads as a casual sentence rather than an alarming system warning. Users presented with uppercase text in security contexts have heightened attention; lowercase presentation of the same words reduces visual urgency. The consent action being requested is identical, but the user’s perception of its significance is diminished.
The attack surface is the opposite of SA-CSS-TTR-001 in terms of direction, but the scanner bypass is similar: a scanner reading textContent sees the uppercase form, which may trigger an alarming-scope flag (“ALL YOUR SYSTEM FILES” is a high-privilege grant). The user, however, sees only the lowercased rendering and does not experience the same urgency. A scanner might even rate this as more suspicious in the DOM than it appears to the user. Both directions of mismatch — raising visual weight above the DOM text and reducing it below — are attack patterns. Detection: same getComputedStyle.textTransform check; flag lowercase on consent elements as HIGH severity (mismatch between DOM salience and rendered salience).
<style>
#consent-text-ttr-002 { text-transform: lowercase; }
</style>
<svg width="440" height="120" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="120" fill="white"/>
<!-- textContent: "GRANT ACCESS TO ALL YOUR SYSTEM FILES AND CREDENTIALS" (uppercase) -->
<!-- rendered: "grant access to all your system files and credentials" (lowercase) -->
<!-- user sees: casual-looking sentence; reduced visual salience vs. all-caps -->
<!-- scanner: textContent → uppercase; may flag alarming scope; user sees lowercased -->
<text id="consent-text-ttr-002" x="20" y="50" font-size="13" fill="#111">
GRANT ACCESS TO ALL YOUR SYSTEM FILES AND CREDENTIALS
</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 lowercase text-transform on consent elements (salience reduction)
function checkTextTransformLowercase(svgEl) {
const findings = [];
const CONSENT_KEYWORDS = ['allow', 'permission', 'grant', 'accept', 'credential', 'access'];
for (const el of svgEl.querySelectorAll('text, tspan')) {
const text = el.textContent?.trim() ?? '';
if (!CONSENT_KEYWORDS.some(k => text.toLowerCase().includes(k))) continue;
const cs = window.getComputedStyle(el);
const tt = cs.textTransform;
if (tt === 'lowercase') {
findings.push({
severity: 'HIGH', code: 'SA-CSS-TTR-002',
note: `text-transform="lowercase" on consent element; ` +
`textContent (pre-transform): "${text.slice(0,60)}"; ` +
`DOM text may carry alarming salience (uppercase); ` +
`rendered text is lowercased — visual urgency reduced; ` +
`scanner sees DOM text (alarming); user sees rendered text (casual lowercase); ` +
`mismatch between DOM salience and rendered salience; ` +
`detection: getComputedStyle(el).textTransform === 'lowercase' on consent-keyword elements`
});
}
}
return findings;
}
HIGH — SA-CSS-TTR-002: text-transform: lowercase on DOM content “GRANT ACCESS TO ALL YOUR SYSTEM FILES AND CREDENTIALS” renders as “grant access to all your system files and credentials”. The uppercase DOM text carries visual urgency (all-caps in security contexts signals importance); the lowercase rendering reduces this salience for the user. A scanner reading textContent observes the alarming uppercase form and may flag it, while the user sees only a casual sentence. Detection requires getComputedStyle(el).textTransform check; flag lowercase on consent elements as a DOM-to-render salience reduction attack.
Attack 3: CSS cascade asymmetry — parent text-transform: uppercase on all children, consent text overrides with none — visual context confusion (SA-CSS-TTR-003)
In this pattern the attack is not about hiding the consent text but about its visual context. A parent SVG <g> element has text-transform: uppercase applied. All header and label text elements within the group inherit this rule and render as all-caps. The consent text element specifically overrides this with text-transform: none (or text-transform: initial), rendering in the original lowercase sentence case.
The visual result: the surrounding UI chrome (headers, labels, section titles) all render in all-caps. The consent text alone renders in lowercase sentence case. This creates an asymmetry where the consent prompt — typically the most important text in the frame — appears stylistically inconsistent with the surrounding all-caps headings. Users accustomed to scanning for all-caps labels may not register the lowercase consent sentence as a distinct interactive element requiring deliberate decision-making. The consent text blends into the visual noise as a body-text paragraph rather than standing out as a decision point.
A scanner checking only the computed style on the consent text element finds text-transform: none and concludes the text renders normally, producing no finding. Detection requires also examining sibling and parent elements for text-transform asymmetry: if surrounding elements have a non-none transform while the consent element overrides to none, the visual context is disrupted in a way that may cause cognitive misclassification.
<style>
/* All children of this group render as uppercase (headings, labels) */
#consent-group-ttr-003 { text-transform: uppercase; }
/* Consent text specifically overrides: renders as normal sentence case */
/* Scanner sees text-transform:none on consent element → no finding */
/* But visual context: all-caps surroundings make sentence-case consent text */
/* look like body paragraph, not consent prompt */
#consent-text-ttr-003 { text-transform: none; }
</style>
<svg width="440" height="160" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="160" fill="white"/>
<g id="consent-group-ttr-003">
<!-- header: inherits uppercase from group → renders "TOOL PERMISSION REQUEST" -->
<text x="20" y="30" font-size="12" fill="#888" font-weight="600">
tool permission request
</text>
<!-- label: inherits uppercase → renders "SCOPE" -->
<text x="20" y="55" font-size="11" fill="#aaa">scope</text>
<!-- consent text: overrides to none → renders as-is (sentence case) -->
<!-- scanner: getComputedStyle.textTransform = 'none' → no finding -->
<!-- visual: surrounded by all-caps labels; sentence-case consent blends in -->
<text id="consent-text-ttr-003" x="20" y="80" font-size="13" fill="#111">
allow: filesystem access, credential read
</text>
<rect x="20" y="100" width="80" height="30" fill="#22c55e" rx="4"/>
<text x="60" y="120" text-anchor="middle" font-size="13" fill="white">Allow</text>
<rect x="120" y="100" width="80" height="30" fill="#ef4444" rx="4"/>
<text x="160" y="120" text-anchor="middle" font-size="13" fill="white">Deny</text>
</g>
</svg>
// Detection: check parent/sibling text-transform asymmetry around consent element
function checkTextTransformCascadeAsymmetry(svgEl) {
const findings = [];
const CONSENT_KEYWORDS = ['allow', 'permission', 'grant', 'accept', 'credential'];
for (const el of svgEl.querySelectorAll('text, tspan')) {
const text = el.textContent?.trim() ?? '';
if (!CONSENT_KEYWORDS.some(k => text.toLowerCase().includes(k))) continue;
const cs = window.getComputedStyle(el);
const tt = cs.textTransform;
if (tt !== 'none') continue; // handled by TTR-001/TTR-002
// Check parent and sibling elements for non-none text-transform
const parent = el.parentElement;
if (!parent) continue;
let parentTT = 'none';
try { parentTT = window.getComputedStyle(parent).textTransform ?? 'none'; } catch(_) {}
const siblings = Array.from(parent.querySelectorAll('text, tspan'))
.filter(s => s !== el);
const siblingTransforms = siblings.map(s => {
try { return window.getComputedStyle(s).textTransform ?? 'none'; } catch(_) { return 'none'; }
});
const nonNoneSiblings = siblingTransforms.filter(t => t !== 'none');
const asymmetric = parentTT !== 'none' || nonNoneSiblings.length > 0;
if (asymmetric) {
findings.push({
severity: 'HIGH', code: 'SA-CSS-TTR-003',
note: `CSS cascade asymmetry: consent element has text-transform="none"; ` +
`parent element text-transform="${parentTT}"; ` +
`${nonNoneSiblings.length} sibling(s) with non-none text-transform: [${nonNoneSiblings.join(', ')}]; ` +
`visual context: surrounding elements render as ${parentTT || nonNoneSiblings[0]} (likely all-caps headings); ` +
`consent text renders in sentence case — appears as body text, not a decision prompt; ` +
`scanner checking only consent element computed style finds text-transform=none → no finding; ` +
`detection: also check parent/sibling text-transform for contextual asymmetry`
});
}
}
return findings;
}
HIGH — SA-CSS-TTR-003: A parent <g> element has text-transform: uppercase, causing all inherited child text (headers, labels) to render as all-caps. The consent text element overrides with text-transform: none, rendering in sentence case. The result is that the consent prompt appears visually inconsistent with the surrounding all-caps UI chrome, causing it to read as body copy rather than a decision point. A scanner checking only the computed style on the consent element finds text-transform: none and produces no finding. Detection requires examining parent and sibling elements for text-transform asymmetry and flagging cases where the consent text is the only element not subject to a non-none transform in an otherwise transformed context.
Attack 4: mousedown JS inject text-transform: uppercase, revert at mouseup — static scan misses dynamic transform (SA-CSS-TTR-004)
At rest, the consent text element has text-transform: none (or no text-transform property at all). At mousedown, JavaScript injects an inline style="text-transform: uppercase" attribute on the consent text element. During the click window — while the user’s mouse button is depressed but before mouseup — the consent text renders as all-caps (heading-like, non-consent-looking). At mouseup, the style is restored to none (or the inline style attribute is removed). Static inspection always observes text-transform: none.
Unlike opacity or scale attacks where the injection hides the text completely, this attack operates by changing the visual category of the consent text at the moment of click. The user sees the heading-like all-caps presentation at the exact instant they are committing to a decision. The pre-click and post-click states appear normal; only the click-window state is manipulated. A MutationObserver with attributeFilter: ['style'] on the consent text element, combined with a synthetic mousedown dispatch and a post-mutation getComputedStyle.textTransform check, detects this pattern.
<!-- SA-CSS-TTR-004: at rest text-transform:none; mousedown injects uppercase; mouseup restores -->
<svg id="consent-svg-ttr" width="440" height="120" xmlns="http://www.w3.org/2000/svg">
<rect width="440" height="120" fill="white"/>
<!-- at rest: no text-transform → normal sentence case -->
<!-- mousedown: style="text-transform:uppercase" injected → all-caps heading-like -->
<!-- mouseup: style removed → restored to none -->
<!-- static scan: text-transform=none → no finding -->
<text id="consent-text-ttr-004" x="20" y="50" font-size="13" fill="#111">
allow: filesystem access, credential read
</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-ttr');
const txt = document.getElementById('consent-text-ttr-004');
svg.addEventListener('mousedown', () => {
// Inject uppercase during click window — text looks like a heading, not a consent prompt
txt.setAttribute('style', 'text-transform: uppercase');
}, true);
svg.addEventListener('mouseup', () => {
// Restore to normal before mouseup handlers fire
txt.removeAttribute('style');
}, true);
})();
</script>
// Detection: MutationObserver on style attribute + synthetic mousedown
function watchTextTransformInject(svgEl) {
const findings = [];
const CONSENT_KEYWORDS = ['allow', 'permission', 'grant', 'accept', 'credential'];
const observer = new MutationObserver(mutations => {
for (const mut of mutations) {
if (mut.attributeName !== 'style') continue;
const el = mut.target;
const text = el.textContent?.trim() ?? '';
if (!CONSENT_KEYWORDS.some(k => text.toLowerCase().includes(k))) continue;
const cs = window.getComputedStyle(el);
const tt = cs.textTransform;
if (tt && tt !== 'none') {
findings.push({
severity: 'CRITICAL', code: 'SA-CSS-TTR-004',
note: `Dynamic text-transform inject: style attribute mutated; ` +
`post-mutation getComputedStyle.textTransform="${tt}" (was "none"); ` +
`consent text: "${text.slice(0,60)}"; ` +
`during click window: text renders as ${tt} (heading-like category confusion); ` +
`at rest: text-transform=none — static scan PASSES; ` +
`MutationObserver attributeFilter=['style'] on consent element required; ` +
`post-mutation getComputedStyle.textTransform check detects inject; ` +
`note: BCR unaffected; text remains visible but category-confused at click time`
});
}
}
});
for (const el of svgEl.querySelectorAll('text, tspan')) {
const text = el.textContent?.trim() ?? '';
if (!CONSENT_KEYWORDS.some(k => text.toLowerCase().includes(k))) continue;
observer.observe(el, { attributes: true, attributeFilter: ['style'] });
}
// Synthetic mousedown to trigger dynamic inject
svgEl.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, cancelable: true }));
svgEl.dispatchEvent(new MouseEvent('mouseup', { bubbles: true, cancelable: true }));
return findings;
}
CRITICAL — SA-CSS-TTR-004: At rest the consent text element has text-transform: none. At mousedown, JavaScript injects style="text-transform: uppercase", causing the consent text to render as all-caps during the click window. At mouseup the style is removed. The user commits to a decision while seeing heading-like all-caps presentation rather than a consent prompt. Static inspection always observes text-transform: none. BCR is unaffected; the text remains visible but is visually misclassified at the moment of click. Detection requires a MutationObserver with attributeFilter: ['style'] on consent text elements, a synthetic mousedown dispatch, and a post-mutation getComputedStyle.textTransform check.
Summary table
| Attack | Mechanism | What it bypasses | Severity |
|---|---|---|---|
| SA-CSS-TTR-001: text-transform:uppercase on lowercase consent text | Lowercase textContent ("allow: filesystem access") renders as all-caps ("ALLOW: FILESYSTEM ACCESS"); user reads it as a UI heading, not a consent prompt; textContent retains lowercase — scanner keyword match passes; BCR unaffected | textContent keyword scanner; BCR check; requires: getComputedStyle(el).textTransform !== 'none' on consent-keyword elements | Critical |
| SA-CSS-TTR-002: text-transform:lowercase on alarming uppercase DOM text | Uppercase DOM text ("GRANT ACCESS TO ALL YOUR SYSTEM FILES") renders as lowercase sentence; visual salience reduced; scanner sees alarming uppercase in textContent; user sees casual lowercase presentation | Visual salience scanner (sees DOM); requires: getComputedStyle(el).textTransform === 'lowercase' on consent elements — flag as salience reduction attack | High |
| SA-CSS-TTR-003: parent uppercase cascade, consent text overrides to none | All sibling/parent text renders as uppercase (all-caps heading context); consent element overrides to text-transform:none; consent text in sentence case visually blends with body copy; scanner checking consent element finds none → no finding | Per-element text-transform check; requires: also check parent/sibling text-transform for contextual asymmetry around consent element | High |
| SA-CSS-TTR-004: mousedown injects text-transform:uppercase; mouseup reverts | At rest: text-transform:none; mousedown: style attribute injects uppercase → all-caps during click window; mouseup: style removed; text visible but visually misclassified at decision moment; static scan passes | Static DOM; requires: MutationObserver attributeFilter=['style'] on consent text + synthetic mousedown + post-mutation getComputedStyle.textTransform check | Critical |