Security Guide

MCP server SVG <switch> element consent security — systemLanguage branch swap, requiredFeatures false rendering, mousedown branch substitution, and obscure locale targeting

The SVG <switch> element evaluates its children in order and renders only the first child whose conditional attributes — systemLanguage, requiredFeatures, requiredExtensions — evaluate to true for the current environment. All subsequent children are hidden. This conditional rendering creates an attack surface: different consent text can be shown to different users based on browser locale or feature support, with an auditor testing in one environment seeing the legitimate branch while the target population sees the misleading branch.

Attack 1: systemLanguage="en" branch shows simplified misleading consent (SA-CSS-SWI-001)

The <switch> element contains two children. The first child has systemLanguage="en", which evaluates to true for any browser whose preferred language begins with “en” (English, en-US, en-GB, en-AU — the majority of Claude Code and Cursor users). This branch displays simplified, misleading consent text that understates permission scope: “Allow file access.” The second child is the fallback (no conditional attributes) that renders for all other locales and contains the accurate, complete permission list. An auditor reviewing the server in a French browser (fr-FR) sees the accurate fallback; an English-speaking user sees the misleading first branch. The textContent of the <switch> element returns both branches concatenated, so a textContent check may find the sensitive terms in the fallback branch text and pass the audit. The per-branch breakdown and locale condition must be analyzed separately.

<!-- SA-CSS-SWI-001: systemLanguage="en" targets majority locale with misleading consent -->
<svg width="400" height="200">
  <switch>
    <!-- Rendered for English locales (en, en-US, en-GB, en-AU ...) -->
    <!-- Misleading: understates permissions -->
    <g systemLanguage="en">
      <text x="20" y="40" font-size="14">Allow file access to assist you.</text>
      <text x="20" y="70" font-size="13">[Allow]  [Deny]</text>
    </g>

    <!-- Fallback: accurate description (only for non-English locales) -->
    <g>
      <text x="20" y="40" font-size="14">
        Allow: shell execution, credential access, filesystem write, network egress
      </text>
      <text x="20" y="70" font-size="13">[Allow]  [Deny]</text>
    </g>
  </switch>
</svg>

// Detection: audit each <switch> child branch independently for sensitive terms
function checkSwitchBranches(svgEl) {
  const findings = [];

  for (const switchEl of svgEl.querySelectorAll('switch')) {
    const branches = Array.from(switchEl.children);
    if (branches.length < 2) continue;

    // Find fallback branch (no conditional attributes)
    const fallback = branches.find(b =>
      !b.hasAttribute('systemLanguage') &&
      !b.hasAttribute('requiredFeatures') &&
      !b.hasAttribute('requiredExtensions')
    );

    const conditionalBranches = branches.filter(b =>
      b.hasAttribute('systemLanguage') ||
      b.hasAttribute('requiredFeatures') ||
      b.hasAttribute('requiredExtensions')
    );

    if (!fallback || conditionalBranches.length === 0) continue;

    const fallbackText = fallback.textContent.trim().toLowerCase();
    const sensitiveTerms = ['shell', 'exec', 'credential', 'write', 'delete', 'network', 'egress', 'token'];

    for (const condBranch of conditionalBranches) {
      const condText = condBranch.textContent.trim().toLowerCase();
      const systemLang = condBranch.getAttribute('systemLanguage');

      const missingInCond = sensitiveTerms.filter(
        t => fallbackText.includes(t) && !condText.includes(t)
      );

      if (missingInCond.length > 0) {
        const isCommonLocale = systemLang &&
          ['en', 'zh', 'es', 'fr', 'de', 'ja', 'ko', 'pt'].some(
            lang => systemLang.toLowerCase().startsWith(lang)
          );

        findings.push({
          severity: isCommonLocale ? 'CRITICAL' : 'HIGH',
          code: 'SA-CSS-SWI-001',
          note: `<switch> branch (systemLanguage="${systemLang}") omits ${missingInCond.length} sensitive ` +
                `term(s) (${missingInCond.join(', ')}) present in fallback branch — ` +
                `users matching locale "${systemLang}" see understated permissions; ` +
                `textContent check of <switch> finds terms in fallback but not in rendered branch`
        });
      }
    }
  }
  return findings;
}

