From 90d6bbb4a9c60bf78dca4eda460368198801a60f Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sun, 11 Oct 2026 15:43:27 -0700 Subject: [PATCH] Drop the custom replace, let marked escape it --- src-ui/src/app/pipes/markdown.pipe.spec.ts | 4 +- src-ui/src/app/pipes/markdown.pipe.ts | 43 +++++++++------------- 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/src-ui/src/app/pipes/markdown.pipe.spec.ts b/src-ui/src/app/pipes/markdown.pipe.spec.ts index 102e58065..a0655486e 100644 --- a/src-ui/src/app/pipes/markdown.pipe.spec.ts +++ b/src-ui/src/app/pipes/markdown.pipe.spec.ts @@ -42,7 +42,7 @@ describe('MarkdownPipe', () => { it('should render safe links with target and rel', () => { const html = pipe.transform('[docs](https://docs.paperless-ngx.com "Docs")') expect(html).toContain( - 'docs' + 'docs' ) expect(pipe.transform('[mail](mailto:a@b.c)')).toContain( 'href="mailto:a@b.c"' @@ -77,7 +77,7 @@ describe('MarkdownPipe', () => { it('should omit the hostname label for mailto links', () => { expect(pipe.transform('[mail](mailto:a@b.c)')).toBe( - '

mail

\n' + '

mail

\n' ) }) diff --git a/src-ui/src/app/pipes/markdown.pipe.ts b/src-ui/src/app/pipes/markdown.pipe.ts index fbfb1d0e6..992714c14 100644 --- a/src-ui/src/app/pipes/markdown.pipe.ts +++ b/src-ui/src/app/pipes/markdown.pipe.ts @@ -3,43 +3,34 @@ import { Marked, Renderer, Tokens } from 'marked' const ALLOWED_LINK_PROTOCOLS = new Set(['http:', 'https:', 'mailto:']) -function escapeHtml(text: string): string { - return text - .replaceAll('&', '&') - .replaceAll('<', '<') - .replaceAll('>', '>') - .replaceAll('"', '"') - .replaceAll("'", ''') -} - -function safeHref(href: string): string | null { +function allowedUrl(href: string): URL | null { try { const url = new URL(href) - return ALLOWED_LINK_PROTOCOLS.has(url.protocol) ? url.href : null + return ALLOWED_LINK_PROTOCOLS.has(url.protocol) ? url : null } catch { return null } } // Treat chat content as untrusted: no raw HTML, no images, and only -// absolute links. Angular sanitizer runs on top of this as well. +// absolute links. All escaping is done by marked's default renderer, and +// Angular's sanitizer runs on top of this as well. const renderer: Partial = { - html({ text }: Tokens.HTML | Tokens.Tag): string { - return escapeHtml(text) + html(this: Renderer, { raw, text }: Tokens.HTML | Tokens.Tag): string { + return this.text({ type: 'text', raw, text }) }, - image({ text }: Tokens.Image): string { - return escapeHtml(text) + image(this: Renderer, { raw, text }: Tokens.Image): string { + return this.text({ type: 'text', raw, text }) }, - link(this: Renderer, { href, title, tokens }: Tokens.Link): string { - const text = this.parser.parseInline(tokens) - const url = safeHref(href) - if (!url) return text - const titleAttr = title ? ` title="${escapeHtml(title)}"` : '' - const hostname = new URL(url).hostname - const hostnameLabel = hostname - ? ` (${escapeHtml(hostname)})` - : '' - return `${text}${hostnameLabel}` + link(this: Renderer, token: Tokens.Link): string { + const url = allowedUrl(token.href) + if (!url) return this.parser.parseInline(token.tokens) + const anchor = Renderer.prototype.link + .call(this, token) + .replace('(${hostname})` }, }