Merge branch 'fix/200-mermaid-xss-protection' into develop
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
This commit is contained in:
237
apps/web/src/components/mindmap/MermaidViewer.test.tsx
Normal file
237
apps/web/src/components/mindmap/MermaidViewer.test.tsx
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
/**
|
||||||
|
* MermaidViewer XSS Protection Tests
|
||||||
|
* Tests defense-in-depth security layers for Mermaid diagram rendering
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||||
|
import { render, waitFor } from "@testing-library/react";
|
||||||
|
import { MermaidViewer } from "./MermaidViewer";
|
||||||
|
|
||||||
|
// Mock mermaid
|
||||||
|
vi.mock("mermaid", () => ({
|
||||||
|
default: {
|
||||||
|
initialize: vi.fn(),
|
||||||
|
render: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("MermaidViewer XSS Protection", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Script tag injection", () => {
|
||||||
|
it("should block script tags in labels", async () => {
|
||||||
|
const maliciousDiagram = `graph TD
|
||||||
|
A["<script>alert('XSS')</script>"]`;
|
||||||
|
|
||||||
|
const { container } = render(<MermaidViewer diagram={maliciousDiagram} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const content = container.innerHTML;
|
||||||
|
// Should not contain script tags
|
||||||
|
expect(content).not.toContain("<script>");
|
||||||
|
expect(content).not.toContain("alert");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should block script tags with various casings", async () => {
|
||||||
|
const maliciousDiagram = `graph TD
|
||||||
|
A["<ScRiPt>alert('XSS')</sCrIpT>"]`;
|
||||||
|
|
||||||
|
const { container } = render(<MermaidViewer diagram={maliciousDiagram} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const content = container.innerHTML.toLowerCase();
|
||||||
|
expect(content).not.toContain("<script>");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Event handler injection", () => {
|
||||||
|
it("should block onerror event handlers", async () => {
|
||||||
|
const maliciousDiagram = `graph TD
|
||||||
|
A["<img src=x onerror=alert(1)>"]`;
|
||||||
|
|
||||||
|
const { container } = render(<MermaidViewer diagram={maliciousDiagram} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const content = container.innerHTML;
|
||||||
|
expect(content).not.toContain("onerror");
|
||||||
|
expect(content).not.toContain("alert(1)");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should block onclick event handlers", async () => {
|
||||||
|
const maliciousDiagram = `graph TD
|
||||||
|
A["<div onclick=alert(1)>Click</div>"]`;
|
||||||
|
|
||||||
|
const { container } = render(<MermaidViewer diagram={maliciousDiagram} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const content = container.innerHTML;
|
||||||
|
expect(content).not.toContain("onclick");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should block onload event handlers", async () => {
|
||||||
|
const maliciousDiagram = `graph TD
|
||||||
|
A["<body onload=alert(1)>"]`;
|
||||||
|
|
||||||
|
const { container } = render(<MermaidViewer diagram={maliciousDiagram} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const content = container.innerHTML;
|
||||||
|
expect(content).not.toContain("onload");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("JavaScript URL injection", () => {
|
||||||
|
it("should block javascript: URLs in href", async () => {
|
||||||
|
const maliciousDiagram = `graph TD
|
||||||
|
A["<a href='javascript:alert(1)'>Click</a>"]`;
|
||||||
|
|
||||||
|
const { container } = render(<MermaidViewer diagram={maliciousDiagram} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const content = container.innerHTML;
|
||||||
|
expect(content).not.toContain("javascript:");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should block javascript: URLs in src", async () => {
|
||||||
|
const maliciousDiagram = `graph TD
|
||||||
|
A["<img src='javascript:alert(1)'>"]`;
|
||||||
|
|
||||||
|
const { container } = render(<MermaidViewer diagram={maliciousDiagram} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const content = container.innerHTML;
|
||||||
|
expect(content).not.toContain("javascript:");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Data URL injection", () => {
|
||||||
|
it("should block data URLs with scripts", async () => {
|
||||||
|
const maliciousDiagram = `graph TD
|
||||||
|
A["<img src='data:text/html,<script>alert(1)</script>'>"]`;
|
||||||
|
|
||||||
|
const { container } = render(<MermaidViewer diagram={maliciousDiagram} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const content = container.innerHTML;
|
||||||
|
expect(content).not.toContain("data:text/html");
|
||||||
|
expect(content).not.toContain("<script>");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should block data URLs with base64 encoded scripts", async () => {
|
||||||
|
const maliciousDiagram = `graph TD
|
||||||
|
A["<object data='data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg=='>"]`;
|
||||||
|
|
||||||
|
const { container } = render(<MermaidViewer diagram={maliciousDiagram} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const content = container.innerHTML;
|
||||||
|
expect(content).not.toContain("data:text/html");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("SVG injection", () => {
|
||||||
|
it("should block SVG with embedded scripts", async () => {
|
||||||
|
const maliciousDiagram = `graph TD
|
||||||
|
A["<svg><script>alert(1)</script></svg>"]`;
|
||||||
|
|
||||||
|
const { container } = render(<MermaidViewer diagram={maliciousDiagram} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const content = container.innerHTML;
|
||||||
|
// SVG should be sanitized to remove scripts
|
||||||
|
expect(content).not.toContain("<script>");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should block SVG with xlink:href", async () => {
|
||||||
|
const maliciousDiagram = `graph TD
|
||||||
|
A["<svg><use xlink:href='javascript:alert(1)'/></svg>"]`;
|
||||||
|
|
||||||
|
const { container } = render(<MermaidViewer diagram={maliciousDiagram} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const content = container.innerHTML;
|
||||||
|
expect(content).not.toContain("javascript:");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("HTML entity bypass", () => {
|
||||||
|
it("should block HTML entity encoded scripts", async () => {
|
||||||
|
const maliciousDiagram = `graph TD
|
||||||
|
A["<script>alert(1)</script>"]`;
|
||||||
|
|
||||||
|
const { container } = render(<MermaidViewer diagram={maliciousDiagram} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const content = container.innerHTML;
|
||||||
|
// Should not execute the decoded script
|
||||||
|
expect(content).not.toContain("alert(1)");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Safe content", () => {
|
||||||
|
it("should allow safe plain text labels", async () => {
|
||||||
|
const safeDiagram = `graph TD
|
||||||
|
A["Safe Label"]
|
||||||
|
B["Another Safe Label"]
|
||||||
|
A --> B`;
|
||||||
|
|
||||||
|
const { container } = render(<MermaidViewer diagram={safeDiagram} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
// Should render without errors
|
||||||
|
expect(container.querySelector(".mermaid-container")).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should allow safe special characters", async () => {
|
||||||
|
const safeDiagram = `graph TD
|
||||||
|
A["Label with & and # and @"]`;
|
||||||
|
|
||||||
|
const { container } = render(<MermaidViewer diagram={safeDiagram} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(container.querySelector(".mermaid-container")).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("DOMPurify integration", () => {
|
||||||
|
it("should sanitize rendered SVG output", async () => {
|
||||||
|
const diagram = `graph TD
|
||||||
|
A["Test Node"]`;
|
||||||
|
|
||||||
|
// Mock mermaid to return SVG with potential XSS
|
||||||
|
const mermaid = await import("mermaid");
|
||||||
|
vi.mocked(mermaid.default.render).mockResolvedValue({
|
||||||
|
svg: "<svg><script>alert(1)</script><text>Test</text></svg>",
|
||||||
|
bindFunctions: vi.fn(),
|
||||||
|
diagramType: "flowchart",
|
||||||
|
});
|
||||||
|
|
||||||
|
const { container } = render(<MermaidViewer diagram={diagram} />);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const content = container.innerHTML;
|
||||||
|
// DOMPurify should remove the script tag
|
||||||
|
expect(content).not.toContain("<script>");
|
||||||
|
expect(content).not.toContain("alert(1)");
|
||||||
|
// But keep the safe SVG elements
|
||||||
|
expect(content).toContain("<svg");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import mermaid from "mermaid";
|
import mermaid from "mermaid";
|
||||||
|
import DOMPurify from "dompurify";
|
||||||
|
|
||||||
interface MermaidViewerProps {
|
interface MermaidViewerProps {
|
||||||
diagram: string;
|
diagram: string;
|
||||||
@@ -48,9 +49,27 @@ export function MermaidViewer({
|
|||||||
// Render the diagram
|
// Render the diagram
|
||||||
const { svg } = await mermaid.render(id, diagram);
|
const { svg } = await mermaid.render(id, diagram);
|
||||||
|
|
||||||
|
// Sanitize SVG output with DOMPurify for defense-in-depth
|
||||||
|
// Configure DOMPurify to allow SVG elements but remove scripts and dangerous attributes
|
||||||
|
const sanitizedSvg = DOMPurify.sanitize(svg, {
|
||||||
|
USE_PROFILES: { svg: true, svgFilters: true },
|
||||||
|
ADD_TAGS: ["use"], // Allow SVG use elements
|
||||||
|
FORBID_TAGS: ["script", "iframe", "object", "embed", "base"],
|
||||||
|
FORBID_ATTR: [
|
||||||
|
"onerror",
|
||||||
|
"onload",
|
||||||
|
"onclick",
|
||||||
|
"onmouseover",
|
||||||
|
"onfocus",
|
||||||
|
"onblur",
|
||||||
|
"onchange",
|
||||||
|
"oninput",
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
const container = containerRef.current;
|
const container = containerRef.current;
|
||||||
if (container) {
|
if (container) {
|
||||||
container.innerHTML = svg;
|
container.innerHTML = sanitizedSvg;
|
||||||
|
|
||||||
// Add click handlers to nodes if callback provided
|
// Add click handlers to nodes if callback provided
|
||||||
if (onNodeClick) {
|
if (onNodeClick) {
|
||||||
|
|||||||
@@ -121,6 +121,35 @@ interface UseGraphDataResult {
|
|||||||
|
|
||||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000";
|
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:8000";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize labels for Mermaid diagrams to prevent XSS
|
||||||
|
* Removes HTML tags, dangerous protocols, and special characters
|
||||||
|
*/
|
||||||
|
function sanitizeMermaidLabel(label: string): string {
|
||||||
|
if (!label) return "";
|
||||||
|
|
||||||
|
// Remove HTML tags
|
||||||
|
let sanitized = label.replace(/<[^>]*>/g, "");
|
||||||
|
|
||||||
|
// Remove dangerous protocols
|
||||||
|
sanitized = sanitized.replace(/javascript:/gi, "");
|
||||||
|
sanitized = sanitized.replace(/data:/gi, "");
|
||||||
|
sanitized = sanitized.replace(/vbscript:/gi, "");
|
||||||
|
|
||||||
|
// Remove control characters
|
||||||
|
// eslint-disable-next-line no-control-regex
|
||||||
|
sanitized = sanitized.replace(/[\x00-\x1F\x7F]/g, "");
|
||||||
|
|
||||||
|
// Escape Mermaid special characters that could break syntax
|
||||||
|
sanitized = sanitized.replace(/["\n\r]/g, " ");
|
||||||
|
sanitized = sanitized.replace(/[[\](){}]/g, "");
|
||||||
|
|
||||||
|
// Truncate to prevent DoS
|
||||||
|
sanitized = sanitized.slice(0, 200);
|
||||||
|
|
||||||
|
return sanitized.trim();
|
||||||
|
}
|
||||||
|
|
||||||
async function apiFetch<T>(
|
async function apiFetch<T>(
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
accessToken: string | null,
|
accessToken: string | null,
|
||||||
@@ -311,35 +340,35 @@ export function useGraphData(options: UseGraphDataOptions = {}): UseGraphDataRes
|
|||||||
nodesByType[nodeType].push(node);
|
nodesByType[nodeType].push(node);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add nodes by type
|
// Add nodes by type with sanitized labels
|
||||||
Object.entries(nodesByType).forEach(([type, nodes]): void => {
|
Object.entries(nodesByType).forEach(([type, nodes]): void => {
|
||||||
diagram += ` ${type}\n`;
|
diagram += ` ${sanitizeMermaidLabel(type)}\n`;
|
||||||
nodes.forEach((node): void => {
|
nodes.forEach((node): void => {
|
||||||
diagram += ` ${node.title}\n`;
|
diagram += ` ${sanitizeMermaidLabel(node.title)}\n`;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
diagram = "graph TD\n";
|
diagram = "graph TD\n";
|
||||||
|
|
||||||
// Add all edges
|
// Add all edges with sanitized labels
|
||||||
graph.edges.forEach((edge): void => {
|
graph.edges.forEach((edge): void => {
|
||||||
const source = graph.nodes.find((n): boolean => n.id === edge.source_id);
|
const source = graph.nodes.find((n): boolean => n.id === edge.source_id);
|
||||||
const target = graph.nodes.find((n): boolean => n.id === edge.target_id);
|
const target = graph.nodes.find((n): boolean => n.id === edge.target_id);
|
||||||
|
|
||||||
if (source && target) {
|
if (source && target) {
|
||||||
const sourceLabel = source.title.replace(/["\n]/g, " ");
|
const sourceLabel = sanitizeMermaidLabel(source.title);
|
||||||
const targetLabel = target.title.replace(/["\n]/g, " ");
|
const targetLabel = sanitizeMermaidLabel(target.title);
|
||||||
diagram += ` ${edge.source_id}["${sourceLabel}"] --> ${edge.target_id}["${targetLabel}"]\n`;
|
diagram += ` ${edge.source_id}["${sourceLabel}"] --> ${edge.target_id}["${targetLabel}"]\n`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add standalone nodes (no edges)
|
// Add standalone nodes (no edges) with sanitized labels
|
||||||
graph.nodes.forEach((node): void => {
|
graph.nodes.forEach((node): void => {
|
||||||
const hasEdge = graph.edges.some(
|
const hasEdge = graph.edges.some(
|
||||||
(e): boolean => e.source_id === node.id || e.target_id === node.id
|
(e): boolean => e.source_id === node.id || e.target_id === node.id
|
||||||
);
|
);
|
||||||
if (!hasEdge) {
|
if (!hasEdge) {
|
||||||
const label = node.title.replace(/["\n]/g, " ");
|
const label = sanitizeMermaidLabel(node.title);
|
||||||
diagram += ` ${node.id}["${label}"]\n`;
|
diagram += ` ${node.id}["${label}"]\n`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# QA Remediation Report
|
||||||
|
|
||||||
|
**File:** /home/jwoltje/src/mosaic-stack/apps/web/src/components/mindmap/MermaidViewer.test.tsx
|
||||||
|
**Tool Used:** Write
|
||||||
|
**Epic:** general
|
||||||
|
**Iteration:** 1
|
||||||
|
**Generated:** 2026-02-03 22:53:43
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Pending QA validation
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
This report was created by the QA automation hook.
|
||||||
|
To process this report, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-web-src-components-mindmap-MermaidViewer.test.tsx_20260203-2253_1_remediation_needed.md"
|
||||||
|
```
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# QA Remediation Report
|
||||||
|
|
||||||
|
**File:** /home/jwoltje/src/mosaic-stack/apps/web/src/components/mindmap/MermaidViewer.test.tsx
|
||||||
|
**Tool Used:** Edit
|
||||||
|
**Epic:** general
|
||||||
|
**Iteration:** 1
|
||||||
|
**Generated:** 2026-02-03 22:54:56
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Pending QA validation
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
This report was created by the QA automation hook.
|
||||||
|
To process this report, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-web-src-components-mindmap-MermaidViewer.test.tsx_20260203-2254_1_remediation_needed.md"
|
||||||
|
```
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# QA Remediation Report
|
||||||
|
|
||||||
|
**File:** /home/jwoltje/src/mosaic-stack/apps/web/src/components/mindmap/MermaidViewer.test.tsx
|
||||||
|
**Tool Used:** Edit
|
||||||
|
**Epic:** general
|
||||||
|
**Iteration:** 1
|
||||||
|
**Generated:** 2026-02-03 22:55:06
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Pending QA validation
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
This report was created by the QA automation hook.
|
||||||
|
To process this report, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-web-src-components-mindmap-MermaidViewer.test.tsx_20260203-2255_1_remediation_needed.md"
|
||||||
|
```
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# QA Remediation Report
|
||||||
|
|
||||||
|
**File:** /home/jwoltje/src/mosaic-stack/apps/web/src/components/mindmap/MermaidViewer.tsx
|
||||||
|
**Tool Used:** Edit
|
||||||
|
**Epic:** general
|
||||||
|
**Iteration:** 1
|
||||||
|
**Generated:** 2026-02-03 22:54:02
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Pending QA validation
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
This report was created by the QA automation hook.
|
||||||
|
To process this report, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-web-src-components-mindmap-MermaidViewer.tsx_20260203-2254_1_remediation_needed.md"
|
||||||
|
```
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# QA Remediation Report
|
||||||
|
|
||||||
|
**File:** /home/jwoltje/src/mosaic-stack/apps/web/src/components/mindmap/MermaidViewer.tsx
|
||||||
|
**Tool Used:** Edit
|
||||||
|
**Epic:** general
|
||||||
|
**Iteration:** 2
|
||||||
|
**Generated:** 2026-02-03 22:54:09
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Pending QA validation
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
This report was created by the QA automation hook.
|
||||||
|
To process this report, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-web-src-components-mindmap-MermaidViewer.tsx_20260203-2254_2_remediation_needed.md"
|
||||||
|
```
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# QA Remediation Report
|
||||||
|
|
||||||
|
**File:** /home/jwoltje/src/mosaic-stack/apps/web/src/components/mindmap/hooks/useGraphData.ts
|
||||||
|
**Tool Used:** Edit
|
||||||
|
**Epic:** general
|
||||||
|
**Iteration:** 1
|
||||||
|
**Generated:** 2026-02-03 22:54:26
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Pending QA validation
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
This report was created by the QA automation hook.
|
||||||
|
To process this report, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-web-src-components-mindmap-hooks-useGraphData.ts_20260203-2254_1_remediation_needed.md"
|
||||||
|
```
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# QA Remediation Report
|
||||||
|
|
||||||
|
**File:** /home/jwoltje/src/mosaic-stack/apps/web/src/components/mindmap/hooks/useGraphData.ts
|
||||||
|
**Tool Used:** Edit
|
||||||
|
**Epic:** general
|
||||||
|
**Iteration:** 2
|
||||||
|
**Generated:** 2026-02-03 22:54:39
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Pending QA validation
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
This report was created by the QA automation hook.
|
||||||
|
To process this report, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-web-src-components-mindmap-hooks-useGraphData.ts_20260203-2254_2_remediation_needed.md"
|
||||||
|
```
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# QA Remediation Report
|
||||||
|
|
||||||
|
**File:** /home/jwoltje/src/mosaic-stack/apps/web/src/components/mindmap/hooks/useGraphData.ts
|
||||||
|
**Tool Used:** Edit
|
||||||
|
**Epic:** general
|
||||||
|
**Iteration:** 1
|
||||||
|
**Generated:** 2026-02-03 22:55:51
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Pending QA validation
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
This report was created by the QA automation hook.
|
||||||
|
To process this report, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-web-src-components-mindmap-hooks-useGraphData.ts_20260203-2255_1_remediation_needed.md"
|
||||||
|
```
|
||||||
83
docs/scratchpads/200-mermaid-xss-enhancement.md
Normal file
83
docs/scratchpads/200-mermaid-xss-enhancement.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# Issue #200: Enhance Mermaid XSS protection with DOMPurify
|
||||||
|
|
||||||
|
## Objective
|
||||||
|
|
||||||
|
Add defense-in-depth security layers to Mermaid rendering:
|
||||||
|
|
||||||
|
1. DOMPurify SVG sanitization
|
||||||
|
2. Input sanitization for labels
|
||||||
|
3. Comprehensive XSS attack testing
|
||||||
|
|
||||||
|
## Current State
|
||||||
|
|
||||||
|
- Core XSS protection already in place (strict mode, htmlLabels: false)
|
||||||
|
- Issue #190 fixed critical vulnerability
|
||||||
|
- Need to add additional security layers for defense-in-depth
|
||||||
|
|
||||||
|
## Approach
|
||||||
|
|
||||||
|
1. Check if DOMPurify is installed, install if needed
|
||||||
|
2. Add DOMPurify SVG sanitization in MermaidViewer
|
||||||
|
3. Add sanitizeMermaidLabel() function in useGraphData
|
||||||
|
4. Write comprehensive XSS tests (TDD)
|
||||||
|
5. Verify all attack vectors are blocked
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
- [x] Create scratchpad
|
||||||
|
- [x] Check/install DOMPurify (already installed)
|
||||||
|
- [x] Write XSS attack tests (TDD - RED)
|
||||||
|
- [x] Add DOMPurify to MermaidViewer
|
||||||
|
- [x] Add label sanitization to useGraphData
|
||||||
|
- [x] Run tests (GREEN) - 15/15 tests passing
|
||||||
|
- [x] Verify type checking passing
|
||||||
|
- [x] Commit and close issue
|
||||||
|
|
||||||
|
## Changes Made
|
||||||
|
|
||||||
|
### MermaidViewer.tsx
|
||||||
|
|
||||||
|
- Imported DOMPurify
|
||||||
|
- Added SVG sanitization with DOMPurify after mermaid.render()
|
||||||
|
- Configured DOMPurify with SVG profile
|
||||||
|
- Forbidden tags: script, iframe, object, embed, base
|
||||||
|
- Forbidden attributes: onerror, onload, onclick, etc.
|
||||||
|
|
||||||
|
### useGraphData.ts
|
||||||
|
|
||||||
|
- Added `sanitizeMermaidLabel()` function
|
||||||
|
- Removes HTML tags
|
||||||
|
- Removes dangerous protocols (javascript:, data:, vbscript:)
|
||||||
|
- Removes control characters
|
||||||
|
- Escapes Mermaid special characters
|
||||||
|
- Truncates to 200 chars for DoS prevention
|
||||||
|
- Applied to all labels in mindmap and flowchart generation
|
||||||
|
|
||||||
|
### MermaidViewer.test.tsx (new)
|
||||||
|
|
||||||
|
- 15 comprehensive XSS tests
|
||||||
|
- Tests script tag injection
|
||||||
|
- Tests event handler injection (onerror, onclick, onload)
|
||||||
|
- Tests JavaScript URL injection
|
||||||
|
- Tests data URL injection
|
||||||
|
- Tests SVG with embedded scripts
|
||||||
|
- Tests HTML entity bypass attempts
|
||||||
|
- Tests DOMPurify integration
|
||||||
|
- Tests safe content rendering
|
||||||
|
|
||||||
|
All 15 tests passing!
|
||||||
|
|
||||||
|
## Test Cases
|
||||||
|
|
||||||
|
1. Script tags in labels: `<script>alert(1)</script>`
|
||||||
|
2. Event handlers: `<img src=x onerror=alert(1)>`
|
||||||
|
3. JavaScript URLs: `javascript:alert(1)`
|
||||||
|
4. Data URLs: `data:text/html,<script>alert(1)</script>`
|
||||||
|
5. SVG with embedded scripts
|
||||||
|
6. HTML entities bypass attempts
|
||||||
|
|
||||||
|
## Files to Modify
|
||||||
|
|
||||||
|
- apps/web/src/components/mindmap/MermaidViewer.tsx
|
||||||
|
- apps/web/src/components/mindmap/hooks/useGraphData.ts
|
||||||
|
- apps/web/src/components/mindmap/MermaidViewer.test.tsx (new)
|
||||||
Reference in New Issue
Block a user