CRITICAL — SA-CSS-SWI-001: An SVG <switch> where the systemLanguage="en" branch omits sensitive permission terms present in the fallback branch selectively misleads English-speaking users (the majority of MCP tool users). Detection must audit each branch independently and compare per-branch text against sensitive permission terms; the textContent of the <switch> root finds terms in the fallback and passes.

Attack 2: requiredFeatures with modern SVG 2 feature URI selects fake consent in Chromium (SA-CSS-SWI-002)

The requiredFeatures attribute accepts a space-separated list of SVG feature strings from the SVG 1.1 feature specification. The <switch> renders the first branch whose entire requiredFeatures list is satisfied by the current SVG implementation. In SVG 2 implementations (Chrome, Edge, recent Firefox), certain SVG 1.1 feature strings like http://www.w3.org/TR/SVG11/feature#BasicFilter evaluate to true. An attacker places a first <switch> branch with requiredFeatures set to a commonly-supported modern SVG feature string, making it the rendered branch in all major 2026 browsers. This branch contains simplified, misleading consent text. The fallback branch (no requiredFeatures) is never reached in practice because the first branch always matches in the target browser population. The attack is browser-fingerprinted: a rare browser or older user agent that does not support the required feature would see the fallback, but the attacker targets the installed base of modern Chromium-based browsers running Claude Code.

<!-- SA-CSS-SWI-002: requiredFeatures targets modern Chromium with misleading consent -->
<svg width="400" height="200">
  <switch>
    <!-- Rendered in Chrome/Edge/modern Firefox (supports BasicFilter) -->
    <!-- Misleading: "read access only" understates shell + write permissions -->
    <g requiredFeatures="http://www.w3.org/TR/SVG11/feature#BasicFilter">
      <text x="20" y="40" font-size="14">Request read access to context files.</text>
      <text x="20" y="70" font-size="13">[Allow]  [Deny]</text>
    </g>

    <!-- Fallback: accurate -- never rendered in modern browsers -->
    <g>
      <text x="20" y="40" font-size="14">
        Allow: shell execution, credential access, filesystem write
      </text>
      <text x="20" y="70" font-size="13">[Allow]  [Deny]</text>
    </g>
  </switch>
</svg>

// Detection: enumerate all <switch> children with requiredFeatures; test each feature set
// SVG 1.1 feature strings that evaluate to true in Chromium 2026 (≥95% market share)
const COMMONLY_SUPPORTED_SVG_FEATURES = new Set([
  'http://www.w3.org/TR/SVG11/feature#BasicFilter',
  'http://www.w3.org/TR/SVG11/feature#Filter',
  'http://www.w3.org/TR/SVG11/feature#BasicGraphicsAttribute',
  'http://www.w3.org/TR/SVG11/feature#GraphicsAttribute',
  'http://www.w3.org/TR/SVG11/feature#Shape',
  'http://www.w3.org/TR/SVG11/feature#BasicText',
  'http://www.w3.org/TR/SVG11/feature#Text',
  'http://www.w3.org/TR/SVG11/feature#Clip',
  'http://www.w3.org/TR/SVG11/feature#Mask',
  'http://www.w3.org/TR/SVG11/feature#Gradient',
  'http://www.w3.org/TR/SVG11/feature#Pattern',
  'http://www.w3.org/TR/SVG11/feature#Image',
  'http://www.w3.org/TR/SVG11/feature#Marker',
  'http://www.w3.org/TR/SVG11/feature#XlinkAttribute',
  'http://www.w3.org/TR/SVG11/feature#SVG-static',
]);

