From 01a7690b28f9b1e22ce9a89df990fefcb90a159d Mon Sep 17 00:00:00 2001 From: Matthieu MALVACHE Date: Thu, 16 Apr 2026 22:57:10 +0200 Subject: [PATCH] fix(security): escape linkClassName in plainTextToSafeHtml Defense-in-depth. All current callers pass hardcoded tailwind class strings, so this is not exploitable today, but a future caller that forwarded a user-controlled value would get HTML injection through the class attribute. Run the value through escapeHtml() and add a test covering the attribute-escape case. --- lib/__tests__/email-sanitization.test.ts | 9 +++++++++ lib/email-sanitization.ts | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/__tests__/email-sanitization.test.ts b/lib/__tests__/email-sanitization.test.ts index d09e462..dba46de 100644 --- a/lib/__tests__/email-sanitization.test.ts +++ b/lib/__tests__/email-sanitization.test.ts @@ -287,6 +287,15 @@ describe('email-sanitization', () => { expect(html).toContain('class="text-primary hover:underline"'); }); + it('escapes linkClassName to defend against a caller-supplied injection', () => { + const malicious = 'x" onfocus="alert(1)" x="'; + const html = plainTextToSafeHtml('https://x.test', { linkClassName: malicious }); + const a = parseAnchor(html); + expect(a).not.toBeNull(); + expect(a!.getAttribute('onfocus')).toBeNull(); + expect(a!.className).toContain('onfocus='); + }); + it('preserves line breaks and tabs', () => { const html = plainTextToSafeHtml('a\nb\tc\r\nd'); expect(html).toContain('a
b    c
d'); diff --git a/lib/email-sanitization.ts b/lib/email-sanitization.ts index 53125e8..00b50aa 100644 --- a/lib/email-sanitization.ts +++ b/lib/email-sanitization.ts @@ -123,7 +123,7 @@ export function plainTextToSafeHtml( options?: { linkClassName?: string } ): string { const linkClass = options?.linkClassName - ? ` class="${options.linkClassName}"` + ? ` class="${escapeHtml(options.linkClassName)}"` : ''; return escapeHtml(text) .replace(/\r\n/g, '
')