diff --git a/frontend/src/app/docs/b2b-channels/renderMarkdown.ts b/frontend/src/app/docs/b2b-channels/renderMarkdown.ts index 2074001b..2b7be464 100644 --- a/frontend/src/app/docs/b2b-channels/renderMarkdown.ts +++ b/frontend/src/app/docs/b2b-channels/renderMarkdown.ts @@ -13,6 +13,24 @@ function escapeHtml(s: string): string { .replace(/'/g, "'"); } +/** + * Allow only safe URL schemes in rendered links. + * Blocks javascript:, data:, vbscript:, file:, etc. + */ +function safeUrl(url: string): string { + const trimmed = url.trim(); + if ( + trimmed.startsWith("https://") || + trimmed.startsWith("http://") || + trimmed.startsWith("/") || + trimmed.startsWith("#") || + trimmed.startsWith("mailto:") + ) { + return trimmed; + } + return "#"; +} + function inlineMd(line: string): string { // escape first, then re-introduce safe markup let html = escapeHtml(line); @@ -20,10 +38,21 @@ function inlineMd(line: string): string { html = html.replace(/`([^`]+)`/g, "$1"); // bold **...** html = html.replace(/\*\*([^*]+)\*\*/g, "$1"); - // links [text](url) + // links [text](url) — URL is validated before being placed in href. + // Note: at this point `html` is already HTML-escaped, so we must unescape + // the captured URL before passing through safeUrl, then re-escape for href. html = html.replace( /\[([^\]]+)\]\(([^)]+)\)/g, - '$1', + (_, text: string, escapedUrl: string) => { + const rawUrl = escapedUrl + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, '"') + .replace(/'/g, "'"); + const href = escapeHtml(safeUrl(rawUrl)); + return `${text}`; + }, ); // checkboxes html = html.replace(