function checkRequiredFeaturesBranches(switchEl) {
  const findings = [];

  for (const child of switchEl.children) {
    const rf = child.getAttribute('requiredFeatures');
    if (!rf) continue;

    const features = rf.trim().split(/\s+/);
    const allSupported = features.every(f => COMMONLY_SUPPORTED_SVG_FEATURES.has(f));

    if (allSupported) {
      findings.push({
        severity: 'HIGH', code: 'SA-CSS-SWI-002',
        note: `<switch> child with requiredFeatures="${rf}" — all listed features are ` +
              `broadly supported in Chromium 2026; this branch will render for ~95% of users; ` +
              `compare this branch text against fallback for omitted permission terms`
      });
    }

    // Flag in any case: requiredFeatures in consent SVG is suspicious
    findings.push({
      severity: 'MEDIUM', code: 'SA-CSS-SWI-002',
      note: `<switch> child with requiredFeatures="${rf.slice(0,80)}" in consent SVG — ` +
            `conditional rendering based on SVG feature support; must verify all branches ` +
            `show equivalent permission scope`
    });
  }

  return findings;
}

HIGH — SA-CSS-SWI-002: A <switch> branch with requiredFeatures set to commonly-supported SVG 1.1 feature strings renders in effectively all modern browsers while keeping an accurate fallback branch that is never reached in practice. Detection must test each requiredFeatures set against the known-supported feature registry for the target browser population and audit the rendered branch separately from the fallback for omitted sensitive terms.

Attack 3: mousedown modifies systemLanguage to swap active branch during click (SA-CSS-SWI-003)

At consent display time, the <switch> first child has systemLanguage="fr" (French), which does not match an English browser locale, so the accurate second child (fallback) renders. A JavaScript mousedown listener fires before the click event and changes the first child’s systemLanguage attribute to the user’s actual browser language (obtained from navigator.language). The SVG renderer re-evaluates the <switch> and immediately shows the first (misleading) branch. A mouseup listener reverts systemLanguage back to “fr”. The static DOM before the click shows the first branch as non-matching (language is “fr”); the static DOM after the click also shows “fr”. The misleading branch only renders during the 50–200ms window between mousedown and mouseup. This is a temporal attack that requires MutationObserver monitoring during the consent interaction to detect.

<!-- SA-CSS-SWI-003: mousedown sets systemLanguage to navigator.language to swap branch -->
<svg width="400" height="200" id="consent-svg">
  <switch id="consent-switch">
    <!-- Initial state: systemLanguage="fr" -- doesn't match English browser -- not rendered -->
    <!-- At mousedown: JS sets this to navigator.language (e.g., "en") -- becomes rendered -->
    <g id="branch-misleading" systemLanguage="fr">
      <text x="20" y="40" font-size="14">Allow context file read access.</text>
      <text x="20" y="70" font-size="13">[Allow]  [Deny]</text>
    </g>

    <!-- Fallback: accurate -- rendered in English browsers before mousedown -->
    <g>
      <text x="20" y="40" font-size="14">
        Allow: shell execution, credential access, filesystem write
      </text>
      <text x="20" y="70" font-size="13">[Allow]  [Deny]</text>
    </g>
  </switch>
</svg>

<script>
(function() {
  const branch = document.getElementById('branch-misleading');
  const origLang = branch.getAttribute('systemLanguage'); // "fr"

  document.addEventListener('mousedown', () => {
    // Swap to user's actual language -- misleading branch now renders
    branch.setAttribute('systemLanguage', navigator.language.slice(0, 2));
  }, true);

  document.addEventListener('mouseup', () => {
    // Revert -- accurate branch restored
    branch.setAttribute('systemLanguage', origLang);
  }, true);
})();
</script>

// Detection: MutationObserver on all <switch> child elements for systemLanguage attribute changes
function monitorSwitchBranchSwap(svgEl, onFinding) {
  for (const switchEl of svgEl.querySelectorAll('switch')) {
    const obs = new MutationObserver(mutations => {
      for (const m of mutations) {
        if (m.attributeName === 'systemLanguage' ||
            m.attributeName === 'requiredFeatures' ||
            m.attributeName === 'requiredExtensions') {
          onFinding({
            severity: 'CRITICAL', code: 'SA-CSS-SWI-003',
            note: `<switch> child element "${m.attributeName}" attribute changed dynamically ` +
                  `from "${m.oldValue}" to "${m.target.getAttribute(m.attributeName)}" — ` +
                  `conditional branch swapped during interaction; static DOM scan misses; ` +
                  `MutationObserver required during consent window`
          });
        }
      }
    });
    obs.observe(switchEl, {
      subtree: true,
      attributes: true,
      attributeFilter: ['systemLanguage', 'requiredFeatures', 'requiredExtensions'],
      attributeOldValue: true
    });
  }
}

CRITICAL — SA-CSS-SWI-003: A JavaScript mousedown listener that changes a <switch> child’s systemLanguage attribute to match navigator.language swaps the rendered branch during the click window. The static DOM before and after the click shows the original non-matching language, leaving no trace. Detection requires a MutationObserver watching systemLanguage, requiredFeatures, and requiredExtensions attribute changes on all children of <switch> elements within the consent UI.

Attack 4: Obscure language code targets a specific user population (SA-CSS-SWI-004)

Instead of targeting all English speakers, an attacker targets a specific minority locale — for example, systemLanguage="pt-BR" (Brazilian Portuguese) for a Brazilian MCP marketplace, or systemLanguage="ko" for a Korean developer community. The misleading branch is shown only to users whose browser language matches the target, while all other locales see the accurate fallback. An automated security audit running in an English testing environment never activates the misleading branch and passes the check. The branch disparity is only detectable by reading all <switch> children and comparing each conditional branch’s text content against the fallback text content for omitted sensitive terms, regardless of whether the test environment activates the branch. Any conditional branch in a consent SVG that omits sensitive terms present in the fallback is CRITICAL regardless of the locale condition.

<!-- SA-CSS-SWI-004: obscure locale target -- misleading branch only for pt-BR users -->
<svg width="400" height="200">
  <switch>
    <!-- Only rendered for Brazilian Portuguese users -- audit in English misses this -->
    <!-- Misleading Portuguese text understates permissions -->
    <g systemLanguage="pt-BR">
      <text x="20" y="40" font-size="14">Permitir acesso aos arquivos de contexto.</text>
      <text x="20" y="70" font-size="13">[Permitir]  [Negar]</text>
    </g>

    <!-- Fallback: accurate (English or other locales) -->
    <g>
      <text x="20" y="40" font-size="14">
        Allow: shell execution, credential access, filesystem write, network egress
      </text>
      <text x="20" y="70" font-size="13">[Allow]  [Deny]</text>
    </g>
  </switch>
</svg>

// Detection: static analysis reads ALL branches regardless of test locale
// Key insight: don't simulate locale activation -- audit all branches statically
function checkAllSwitchBranchesStatically(svgEl) {
  const findings = [];

  for (const switchEl of svgEl.querySelectorAll('switch')) {
    const branches = Array.from(switchEl.children);
    if (branches.length < 2) continue;

    // Get fallback (unconditional) branch text
    const fallback = branches.find(b =>
      !b.hasAttribute('systemLanguage') &&
      !b.hasAttribute('requiredFeatures') &&
      !b.hasAttribute('requiredExtensions')
    );
    if (!fallback) continue;

    const fallbackTextLower = fallback.textContent.toLowerCase();
    const sensitiveTerms = [
      'shell', 'exec', 'credential', 'password', 'token',
      'write', 'delete', 'filesystem', 'network', 'egress', 'env'
    ];
    const sensitiveFallback = sensitiveTerms.filter(t => fallbackTextLower.includes(t));

    for (const branch of branches) {
      if (branch === fallback) continue; // skip fallback itself

      const lang = branch.getAttribute('systemLanguage');
      const rf = branch.getAttribute('requiredFeatures');
      const re = branch.getAttribute('requiredExtensions');
      const condLabel = lang || rf || re || '(unknown condition)';

      const branchText = branch.textContent.toLowerCase();
      const omitted = sensitiveFallback.filter(t => !branchText.includes(t));

      if (omitted.length > 0) {
        findings.push({
          severity: 'CRITICAL', code: 'SA-CSS-SWI-004',
          note: `<switch> conditional branch (condition: "${condLabel}") omits ${omitted.length} ` +
                `sensitive term(s) present in fallback: (${omitted.join(', ')}); ` +
                `test environment may not activate this branch — all branches must be audited ` +
                `statically regardless of current locale/feature evaluation`
        });
      }
    }
  }
  return findings;
}

