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.
This commit is contained in:
Matthieu MALVACHE
2026-04-16 22:57:10 +02:00
committed by Matthieu MALVACHE
parent cc01b84f46
commit 01a7690b28
2 changed files with 10 additions and 1 deletions
+9
View File
@@ -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<br>b&nbsp;&nbsp;&nbsp;&nbsp;c<br>d');
+1 -1
View File
@@ -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, '<br>')