diff --git a/lib/__tests__/email-sanitization.test.ts b/lib/__tests__/email-sanitization.test.ts
index f36c5b50..1f9c2cb2 100644
--- a/lib/__tests__/email-sanitization.test.ts
+++ b/lib/__tests__/email-sanitization.test.ts
@@ -78,11 +78,67 @@ describe('email-sanitization', () => {
expect(clean).toContain('John Doe');
});
- it('should remove images from signatures', () => {
- const signature = '
John
';
+ it('should allow img with https src', () => {
+ const signature = 'John
';
const clean = sanitizeSignatureHtml(signature);
+ expect(clean).toContain('
{
+ const dataUri = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX/AAAZ4gk3AAAAAXRSTlPM0jRW/QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAA1JREFUCNdjYGBgAAAABAABc7Rs9wAAAABJRU5ErkJggg==';
+ const signature = `
`;
+ const clean = sanitizeSignatureHtml(signature);
+ expect(clean).toContain('
{
+ const cases = ['data:image/jpeg;base64,AAA', 'data:image/jpg;base64,AAA', 'data:image/gif;base64,AAA', 'data:image/webp;base64,AAA'];
+ for (const src of cases) {
+ const clean = sanitizeSignatureHtml(`
`);
+ expect(clean).toContain('
{
+ const signature = '
';
+ const clean = sanitizeSignatureHtml(signature);
+ expect(clean).not.toContain('http://insecure.example.com');
expect(clean).not.toContain('
{
+ const signature = '
';
+ const clean = sanitizeSignatureHtml(signature);
+ expect(clean).not.toContain('javascript:');
+ expect(clean).not.toContain('
{
+ const signature = '
';
+ const clean = sanitizeSignatureHtml(signature);
+ expect(clean).not.toContain('data:image/svg');
+ expect(clean).not.toContain('
{
+ const signature = '
';
+ const clean = sanitizeSignatureHtml(signature);
+ expect(clean).not.toContain('data:text/html');
+ expect(clean).not.toContain('
{
+ const signature = '
';
+ const clean = sanitizeSignatureHtml(signature);
+ expect(clean).not.toContain('onerror');
+ expect(clean).not.toContain('onload');
+ expect(clean).toContain('https://cdn.example.com/logo.png');
});
it('should remove video and audio tags', () => {
@@ -113,16 +169,17 @@ describe('email-sanitization', () => {
});
it('should be stricter than email sanitization', () => {
- const html = 'Text
';
+ const html = 'Text
';
const emailClean = sanitizeEmailHtml(html);
const signatureClean = sanitizeSignatureHtml(html);
- // Email allows img and table
- expect(emailClean).toContain('
');
- // Signature blocks img but may allow some tables (verify in implementation)
- expect(signatureClean).not.toContain('
for company logos
*/
export const SIGNATURE_SANITIZE_CONFIG = {
- ALLOWED_TAGS: ['p', 'br', 'b', 'strong', 'i', 'em', 'u', 'a', 'span', 'div'],
- ALLOWED_ATTR: ['href', 'style', 'class'],
+ ALLOWED_TAGS: ['p', 'br', 'b', 'strong', 'i', 'em', 'u', 'a', 'span', 'div', 'img'],
+ ALLOWED_ATTR: ['href', 'style', 'class', 'src', 'alt', 'width', 'height', 'title'],
ALLOW_DATA_ATTR: false,
- FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'img', 'video', 'audio'],
+ FORBID_TAGS: ['script', 'iframe', 'object', 'embed', 'video', 'audio'],
FORBID_ATTR: ['onerror', 'onload', 'onclick', 'onmouseover'],
};
/**
- * Sanitize HTML signature for storage and display
+ * Sanitize HTML signature for storage and display.
+ * img src is restricted to https: or base64-embedded raster data: URIs
+ * (png/jpeg/gif/webp). SVG is excluded because DOMPurify cannot inspect
+ * bytes inside a data: URI. Images with a disallowed src are removed
+ * entirely so they don't render as broken-image icons.
* @param html - User-provided HTML signature
* @returns Sanitized signature (no scripts, no external resources)
*/
export function sanitizeSignatureHtml(html: string): string {
if (!html?.trim()) return '';
- return DOMPurify.sanitize(html, SIGNATURE_SANITIZE_CONFIG);
+ DOMPurify.addHook('afterSanitizeAttributes', (node) => {
+ if (node.tagName !== 'IMG') return;
+ const src = node.getAttribute('src');
+ if (!src || !/^(?:https:\/\/|data:image\/(?:png|jpe?g|gif|webp);base64,)/i.test(src)) {
+ node.remove();
+ }
+ });
+ try {
+ return DOMPurify.sanitize(html, SIGNATURE_SANITIZE_CONFIG);
+ } finally {
+ DOMPurify.removeAllHooks();
+ }
}
/**