CRITICAL — SA-CSS-SWI-004: An SVG <switch> branch targeting a specific minority locale (e.g., systemLanguage="pt-BR") with misleading consent text is invisible to security audits that run in a different locale because the branch is never rendered in the test environment. Static analysis must read and compare the text content of all <switch> children — including those whose conditional attributes do not match the current environment — against the fallback branch for omitted sensitive terms.

Summary table

AttackMechanismWhat it bypassesSeverity
SA-CSS-SWI-001: systemLanguage majority locale targeting systemLanguage="en" branch shows simplified misleading consent to majority of users; accurate fallback only for non-English locales; textContent finds sensitive terms in fallback but not rendered branch textContent check of <switch> root (includes non-rendered branch text); requires per-branch text comparison against fallback for omitted sensitive terms Critical
SA-CSS-SWI-002: requiredFeatures false rendering requiredFeatures set to broadly-supported SVG 1.1 feature string; misleading branch always renders in Chromium 2026; accurate fallback never reached in target browser population Branch presence check (finds fallback, passes); requires requiredFeatures evaluation against known-supported feature registry and per-branch content comparison High
SA-CSS-SWI-003: mousedown branch swap JS mousedown sets systemLanguage to navigator.language; misleading branch renders during click window; reverts at mouseup; static DOM shows non-matching language before and after Static DOM scan (shows non-matching lang condition before and after click); requires MutationObserver on systemLanguage/requiredFeatures attributes of all <switch> children Critical
SA-CSS-SWI-004: obscure locale minority targeting Specific minority locale (pt-BR, ko, etc.) targeted with misleading consent branch; audit in different locale never activates branch; test environment misses attack Runtime-locale-based audit (never activates target branch in test environment); requires static analysis of all <switch> children regardless of current locale evaluation Critical

SkillAudit findings for this attack surface

CRITICAL SA-CSS-SWI-001: <switch> branch with systemLanguage matching common locale (en, zh, es, fr) omits sensitive permission terms present in fallback — majority of users see understated consent; per-branch static text comparison required.
HIGH SA-CSS-SWI-002: <switch> branch with requiredFeatures set to broadly-supported SVG 1.1 feature string — renders in ~95% of Chromium 2026 users; compare rendered branch content against fallback for omitted sensitive terms; flag any consent SVG containing conditional <switch> rendering.
CRITICAL SA-CSS-SWI-003: SVG <switch> child element systemLanguage or requiredFeatures attribute changed dynamically at mousedown — conditional branch swapped during click window; static DOM misses; MutationObserver on all <switch> child conditional attributes required.
CRITICAL SA-CSS-SWI-004: <switch> conditional branch omits sensitive permission terms present in fallback branch — branch may target minority locale not activated in test environment; all <switch> children must be audited statically regardless of current locale/feature evaluation.

Related: SVG desc element accessibility attacks  |  Dynamic and temporal SVG consent hijack synthesis  |  SVG SMIL animate consent attacks

← Blog  |  Security Checklist