diff --git a/.changeset/brave-baths-behave.md b/.changeset/brave-baths-behave.md new file mode 100644 index 000000000..b688a1faf --- /dev/null +++ b/.changeset/brave-baths-behave.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +fix: Prevent HTML tags from being escaped in sandbox label rendering diff --git a/.changeset/ten-plums-bet.md b/.changeset/ten-plums-bet.md new file mode 100644 index 000000000..f00a41090 --- /dev/null +++ b/.changeset/ten-plums-bet.md @@ -0,0 +1,5 @@ +--- +'mermaid': patch +--- + +fix: Support ComponentQueue_Ext to prevent parsing error diff --git a/cypress/helpers/util.ts b/cypress/helpers/util.ts index 0332178f6..51268c2a9 100644 --- a/cypress/helpers/util.ts +++ b/cypress/helpers/util.ts @@ -98,12 +98,21 @@ export const openURLAndVerifyRendering = ( cy.visit(url); cy.window().should('have.property', 'rendered', true); - cy.get('svg').should('be.visible'); - // cspell:ignore viewbox - cy.get('svg').should('not.have.attr', 'viewbox'); - if (validation) { - cy.get('svg').should(validation); + // Handle sandbox mode where SVG is inside an iframe + if (options.securityLevel === 'sandbox') { + cy.get('iframe').should('be.visible'); + if (validation) { + cy.get('iframe').should(validation); + } + } else { + cy.get('svg').should('be.visible'); + // cspell:ignore viewbox + cy.get('svg').should('not.have.attr', 'viewbox'); + + if (validation) { + cy.get('svg').should(validation); + } } if (screenshot) { diff --git a/cypress/integration/rendering/c4.spec.js b/cypress/integration/rendering/c4.spec.js index 00e71adec..92b834d41 100644 --- a/cypress/integration/rendering/c4.spec.js +++ b/cypress/integration/rendering/c4.spec.js @@ -114,4 +114,28 @@ describe('C4 diagram', () => { {} ); }); + it('C4.6 should render C4Context diagram with ComponentQueue_Ext', () => { + imgSnapshotTest( + ` + C4Context + title System Context diagram with ComponentQueue_Ext + + Enterprise_Boundary(b0, "BankBoundary0") { + Person(customerA, "Banking Customer A", "A customer of the bank, with personal bank accounts.") + + System(SystemAA, "Internet Banking System", "Allows customers to view information about their bank accounts, and make payments.") + + Enterprise_Boundary(b1, "BankBoundary") { + ComponentQueue_Ext(msgQueue, "Message Queue", "RabbitMQ", "External message queue system for processing banking transactions") + System_Ext(SystemC, "E-mail system", "The internal Microsoft Exchange e-mail system.") + } + } + + BiRel(customerA, SystemAA, "Uses") + Rel(SystemAA, msgQueue, "Sends messages to") + Rel(SystemAA, SystemC, "Sends e-mails", "SMTP") + `, + {} + ); + }); }); diff --git a/cypress/integration/rendering/flowchart-v2.spec.js b/cypress/integration/rendering/flowchart-v2.spec.js index 5ef32c269..cd3676fbf 100644 --- a/cypress/integration/rendering/flowchart-v2.spec.js +++ b/cypress/integration/rendering/flowchart-v2.spec.js @@ -79,6 +79,18 @@ describe('Flowchart v2', () => { { htmlLabels: true, flowchart: { htmlLabels: true }, securityLevel: 'loose' } ); }); + it('6a: should render complex HTML in labels with sandbox security', () => { + imgSnapshotTest( + `flowchart TD + A[Christmas] -->|Get money| B(Go shopping) + B --> C{Let me think} + C -->|One| D[Laptop] + C -->|Two| E[iPhone] + C -->|Three| F[fa:fa-car Car] + `, + { securityLevel: 'sandbox', flowchart: { htmlLabels: true } } + ); + }); it('7: should render a flowchart when useMaxWidth is true (default)', () => { renderGraph( `flowchart TD diff --git a/packages/mermaid/src/diagrams/c4/parser/c4Component.spec.js b/packages/mermaid/src/diagrams/c4/parser/c4Component.spec.js new file mode 100644 index 000000000..70d93d8be --- /dev/null +++ b/packages/mermaid/src/diagrams/c4/parser/c4Component.spec.js @@ -0,0 +1,58 @@ +import c4Db from '../c4Db.js'; +import c4 from './c4Diagram.jison'; +import { setConfig } from '../../../config.js'; + +setConfig({ + securityLevel: 'strict', +}); + +describe.each([ + ['Component', 'component'], + ['ComponentDb', 'component_db'], + ['ComponentQueue', 'component_queue'], + ['Component_Ext', 'external_component'], + ['ComponentDb_Ext', 'external_component_db'], + ['ComponentQueue_Ext', 'external_component_queue'], +])('parsing a C4 %s', function (macroName, elementName) { + beforeEach(function () { + c4.parser.yy = c4Db; + c4.parser.yy.clear(); + }); + + it('should parse a C4 diagram with one Component correctly', function () { + c4.parser.parse(`C4Component +title Component diagram for Internet Banking Component +${macroName}(ComponentAA, "Internet Banking Component", "Technology", "Allows customers to view information about their bank accounts, and make payments.")`); + + const yy = c4.parser.yy; + + const shapes = yy.getC4ShapeArray(); + expect(shapes.length).toBe(1); + const onlyShape = shapes[0]; + + expect(onlyShape).toMatchObject({ + alias: 'ComponentAA', + descr: { + text: 'Allows customers to view information about their bank accounts, and make payments.', + }, + label: { + text: 'Internet Banking Component', + }, + techn: { + text: 'Technology', + }, + typeC4Shape: { + text: elementName, + }, + }); + }); + + it('should handle a trailing whitespaces after Component', function () { + const whitespace = ' '; + const rendered = c4.parser.parse(`C4Component${whitespace} +title Component diagram for Internet Banking Component${whitespace} +${macroName}(ComponentAA, "Internet Banking Component", "Technology", "Allows customers to view information about their bank accounts, and make payments.")${whitespace}`); + + expect(rendered).toBe(true); + }); +}); diff --git a/packages/mermaid/src/diagrams/c4/parser/c4Diagram.jison b/packages/mermaid/src/diagrams/c4/parser/c4Diagram.jison index 63856f044..f0ce80d33 100644 --- a/packages/mermaid/src/diagrams/c4/parser/c4Diagram.jison +++ b/packages/mermaid/src/diagrams/c4/parser/c4Diagram.jison @@ -158,10 +158,10 @@ accDescr\s*"{"\s* { this.begin("acc_descr_multiline");} "UpdateRelStyle" { this.begin("update_rel_style"); return 'UPDATE_REL_STYLE';} "UpdateLayoutConfig" { this.begin("update_layout_config"); return 'UPDATE_LAYOUT_CONFIG';} -<> return "EOF_IN_STRUCT"; -[(][ ]*[,] { this.begin("attribute"); return "ATTRIBUTE_EMPTY";} -[(] { this.begin("attribute"); } -[)] { this.popState();this.popState();} +<> return "EOF_IN_STRUCT"; +[(][ ]*[,] { this.begin("attribute"); return "ATTRIBUTE_EMPTY";} +[(] { this.begin("attribute"); } +[)] { this.popState();this.popState();} ",," { return 'ATTRIBUTE_EMPTY';} "," { } diff --git a/packages/mermaid/src/diagrams/common/common.spec.ts b/packages/mermaid/src/diagrams/common/common.spec.ts index 3c7e0fdb8..edaf0b6dd 100644 --- a/packages/mermaid/src/diagrams/common/common.spec.ts +++ b/packages/mermaid/src/diagrams/common/common.spec.ts @@ -70,6 +70,31 @@ describe('Sanitize text', () => { }); expect(result).not.toContain('javascript:alert(1)'); }); + + it('should allow HTML tags in sandbox mode', () => { + const htmlStr = '

This is a bold text

'; + const result = sanitizeText(htmlStr, { + securityLevel: 'sandbox', + flowchart: { htmlLabels: true }, + }); + expect(result).toContain('

'); + expect(result).toContain(''); + expect(result).toContain(''); + expect(result).toContain('

'); + }); + + it('should remove script tags in sandbox mode', () => { + const maliciousStr = '

Hello world

'; + const result = sanitizeText(maliciousStr, { + securityLevel: 'sandbox', + flowchart: { htmlLabels: true }, + }); + expect(result).not.toContain('