diff --git a/cypress/integration/rendering/classDiagram.spec.js b/cypress/integration/rendering/classDiagram.spec.js
index a23430b083..cab3649df4 100644
--- a/cypress/integration/rendering/classDiagram.spec.js
+++ b/cypress/integration/rendering/classDiagram.spec.js
@@ -501,4 +501,16 @@ describe('Class diagram', () => {
B : -methods()
`);
});
+
+ it('should handle notes with anchor tag having target attribute', () => {
+ renderGraph(
+ `classDiagram
+ class test { }
+ note for test "note about mermaid
"`
+ );
+
+ cy.get('svg').then((svg) => {
+ cy.get('a').should('have.attr', 'target', '_blank').should('have.attr', 'rel', 'noopener');
+ });
+ });
});
diff --git a/packages/mermaid/src/diagrams/common/common.spec.ts b/packages/mermaid/src/diagrams/common/common.spec.ts
index 4dac5b33c1..9af2444061 100644
--- a/packages/mermaid/src/diagrams/common/common.spec.ts
+++ b/packages/mermaid/src/diagrams/common/common.spec.ts
@@ -38,6 +38,20 @@ describe('when securityLevel is antiscript, all script must be removed', () => {
compareRemoveScript(``, ``);
});
+ it('should detect unsecured target attribute, if value is _blank then generate a secured link', () => {
+ compareRemoveScript(
+ `note about mermaid`,
+ `note about mermaid`
+ );
+ });
+
+ it('should detect unsecured target attribute from links', () => {
+ compareRemoveScript(
+ `note about mermaid`,
+ `note about mermaid`
+ );
+ });
+
it('should detect iframes', () => {
compareRemoveScript(
`
diff --git a/packages/mermaid/src/diagrams/common/common.ts b/packages/mermaid/src/diagrams/common/common.ts
index e0ca2929db..caf43bc682 100644
--- a/packages/mermaid/src/diagrams/common/common.ts
+++ b/packages/mermaid/src/diagrams/common/common.ts
@@ -25,7 +25,27 @@ export const getRows = (s?: string): string[] => {
* @returns The safer text
*/
export const removeScript = (txt: string): string => {
- return DOMPurify.sanitize(txt);
+ const TEMPORARY_ATTRIBUTE = 'data-temp-href-target';
+
+ DOMPurify.addHook('beforeSanitizeAttributes', (node: Element) => {
+ if (node.tagName === 'A' && node.hasAttribute('target')) {
+ node.setAttribute(TEMPORARY_ATTRIBUTE, node.getAttribute('target') || '');
+ }
+ });
+
+ const sanitizedText = DOMPurify.sanitize(txt);
+
+ DOMPurify.addHook('afterSanitizeAttributes', (node: Element) => {
+ if (node.tagName === 'A' && node.hasAttribute(TEMPORARY_ATTRIBUTE)) {
+ node.setAttribute('target', node.getAttribute(TEMPORARY_ATTRIBUTE) || '');
+ node.removeAttribute(TEMPORARY_ATTRIBUTE);
+ if (node.getAttribute('target') === '_blank') {
+ node.setAttribute('rel', 'noopener');
+ }
+ }
+ });
+
+ return sanitizedText;
};
const sanitizeMore = (text: string, config: MermaidConfig) => {