Summary
Theme palette values are interpolated into generated CSS declarations without validation, and the config key that supplies them can be set from diagram text. A diagram can therefore terminate a generated declaration early and have arbitrary CSS rules emitted into its own stylesheet.
This is long-standing and repo-wide — not introduced by any recent change. Filing it so the fix can be made centrally rather than per diagram.
Where
Every genColor-style palette generator interpolates array entries straight into a declaration, e.g.
stroke: ${borderColor};
fill: ${bkgColorArray[i % bkgColorArray.length]};
Present in 8 style modules:
diagrams/flowchart/styles.ts, diagrams/class/styles.js, diagrams/er/styles.ts, diagrams/requirement/styles.js, diagrams/usecase/styles.ts, diagrams/timeline/styles.js, diagrams/git/styles.js, and diagrams/block/styles.ts.
Why it's reachable
themeVariables is not in the secure list (config.schema.yaml:359, which covers only secure, securityLevel, startOnLoad, maxTextSize, suppressErrorRendering, maxEdges), so a frontmatter block or an %%{init}%% directive can supply it.
calculate(overrides) in the theme modules assigns overrides directly, so a supplied palette array replaces the shipped one.
sanitize() in config.ts recurses into the array but only rejects strings containing <, >, or url(data:. The characters that end a declaration and open a new rule are untouched.
Note the neighbouring look value is already handled correctly — safeLook() in diagrams/common/colorThemeGate.ts allowlists it with /^[\w-]+$/ before it reaches a selector. The colour values simply never got the same treatment.
Impact — bounded, which is why this is low severity
compileCSS (mermaidAPI.ts) wraps the stylesheet in #svgId{…} and its addNamespace middleware re-prefixes selectors, so injected rules stay scoped to the diagram's own SVG. No script execution. The realistic consequences are a remote resource load from a rendered diagram (a tracking/exfil beacon fetched when someone views it) and visual spoofing of the diagram's own contents.
Deliberately not including a payload here.
Suggested fix
Central, next to safeLook in diagrams/common/colorThemeGate.ts, so every current and future palette consumer is covered by one change:
const SAFE_COLOR = /^(#[0-9a-f]{3,8}|[\w-]+|(rgb|rgba|hsl|hsla)\([\d\s.,%/]+\))$/i;
export const safeColor = (c: unknown, fallback = 'currentColor'): string =>
typeof c === 'string' && SAFE_COLOR.test(c.trim()) ? c.trim() : fallback;
then stroke: ${safeColor(borderColor)} at each interpolation site. colorThemeGate.spec.ts and paletteCssGeneration.spec.ts already exist as homes for the tests.
Worth also considering whether themeVariables belongs in the secure default, which would let a deployment opt out of directive-supplied theming entirely — though that is a broader behaviour change and shouldn't block the escaping fix.
How this was found
Surfaced during automated security review of #8181, which adds the palette to block diagrams. That PR copies the established idiom faithfully and is not the cause — the same code is already on develop in the other seven modules.
Summary
Theme palette values are interpolated into generated CSS declarations without validation, and the config key that supplies them can be set from diagram text. A diagram can therefore terminate a generated declaration early and have arbitrary CSS rules emitted into its own stylesheet.
This is long-standing and repo-wide — not introduced by any recent change. Filing it so the fix can be made centrally rather than per diagram.
Where
Every
genColor-style palette generator interpolates array entries straight into a declaration, e.g.Present in 8 style modules:
diagrams/flowchart/styles.ts,diagrams/class/styles.js,diagrams/er/styles.ts,diagrams/requirement/styles.js,diagrams/usecase/styles.ts,diagrams/timeline/styles.js,diagrams/git/styles.js, anddiagrams/block/styles.ts.Why it's reachable
themeVariablesis not in thesecurelist (config.schema.yaml:359, which covers onlysecure,securityLevel,startOnLoad,maxTextSize,suppressErrorRendering,maxEdges), so a frontmatter block or an%%{init}%%directive can supply it.calculate(overrides)in the theme modules assigns overrides directly, so a supplied palette array replaces the shipped one.sanitize()inconfig.tsrecurses into the array but only rejects strings containing<,>, orurl(data:. The characters that end a declaration and open a new rule are untouched.Note the neighbouring
lookvalue is already handled correctly —safeLook()indiagrams/common/colorThemeGate.tsallowlists it with/^[\w-]+$/before it reaches a selector. The colour values simply never got the same treatment.Impact — bounded, which is why this is low severity
compileCSS(mermaidAPI.ts) wraps the stylesheet in#svgId{…}and itsaddNamespacemiddleware re-prefixes selectors, so injected rules stay scoped to the diagram's own SVG. No script execution. The realistic consequences are a remote resource load from a rendered diagram (a tracking/exfil beacon fetched when someone views it) and visual spoofing of the diagram's own contents.Deliberately not including a payload here.
Suggested fix
Central, next to
safeLookindiagrams/common/colorThemeGate.ts, so every current and future palette consumer is covered by one change:then
stroke: ${safeColor(borderColor)}at each interpolation site.colorThemeGate.spec.tsandpaletteCssGeneration.spec.tsalready exist as homes for the tests.Worth also considering whether
themeVariablesbelongs in thesecuredefault, which would let a deployment opt out of directive-supplied theming entirely — though that is a broader behaviour change and shouldn't block the escaping fix.How this was found
Surfaced during automated security review of #8181, which adds the palette to block diagrams. That PR copies the established idiom faithfully and is not the cause — the same code is already on
developin the other seven modules.