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('