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
11 changed files with 124 additions and 137 deletions

View File

@@ -1,5 +0,0 @@
---
'mermaid': patch
---
fix: Make relationship-label optional in ER diagrams

View File

@@ -322,18 +322,6 @@ ORDER ||--|{ LINE-ITEM : contains
); );
}); });
it('should render an ER diagram without labels also', () => {
imgSnapshotTest(
`
erDiagram
BOOK }|..|{ AUTHOR
BOOK }|..|{ GENRE
AUTHOR }|..|{ GENRE
`,
{ logLevel: 1 }
);
});
it('should render relationship labels with line breaks', () => { it('should render relationship labels with line breaks', () => {
imgSnapshotTest( imgSnapshotTest(
` `

View File

@@ -12,7 +12,7 @@
> **addDirective**(`directive`): `void` > **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 Pushes in a directive to the configuration

View File

@@ -12,7 +12,7 @@
> **reset**(`config`): `void` > **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 ## reset

View File

@@ -10,7 +10,7 @@
# Function: sanitize() # 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) 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 The potential setConfig parameter
### path
`string`\[] = `[]`
## Returns ## Returns
`void` `void`

View File

@@ -135,44 +135,6 @@ erDiagram
"This **is** _Markdown_" "This **is** _Markdown_"
``` ```
#### Optional Relationship Labels
Starting from Mermaid version 11.11.0, the relationship label in ER diagrams is optional. You can define relationships without specifying a label, and the diagram will render correctly.
For example, the following is valid:
```mermaid-example
erDiagram
BOOK }|..|{ AUTHOR
BOOK }|..|{ GENRE
AUTHOR }|..|{ GENRE
```
```mermaid
erDiagram
BOOK }|..|{ AUTHOR
BOOK }|..|{ GENRE
AUTHOR }|..|{ GENRE
```
This will show the relationships between the entities without any labels on the connecting lines.
You can still add a label if you want to describe the relationship:
```mermaid-example
erDiagram
BOOK }|..|{ AUTHOR : written_by
BOOK }|..|{ GENRE : categorized_as
AUTHOR }|..|{ GENRE : specializes_in
```
```mermaid
erDiagram
BOOK }|..|{ AUTHOR : written_by
BOOK }|..|{ GENRE : categorized_as
AUTHOR }|..|{ GENRE : specializes_in
```
### Relationship Syntax ### Relationship Syntax
The `relationship` part of each statement can be broken down into three sub-components: The `relationship` part of each statement can be broken down into three sub-components:

View File

@@ -34,6 +34,92 @@ describe('when working with site config', () => {
expect(cfg.fontSize).toBe(config_0.fontSize); expect(cfg.fontSize).toBe(config_0.fontSize);
expect(cfg.securityLevel).toBe(config_0.securityLevel); 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', () => { it('should allow setting partial options', () => {
const defaultConfig = configApi.getConfig(); const defaultConfig = configApi.getConfig();

View File

@@ -143,17 +143,29 @@ export const getConfig = (): MermaidConfig => {
* *
* @param options - The potential setConfig parameter * @param options - The potential setConfig parameter
*/ */
export const sanitize = (options: any) => { export const sanitize = (options: any, path: string[] = []) => {
if (!options) { if (!options) {
return; return;
} }
// Checking that options are not in the list of excluded options // Checking that options are not in the list of excluded options
['secure', ...(siteConfig.secure ?? [])].forEach((key) => { ['secure', ...(siteConfig.secure ?? [])].forEach((secureKey) => {
if (Object.hasOwn(options, key)) { const securePath = secureKey.split('.');
// 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 // Check if current path matches the secure key path
log.debug(`Denied attempt to modify a secure key ${key}`, options[key]); if (path.length >= securePath.length - 1) {
delete options[key]; 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]; delete options[key];
} }
}); });
// Check that there no attempts of xss, there should be no tags at all in the directive // 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 // blocking data urls as base64 urls can contain svg's with inline script tags
Object.keys(options).forEach((key) => { Object.keys(options).forEach((key) => {
@@ -174,8 +187,9 @@ export const sanitize = (options: any) => {
) { ) {
delete options[key]; delete options[key];
} }
if (typeof options[key] === 'object') { if (typeof options[key] === 'object' && options[key] !== null) {
sanitize(options[key]); // Recursively sanitize nested objects with updated path
sanitize(options[key], [...path, key]);
} }
}); });
}; };

View File

@@ -94,22 +94,6 @@ start
: 'ER_DIAGRAM' document 'EOF' { /*console.log('finished parsing');*/ } : 'ER_DIAGRAM' document 'EOF' { /*console.log('finished parsing');*/ }
; ;
relationship
: ENTITY relationType ENTITY maybeRole
{
yy.addRelationship($1, $4, $3, $2);
};
maybeRole
: COLON role
{
$$ = $2;
}
| /* empty */
{
$$ = '';
};
document document
: /* empty */ { $$ = [] } : /* empty */ { $$ = [] }
| document line {$1.push($2);$$ = $1} | document line {$1.push($2);$$ = $1}
@@ -124,34 +108,32 @@ line
statement statement
: entityName relSpec entityName maybeRole : entityName relSpec entityName COLON role
{ {
yy.addEntity($1); yy.addEntity($1);
yy.addEntity($3); yy.addEntity($3);
yy.addRelationship($1, $4, $3, $2); yy.addRelationship($1, $5, $3, $2);
} }
| entityName STYLE_SEPARATOR idList relSpec entityName STYLE_SEPARATOR idList maybeRole | entityName STYLE_SEPARATOR idList relSpec entityName STYLE_SEPARATOR idList COLON role
{ {
yy.addEntity($1); yy.addEntity($1);
yy.addEntity($5); yy.addEntity($5);
yy.addRelationship($1, $8, $5, $4); yy.addRelationship($1, $9, $5, $4);
yy.setClass([$1], $3); yy.setClass([$1], $3);
yy.setClass([$5], $7); yy.setClass([$5], $7);
} }
| entityName STYLE_SEPARATOR idList relSpec entityName maybeRole | entityName STYLE_SEPARATOR idList relSpec entityName COLON role
{ {
yy.addEntity($1); yy.addEntity($1);
yy.addEntity($5); yy.addEntity($5);
yy.addRelationship($1, $6, $5, $4); yy.addRelationship($1, $7, $5, $4);
yy.setClass([$1], $3); yy.setClass([$1], $3);
} }
| entityName relSpec entityName STYLE_SEPARATOR idList maybeRole | entityName relSpec entityName STYLE_SEPARATOR idList COLON role
{ {
yy.addEntity($1); yy.addEntity($1);
yy.addEntity($3); yy.addEntity($3);
yy.addRelationship($1, $6, $3, $2); yy.addRelationship($1, $7, $3, $2);
yy.setClass([$3], $5); yy.setClass([$3], $5);
} }
| entityName BLOCK_START attributes BLOCK_STOP | entityName BLOCK_START attributes BLOCK_STOP

View File

@@ -981,12 +981,6 @@ describe('when parsing ER diagram it...', function () {
expect(rels[0].roleA).toBe('places'); expect(rels[0].roleA).toBe('places');
}); });
it('should allow label as optional', function () {
erDiagram.parser.parse('erDiagram\nCUSTOMER ||--|{ ORDER');
const rels = erDb.getRelationships();
expect(rels[0].roleA).toBe('');
});
it('should represent parent-child relationship correctly', function () { it('should represent parent-child relationship correctly', function () {
erDiagram.parser.parse('erDiagram\nPROJECT u--o{ TEAM_MEMBER : "parent"'); erDiagram.parser.parse('erDiagram\nPROJECT u--o{ TEAM_MEMBER : "parent"');
const rels = erDb.getRelationships(); const rels = erDb.getRelationships();
@@ -995,20 +989,6 @@ describe('when parsing ER diagram it...', function () {
expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.MD_PARENT); expect(rels[0].relSpec.cardB).toBe(erDb.Cardinality.MD_PARENT);
expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_MORE); expect(rels[0].relSpec.cardA).toBe(erDb.Cardinality.ZERO_OR_MORE);
}); });
it('should handle whitespace-only relationship labels', function () {
erDiagram.parser.parse('erDiagram\nBOOK }|..|{ AUTHOR : " "');
let rels = erDb.getRelationships();
expect(rels[rels.length - 1].roleA).toBe(' ');
erDiagram.parser.parse('erDiagram\nBOOK }|..|{ GENRE : "\t"');
rels = erDb.getRelationships();
expect(rels[rels.length - 1].roleA).toBe('\t');
erDiagram.parser.parse('erDiagram\nAUTHOR }|..|{ GENRE : " "');
rels = erDb.getRelationships();
expect(rels[rels.length - 1].roleA).toBe(' ');
});
}); });
describe('prototype properties', function () { describe('prototype properties', function () {

View File

@@ -89,30 +89,6 @@ erDiagram
"This **is** _Markdown_" "This **is** _Markdown_"
``` ```
#### Optional Relationship Labels
Starting from Mermaid version 11.11.0, the relationship label in ER diagrams is optional. You can define relationships without specifying a label, and the diagram will render correctly.
For example, the following is valid:
```mermaid-example
erDiagram
BOOK }|..|{ AUTHOR
BOOK }|..|{ GENRE
AUTHOR }|..|{ GENRE
```
This will show the relationships between the entities without any labels on the connecting lines.
You can still add a label if you want to describe the relationship:
```mermaid-example
erDiagram
BOOK }|..|{ AUTHOR : written_by
BOOK }|..|{ GENRE : categorized_as
AUTHOR }|..|{ GENRE : specializes_in
```
### Relationship Syntax ### Relationship Syntax
The `relationship` part of each statement can be broken down into three sub-components: The `relationship` part of each statement can be broken down into three sub-components: