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(