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(