fix(docs): block javascript: URLs in renderMarkdown href (XSS guard) #131
1 changed files with 31 additions and 2 deletions
|
|
@ -13,6 +13,24 @@ function escapeHtml(s: string): string {
|
||||||
.replace(/'/g, "'");
|
.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 {
|
function inlineMd(line: string): string {
|
||||||
// escape first, then re-introduce safe markup
|
// escape first, then re-introduce safe markup
|
||||||
let html = escapeHtml(line);
|
let html = escapeHtml(line);
|
||||||
|
|
@ -20,10 +38,21 @@ function inlineMd(line: string): string {
|
||||||
html = html.replace(/`([^`]+)`/g, "<code>$1</code>");
|
html = html.replace(/`([^`]+)`/g, "<code>$1</code>");
|
||||||
// bold **...**
|
// bold **...**
|
||||||
html = html.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
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(
|
html = html.replace(
|
||||||
/\[([^\]]+)\]\(([^)]+)\)/g,
|
/\[([^\]]+)\]\(([^)]+)\)/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
|
// checkboxes
|
||||||
html = html.replace(
|
html = html.replace(
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue