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,
},