From 75d17d4e378b6ae2227dffb7c69c6b4361d9bbf5 Mon Sep 17 00:00:00 2001 From: honzup <5564623+honzup@users.noreply.github.com> Date: Sat, 11 Jul 2026 09:33:10 +0200 Subject: [PATCH] fix: keep target/rel on links in plain-text message bodies Plain-text bodies render into the main document rather than the sandboxed iframe, so an anchor without target="_blank" navigates the whole app away instead of opening a new tab. plainTextToSafeHtml emits target and rel correctly, but sanitizePlainTextRenderedHtml stripped both back off: DOMPurify URI-tests every attribute value not on its URI-safe list, and "_blank" does not match PLAIN_TEXT_RENDERED_CONFIG's ALLOWED_URI_REGEXP. EMAIL_SANITIZE_CONFIG avoids this only because its regex carries a catch-all alternation for non-URI values. Mark target and rel as URI-safe so they survive the URI test, rather than loosening href validation. --- lib/__tests__/email-sanitization.test.ts | 21 +++++++++++++++++++++ lib/email-sanitization.ts | 6 ++++++ 2 files changed, 27 insertions(+) diff --git a/lib/__tests__/email-sanitization.test.ts b/lib/__tests__/email-sanitization.test.ts index 4691b0e0..103d2dcf 100644 --- a/lib/__tests__/email-sanitization.test.ts +++ b/lib/__tests__/email-sanitization.test.ts @@ -6,6 +6,7 @@ import { parseHtmlSafely, hasRichFormatting, plainTextToSafeHtml, + sanitizePlainTextRenderedHtml, EMAIL_SANITIZE_CONFIG, EMAIL_IFRAME_SANITIZE_CONFIG, isExternalResourceUrl, @@ -580,4 +581,24 @@ describe('email-sanitization', () => { expect(result).toContain('javascript:alert(1)'); }); }); + + describe('sanitizePlainTextRenderedHtml', () => { + // This branch renders into the main document, not the sandboxed iframe, so + // an anchor that loses target="_blank" navigates the whole app away. + it('preserves target and rel on links emitted by plainTextToSafeHtml', () => { + const rendered = sanitizePlainTextRenderedHtml( + plainTextToSafeHtml('see https://github.com/honzup/webmail/pull/560'), + ); + expect(rendered).toContain('target="_blank"'); + expect(rendered).toContain('rel="noopener noreferrer"'); + }); + + it('still strips dangerous schemes and tags', () => { + const rendered = sanitizePlainTextRenderedHtml( + 'x', + ); + expect(rendered).not.toContain('javascript:'); + expect(rendered).not.toContain('