diff --git a/apps/web/src/lib/email.test.ts b/apps/web/src/lib/email.test.ts new file mode 100644 index 0000000000..83f44c3de6 --- /dev/null +++ b/apps/web/src/lib/email.test.ts @@ -0,0 +1,13 @@ +import { renderNonAutolinkedText } from '@/lib/email'; + +describe('email rendering helpers', () => { + it('escapes HTML while neutralizing URL autolinking', () => { + const rendered = renderNonAutolinkedText( + 'https://evil.example www.bad.example acme.com' + ); + + expect(rendered.html).toBe( + '<b>https:/​/​evil.​example</​b> www.​bad.​example acme.​com' + ); + }); +}); diff --git a/apps/web/src/lib/email.ts b/apps/web/src/lib/email.ts index 064269819a..a57c1484f0 100644 --- a/apps/web/src/lib/email.ts +++ b/apps/web/src/lib/email.ts @@ -79,6 +79,10 @@ export class RawHtml { type TemplateVars = Record; +export function renderNonAutolinkedText(str: string): RawHtml { + return new RawHtml(escapeHtml(str).replace(/[/.]/g, '$&​')); +} + export function renderTemplate(name: string, vars: TemplateVars): string { const templatePath = path.join(process.cwd(), 'src', 'emails', `${name}.html`); const html = fs.readFileSync(templatePath, 'utf-8'); @@ -195,7 +199,7 @@ export async function sendOrganizationInviteEmail( to: data.to, templateName: 'orgInvitation', templateVars: { - organization_name: data.organizationName, + organization_name: renderNonAutolinkedText(data.organizationName), inviter_name: data.inviterName, accept_invite_url: data.acceptInviteUrl, },