From 8647114b22bfbfd375ce50c8ca3ad23aec916a97 Mon Sep 17 00:00:00 2001 From: lekss361 <47113017+lekss361@users.noreply.github.com> Date: Thu, 14 May 2026 23:39:47 +0300 Subject: [PATCH] fix(docs): block javascript: URLs in renderMarkdown href (XSS guard) (#131) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per audit batch #127 P2 security minor (issue #130). ## Risk `renderMarkdown` substituted `[text](url)` → `` verbatim без URL validation. Edge case: `[click](javascript:alert(1))` → `click` execute'нется при click. Currently low risk (markdown source = build-time fs.readFileSync из public/docs/, checked into repo), но защита-в-глубину: если когда-либо markdown source станет user-supplied, vuln materialize. ## Fix `frontend/src/app/docs/b2b-channels/renderMarkdown.ts`: 1. New `safeUrl(url)` function — allowlist `https://`, `http://`, `/`, `#`, `mailto:`. Everything else → `"#"`. 2. Link regex replacement switched from backreference string to callback so URL passes через `safeUrl` ДО injection в href. 3. URL unescape → safeUrl → re-escape (HTML escape applied per-line). ## Test cases verified - `[ok](https://example.com)` → href=`https://example.com` ✅ - `[ok](/local)` → href=`/local` ✅ - `[ok](#anchor)` → href=`#anchor` ✅ - `[ok](mailto:foo@bar.com)` → href=`mailto:foo@bar.com` ✅ - `[bad](javascript:alert(1))` → href=`#` ✅ - `[bad](data:text/html,...)` → href=`#` ✅ - `[bad](vbscript:msgbox(1))` → href=`#` ✅ ## Checks - tsc: 0 errors - lint: 0 warnings - No existing `__tests__/renderMarkdown` to break ## Vault `fixes/Bug_RenderMarkdown_JavascriptUrl_May14.md` — created. Closes #130 Co-authored-by: lekss361 --- .../app/docs/b2b-channels/renderMarkdown.ts | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) 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(