diff --git a/server/src/emails/album-invite.email.tsx b/server/src/emails/album-invite.email.tsx
index 49adb25e5..e82e2ec79 100644
--- a/server/src/emails/album-invite.email.tsx
+++ b/server/src/emails/album-invite.email.tsx
@@ -3,7 +3,7 @@ import * as React from 'react';
import { ImmichButton } from 'src/emails/components/button.component.js';
import ImmichLayout from 'src/emails/components/immich.layout.js';
import { AlbumInviteEmailProps } from 'src/repositories/email.repository.js';
-import { replaceTemplateTags } from 'src/utils/replace-template-tags.js';
+import { replaceTemplateTags, templatePreview } from 'src/utils/replace-template-tags.js';
export const AlbumInviteEmail = ({
baseUrl,
@@ -37,7 +37,9 @@ export const AlbumInviteEmail = ({
);
return (
-
', + albumId: '123', + senderName: '', + recipientName: "Tom & Jerry O'Brien", + baseUrl: 'http://localhost', + }, + customTemplate: '
Hey {recipientName}, {senderName} added you to {albumName}
', + }; + + const result = await sut.renderEmail(request); + + expect(result.html).toContain('<script>alert(1)</script>'); + expect(result.html).toContain('<a href="https://example.com">'); + expect(result.html).not.toContain(''); + expect(result.html).not.toContain(''); + expect(result.html).toContain('Hey Tom & Jerry O'Brien'); + }); + it('should render the email correctly for ALBUM_UPDATE template', async () => { const request: EmailRenderRequest = { template: EmailTemplate.ALBUM_UPDATE, diff --git a/server/src/utils/replace-template-tags.spec.ts b/server/src/utils/replace-template-tags.spec.ts new file mode 100644 index 000000000..b3c800ada --- /dev/null +++ b/server/src/utils/replace-template-tags.spec.ts @@ -0,0 +1,40 @@ +import { replaceTemplateTags, templatePreview } from 'src/utils/replace-template-tags.js'; + +describe(replaceTemplateTags.name, () => { + it('should replace tags', () => { + expect(replaceTemplateTags('Hello {name}, welcome to {instance}', { name: 'Alan', instance: 'Immich' })).toBe( + 'Hello Alan, welcome to Immich', + ); + }); + + it('should leave unknown tags untouched', () => { + expect(replaceTemplateTags('Hello {name} {unknown}', { name: 'Alan' })).toBe('Hello Alan {unknown}'); + }); + + it('should escape html in values', () => { + expect( + replaceTemplateTags('
{name} shared {album}
', { + name: '', + }), + ).toBe( + '
<img src=x onerror="alert(1)"> shared </p><a href="https://example.com">x</a><p>
', + ); + }); + + it('should not escape html when disabled', () => { + expect(replaceTemplateTags('{name}', { name: 'Alan' }, { escapeHtml: false })).toBe('Alan'); + }); +}); + +describe(templatePreview.name, () => { + it('should produce plain text with unescaped values', () => { + expect(templatePreview('Hey {name}!
', { name: "Tom & Jerry O'Brien" })).toBe( + "Hey Tom & Jerry O'Brien!", + ); + }); + + it('should strip tags that came from values', () => { + expect(templatePreview('{name}
', { name: 'click' })).toBe('click'); + }); +}); diff --git a/server/src/utils/replace-template-tags.ts b/server/src/utils/replace-template-tags.ts index 70333d7df..255a7f155 100644 --- a/server/src/utils/replace-template-tags.ts +++ b/server/src/utils/replace-template-tags.ts @@ -1,5 +1,25 @@ -export const replaceTemplateTags = (template: string, variables: Record