diff --git a/apps/web/src/components/knowledge/WikiLinkRenderer.tsx b/apps/web/src/components/knowledge/WikiLinkRenderer.tsx index ffa3511..e0027c5 100644 --- a/apps/web/src/components/knowledge/WikiLinkRenderer.tsx +++ b/apps/web/src/components/knowledge/WikiLinkRenderer.tsx @@ -28,7 +28,58 @@ export function WikiLinkRenderer({ className = "", }: WikiLinkRendererProps): React.ReactElement { const processedHtml = React.useMemo(() => { - return parseWikiLinks(html); + // SEC-WEB-2 FIX: Sanitize ENTIRE HTML input BEFORE processing wiki-links + // This prevents stored XSS via knowledge entry content + const sanitizedHtml = DOMPurify.sanitize(html, { + // Allow common formatting tags that are safe + ALLOWED_TAGS: [ + "p", + "br", + "strong", + "b", + "em", + "i", + "u", + "s", + "strike", + "del", + "ins", + "mark", + "small", + "sub", + "sup", + "code", + "pre", + "blockquote", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "ul", + "ol", + "li", + "dl", + "dt", + "dd", + "table", + "thead", + "tbody", + "tfoot", + "tr", + "th", + "td", + "hr", + "span", + "div", + ], + // Allow safe attributes only + ALLOWED_ATTR: ["class", "id", "title", "lang", "dir"], + // Remove any data: or javascript: URIs + ALLOW_DATA_ATTR: false, + }); + return parseWikiLinks(sanitizedHtml); }, [html]); return ( diff --git a/apps/web/src/components/knowledge/__tests__/WikiLinkRenderer.test.tsx b/apps/web/src/components/knowledge/__tests__/WikiLinkRenderer.test.tsx index 03ffb47..c34ee0c 100644 --- a/apps/web/src/components/knowledge/__tests__/WikiLinkRenderer.test.tsx +++ b/apps/web/src/components/knowledge/__tests__/WikiLinkRenderer.test.tsx @@ -69,19 +69,19 @@ describe("WikiLinkRenderer", (): void => { }); it("escapes HTML in link text to prevent XSS", (): void => { + // SEC-WEB-2: DOMPurify now sanitizes entire HTML BEFORE wiki-link processing + // Script tags are stripped, which may break wiki-link patterns like [[entry|]] const html = "
[[entry|]]
"; const { container } = render([[entry|]]
- malformed wiki-link (empty display text with |) + // The wiki-link regex doesn't match [[entry|]] because |([^\]]+) requires 1+ chars + // So no wiki-link is created - the XSS is prevented by stripping dangerous content - // Script tags should be removed by DOMPurify (including content) - const linkHtml = link?.innerHTML ?? ""; - expect(linkHtml).not.toContain("]]"; const { container } = render([[my-entry]]
'; + const { container } = render(