CRITICAL SECURITY FIXES for two XSS vulnerabilities Mermaid XSS Fix (#190): - Changed securityLevel from "loose" to "strict" - Disabled htmlLabels to prevent HTML injection - Blocks script execution and event handlers in SVG output WikiLink XSS Fix (#191): - Added alphanumeric whitelist validation for slugs - Escape HTML entities in title attribute - Reject slugs with special characters that could break attributes - Return escaped text for invalid slugs Security Impact: - Prevents account takeover via cookie theft - Blocks malicious script execution in user browsers - Enforces strict content security for user-provided content Fixes #190, #191 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
118 lines
3.3 KiB
TypeScript
118 lines
3.3 KiB
TypeScript
/* eslint-disable security/detect-unsafe-regex */
|
|
"use client";
|
|
|
|
import React from "react";
|
|
|
|
interface WikiLinkRendererProps {
|
|
/** HTML content with wiki-links to parse */
|
|
html: string;
|
|
/** Additional CSS classes */
|
|
className?: string;
|
|
}
|
|
|
|
/**
|
|
* WikiLinkRenderer - Parses and renders wiki-links in HTML content
|
|
*
|
|
* Converts:
|
|
* - [[slug]] → clickable link to /knowledge/slug
|
|
* - [[slug|display text]] → clickable link with custom text
|
|
*
|
|
* Features:
|
|
* - Distinct styling for wiki-links (blue color, underline)
|
|
* - Graceful handling of broken links (gray out)
|
|
* - Preserves all other HTML formatting
|
|
*/
|
|
export function WikiLinkRenderer({
|
|
html,
|
|
className = "",
|
|
}: WikiLinkRendererProps): React.ReactElement {
|
|
const processedHtml = React.useMemo(() => {
|
|
return parseWikiLinks(html);
|
|
}, [html]);
|
|
|
|
return (
|
|
<div
|
|
className={`wiki-link-content ${className}`}
|
|
dangerouslySetInnerHTML={{ __html: processedHtml }}
|
|
onClick={handleWikiLinkClick}
|
|
/>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Parse wiki-links in HTML and convert to anchor tags
|
|
*
|
|
* Supports:
|
|
* - [[slug]] - basic link
|
|
* - [[slug|display text]] - link with custom display text
|
|
*/
|
|
function parseWikiLinks(html: string): string {
|
|
// Match [[...]] patterns
|
|
// Group 1: target slug
|
|
// Group 2: optional display text after |
|
|
const wikiLinkRegex = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
|
|
|
|
return html.replace(wikiLinkRegex, (match, slug: string, displayText?: string) => {
|
|
const trimmedSlug = slug.trim();
|
|
const text = displayText?.trim() ?? trimmedSlug;
|
|
|
|
// Validate slug contains only safe characters
|
|
if (!/^[a-zA-Z0-9\-_./]+$/.test(trimmedSlug)) {
|
|
// Invalid slug - return original text without creating a link
|
|
return escapeHtml(match);
|
|
}
|
|
|
|
// Create a styled link
|
|
// Using data-wiki-link attribute for styling and click handling
|
|
return `<a
|
|
href="/knowledge/${encodeURIComponent(trimmedSlug)}"
|
|
data-wiki-link="true"
|
|
data-slug="${encodeURIComponent(trimmedSlug)}"
|
|
class="wiki-link text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 underline decoration-dotted hover:decoration-solid transition-colors"
|
|
title="Go to ${escapeHtml(trimmedSlug)}"
|
|
>${escapeHtml(text)}</a>`;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handle wiki-link clicks
|
|
* Intercepts clicks on wiki-links to use Next.js navigation
|
|
*/
|
|
function handleWikiLinkClick(e: React.MouseEvent<HTMLDivElement>): void {
|
|
const target = e.target as HTMLElement;
|
|
|
|
// Check if the clicked element is a wiki-link
|
|
if (target.tagName === "A" && target.dataset.wikiLink === "true") {
|
|
const href = target.getAttribute("href");
|
|
if (href?.startsWith("/knowledge/")) {
|
|
// Let Next.js Link handle navigation naturally
|
|
// No need to preventDefault - the href will work
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Escape HTML to prevent XSS
|
|
*/
|
|
function escapeHtml(text: string): string {
|
|
const div = document.createElement("div");
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
/**
|
|
* Custom hook to check if a wiki-link target exists
|
|
* (For future enhancement - mark broken links differently)
|
|
*/
|
|
export function useWikiLinkValidation(_slug: string): {
|
|
isValid: boolean;
|
|
isLoading: boolean;
|
|
} {
|
|
// Placeholder for future implementation
|
|
// Could fetch from /api/knowledge/entries/:slug to check existence
|
|
return {
|
|
isValid: true,
|
|
isLoading: false,
|
|
};
|
|
}
|