Compare commits

..

1 Commits

Author SHA1 Message Date
Sidharth Vinod
6359ab504f feat: Add support for nested secure keys.
Now we can use nested keys with dot separators like `themeVariables.fontSize` inside the `secure` config.
2025-08-20 14:28:36 +05:30
10 changed files with 5679 additions and 4274 deletions

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: Ensure edge label color is applied when using classDef with edge IDs

View File

@@ -1186,17 +1186,4 @@ end
imgSnapshotTest(graph, { htmlLabels: false });
});
});
it('V2 - 17: should apply class def colour to edge label', () => {
imgSnapshotTest(
` graph LR
id1(Start) link@-- "Label" -->id2(Stop)
style id1 fill:#f9f,stroke:#333,stroke-width:4px
class id2 myClass
classDef myClass fill:#bbf,stroke:#f66,stroke-width:2px,color:white,stroke-dasharray: 5 5
class link myClass
`
);
});
});

View File

@@ -12,7 +12,7 @@
> **addDirective**(`directive`): `void`
Defined in: [packages/mermaid/src/config.ts:188](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L188)
Defined in: [packages/mermaid/src/config.ts:202](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L202)
Pushes in a directive to the configuration

View File

@@ -12,7 +12,7 @@
> **reset**(`config`): `void`
Defined in: [packages/mermaid/src/config.ts:221](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L221)
Defined in: [packages/mermaid/src/config.ts:235](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L235)
## reset

View File

@@ -10,7 +10,7 @@
# Function: sanitize()
> **sanitize**(`options`): `void`
> **sanitize**(`options`, `path`): `void`
Defined in: [packages/mermaid/src/config.ts:146](https://github.com/mermaid-js/mermaid/blob/master/packages/mermaid/src/config.ts#L146)
@@ -31,6 +31,10 @@ options in-place
The potential setConfig parameter
### path
`string`\[] = `[]`
## Returns
`void`

View File

@@ -34,6 +34,92 @@ describe('when working with site config', () => {
expect(cfg.fontSize).toBe(config_0.fontSize);
expect(cfg.securityLevel).toBe(config_0.securityLevel);
});
it('should respect nested secure keys when applying directives', () => {
const config_0: MermaidConfig = {
fontFamily: 'foo-font',
themeVariables: {
fontSize: 16,
fontFamily: 'default-font',
},
secure: [
...configApi.defaultConfig.secure!,
'themeVariables.fontSize',
'themeVariables.fontFamily',
],
};
configApi.setSiteConfig(config_0);
const directive: MermaidConfig = {
fontFamily: 'baf',
themeVariables: {
fontSize: 24, // shouldn't be changed
fontFamily: 'new-font', // shouldn't be changed
primaryColor: '#ff0000', // should be allowed
},
};
const cfg: MermaidConfig = configApi.updateCurrentConfig(config_0, [directive]);
expect(cfg.fontFamily).toEqual(directive.fontFamily);
expect(cfg.themeVariables!.fontSize).toBe(config_0.themeVariables!.fontSize);
expect(cfg.themeVariables!.fontFamily).toBe(config_0.themeVariables!.fontFamily);
expect(cfg.themeVariables!.primaryColor).toBe(directive.themeVariables!.primaryColor);
});
it('should handle deeply nested secure keys', () => {
const config_0: MermaidConfig = {
flowchart: {
nodeSpacing: 50,
rankSpacing: 50,
curve: 'basis',
htmlLabels: true,
useMaxWidth: true,
diagramPadding: 8,
},
secure: [
...configApi.defaultConfig.secure!,
'flowchart.nodeSpacing',
'flowchart.rankSpacing',
],
};
configApi.setSiteConfig(config_0);
const directive: MermaidConfig = {
flowchart: {
nodeSpacing: 100, // shouldn't be changed
rankSpacing: 100, // shouldn't be changed
curve: 'linear', // should be allowed
htmlLabels: false, // should be allowed
},
};
const cfg: MermaidConfig = configApi.updateCurrentConfig(config_0, [directive]);
expect(cfg.flowchart!.nodeSpacing).toBe(config_0.flowchart!.nodeSpacing);
expect(cfg.flowchart!.rankSpacing).toBe(config_0.flowchart!.rankSpacing);
expect(cfg.flowchart!.curve).toBe(directive.flowchart!.curve);
expect(cfg.flowchart!.htmlLabels).toBe(directive.flowchart!.htmlLabels);
expect(cfg.flowchart!.diagramPadding).toBe(config_0.flowchart!.diagramPadding);
});
it('should handle mixed top-level and nested secure keys', () => {
const config_0: MermaidConfig = {
fontFamily: 'foo-font',
themeVariables: {
fontSize: 16,
primaryColor: '#000000',
},
secure: [...configApi.defaultConfig.secure!, 'fontFamily', 'themeVariables.fontSize'],
};
configApi.setSiteConfig(config_0);
const directive: MermaidConfig = {
fontFamily: 'new-font', // shouldn't be changed
themeVariables: {
fontSize: 24, // shouldn't be changed
primaryColor: '#ff0000', // should be allowed
},
};
const cfg: MermaidConfig = configApi.updateCurrentConfig(config_0, [directive]);
expect(cfg.fontFamily).toBe(config_0.fontFamily);
expect(cfg.themeVariables!.fontSize).toBe(config_0.themeVariables!.fontSize);
expect(cfg.themeVariables!.primaryColor).toBe(directive.themeVariables!.primaryColor);
});
it('should allow setting partial options', () => {
const defaultConfig = configApi.getConfig();

View File

@@ -143,17 +143,29 @@ export const getConfig = (): MermaidConfig => {
*
* @param options - The potential setConfig parameter
*/
export const sanitize = (options: any) => {
export const sanitize = (options: any, path: string[] = []) => {
if (!options) {
return;
}
// Checking that options are not in the list of excluded options
['secure', ...(siteConfig.secure ?? [])].forEach((key) => {
if (Object.hasOwn(options, key)) {
// DO NOT attempt to print options[key] within `${}` as a malicious script
// can exploit the logger's attempt to stringify the value and execute arbitrary code
log.debug(`Denied attempt to modify a secure key ${key}`, options[key]);
delete options[key];
['secure', ...(siteConfig.secure ?? [])].forEach((secureKey) => {
const securePath = secureKey.split('.');
// Check if current path matches the secure key path
if (path.length >= securePath.length - 1) {
const targetKey = securePath[securePath.length - 1];
const pathSuffix = path.slice(-(securePath.length - 1));
const pathPrefix = securePath.slice(0, -1);
const isMatch =
securePath.length === 1 ? path.length === 0 : pathSuffix.join('.') === pathPrefix.join('.');
if (isMatch && Object.hasOwn(options, targetKey)) {
const fullPath = path.length > 0 ? `${path.join('.')}.${secureKey}` : secureKey;
log.debug(`Denied attempt to modify a secure key ${fullPath}`, options[targetKey]);
delete options[targetKey];
}
}
});
@@ -163,6 +175,7 @@ export const sanitize = (options: any) => {
delete options[key];
}
});
// Check that there no attempts of xss, there should be no tags at all in the directive
// blocking data urls as base64 urls can contain svg's with inline script tags
Object.keys(options).forEach((key) => {
@@ -174,8 +187,9 @@ export const sanitize = (options: any) => {
) {
delete options[key];
}
if (typeof options[key] === 'object') {
sanitize(options[key]);
if (typeof options[key] === 'object' && options[key] !== null) {
// Recursively sanitize nested objects with updated path
sanitize(options[key], [...path, key]);
}
});
};

View File

@@ -25,7 +25,7 @@ import {
import rough from 'roughjs';
import createLabel from './createLabel.js';
import { addEdgeMarkers } from './edgeMarker.ts';
import { isLabelStyle, styles2String } from './shapes/handDrawnShapeStyles.js';
import { isLabelStyle } from './shapes/handDrawnShapeStyles.js';
const edgeLabels = new Map();
const terminalLabels = new Map();
@@ -43,10 +43,8 @@ export const getLabelStyles = (styleArray) => {
export const insertEdgeLabel = async (elem, edge) => {
let useHtmlLabels = evaluate(getConfig().flowchart.htmlLabels);
const { labelStyles } = styles2String(edge);
edge.labelStyle = labelStyles;
const labelElement = await createText(elem, edge.label, {
style: edge.labelStyle,
style: getLabelStyles(edge.labelStyle),
useHtmlLabels,
addSvgBackground: true,
isNode: false,

View File

@@ -20,11 +20,7 @@ export const compileStyles = (node: Node) => {
// the array is the styles of node from the classes it is using
// node.cssStyles is an array of styles directly set on the node
// concat the arrays and remove duplicates such that the values from node.cssStyles are used if there are duplicates
const stylesMap = styles2Map([
...(node.cssCompiledStyles || []),
...(node.cssStyles || []),
...(node.labelStyle || []),
]);
const stylesMap = styles2Map([...(node.cssCompiledStyles || []), ...(node.cssStyles || [])]);
return { stylesMap, stylesArray: [...stylesMap] };
};

9795
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff