fix(docs): block javascript: URLs in renderMarkdown href (XSS guard)
Per audit batch #127 P2 security minor (issue #130). ## Risk `renderMarkdown` substituted `[text](url)` → `<a href="...url...">` verbatim без URL validation. Edge case: `[click](javascript:alert(1))` → `<a href="javascript:alert(1)">click</a>` 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
This commit is contained in:
parent
8a266502a1
commit
6186804eb8
1 changed files with 31 additions and 2 deletions
|
|
@ -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, "<code>$1</code>");
|
||||
// bold **...**
|
||||
html = html.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
||||
// 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,
|
||||
'<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>',
|
||||
(_, text: string, escapedUrl: string) => {
|
||||
const rawUrl = escapedUrl
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'");
|
||||
const href = escapeHtml(safeUrl(rawUrl));
|
||||
return `<a href="${href}" target="_blank" rel="noopener noreferrer">${text}</a>`;
|
||||
},
|
||||
);
|
||||
// checkboxes
|
||||
html = html.replace(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue