feat: add sanitize-html for improved email content sanitization

This commit is contained in:
Dries Augustyns
2026-05-06 11:06:50 +02:00
parent 1193d6c7e6
commit 735acff454
3 changed files with 90 additions and 6 deletions
+2
View File
@@ -39,6 +39,7 @@
"mailparser": "^3.9.8", "mailparser": "^3.9.8",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"multer": "^2.1.1", "multer": "^2.1.1",
"sanitize-html": "^2.17.3",
"signale": "^1.4.0", "signale": "^1.4.0",
"stripe": "^20.0.0" "stripe": "^20.0.0"
}, },
@@ -52,6 +53,7 @@
"@types/mailparser": "^3.4.6", "@types/mailparser": "^3.4.6",
"@types/morgan": "^1.9.9", "@types/morgan": "^1.9.9",
"@types/multer": "^2.0.0", "@types/multer": "^2.0.0",
"@types/sanitize-html": "^2.16.1",
"@types/signale": "^1.4.7", "@types/signale": "^1.4.7",
"concurrently": "^9.2.1", "concurrently": "^9.2.1",
"tsx": "^4.20.6" "tsx": "^4.20.6"
+17 -4
View File
@@ -3,6 +3,7 @@ import type {Prisma} from '@plunk/db';
import {EmailSourceType, EmailStatus} from '@plunk/db'; import {EmailSourceType, EmailStatus} from '@plunk/db';
import type {Request, Response} from 'express'; import type {Request, Response} from 'express';
import {simpleParser} from 'mailparser'; import {simpleParser} from 'mailparser';
import sanitizeHtml from 'sanitize-html';
import signale from 'signale'; import signale from 'signale';
import type Stripe from 'stripe'; import type Stripe from 'stripe';
@@ -157,19 +158,31 @@ export class Webhooks {
// Parse email content if available // Parse email content if available
let htmlBody: string | undefined; let htmlBody: string | undefined;
if (body.content) { if (body.content && typeof body.content === 'string') {
try { try {
const isBase64 = body.receipt?.action?.encoding === 'BASE64'; const isBase64 = body.receipt?.action?.encoding === 'BASE64';
const emailBuffer = isBase64 const emailBuffer = isBase64
? Buffer.from(body.content as string, 'base64') ? Buffer.from(body.content, 'base64')
: Buffer.from(body.content as string); : Buffer.from(body.content);
const parsed = await simpleParser(emailBuffer); const parsed = await simpleParser(emailBuffer);
htmlBody = const raw =
(parsed.html ? String(parsed.html) : undefined) ?? (parsed.html ? String(parsed.html) : undefined) ??
parsed.textAsHtml ?? parsed.textAsHtml ??
parsed.text ?? parsed.text ??
undefined; undefined;
if (raw) {
htmlBody = sanitizeHtml(raw, {
allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img']),
allowedAttributes: {
...sanitizeHtml.defaults.allowedAttributes,
img: ['src', 'alt', 'width', 'height'],
'*': ['style'],
},
allowedSchemes: ['http', 'https', 'mailto'],
});
}
} catch (parseError) { } catch (parseError) {
signale.error('[WEBHOOK] Failed to parse email content:', parseError); signale.error('[WEBHOOK] Failed to parse email content:', parseError);
} }
+71 -2
View File
@@ -7086,6 +7086,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@types/sanitize-html@npm:^2.16.1":
version: 2.16.1
resolution: "@types/sanitize-html@npm:2.16.1"
dependencies:
htmlparser2: "npm:^10.1"
checksum: 10c0/4e0d326a5243edb49780105d3b9a246e6672d998e20aefbc29e5d840bf51a0b03825a20901851ed8f333b4a52ea58b2ac634132792d3c1b0ce91fded9ffe8315
languageName: node
linkType: hard
"@types/send@npm:*": "@types/send@npm:*":
version: 1.2.1 version: 1.2.1
resolution: "@types/send@npm:1.2.1" resolution: "@types/send@npm:1.2.1"
@@ -8072,6 +8081,7 @@ __metadata:
"@types/mailparser": "npm:^3.4.6" "@types/mailparser": "npm:^3.4.6"
"@types/morgan": "npm:^1.9.9" "@types/morgan": "npm:^1.9.9"
"@types/multer": "npm:^2.0.0" "@types/multer": "npm:^2.0.0"
"@types/sanitize-html": "npm:^2.16.1"
"@types/signale": "npm:^1.4.7" "@types/signale": "npm:^1.4.7"
"@zootools/email-spell-checker": "npm:^1.12.0" "@zootools/email-spell-checker": "npm:^1.12.0"
bcrypt: "npm:^6.0.0" bcrypt: "npm:^6.0.0"
@@ -8091,6 +8101,7 @@ __metadata:
mailparser: "npm:^3.9.8" mailparser: "npm:^3.9.8"
morgan: "npm:^1.10.0" morgan: "npm:^1.10.0"
multer: "npm:^2.1.1" multer: "npm:^2.1.1"
sanitize-html: "npm:^2.17.3"
signale: "npm:^1.4.0" signale: "npm:^1.4.0"
stripe: "npm:^20.0.0" stripe: "npm:^20.0.0"
tsx: "npm:^4.20.6" tsx: "npm:^4.20.6"
@@ -9606,7 +9617,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"deepmerge@npm:^4.3.1": "deepmerge@npm:^4.2.2, deepmerge@npm:^4.3.1":
version: 4.3.1 version: 4.3.1
resolution: "deepmerge@npm:4.3.1" resolution: "deepmerge@npm:4.3.1"
checksum: 10c0/e53481aaf1aa2c4082b5342be6b6d8ad9dfe387bc92ce197a66dea08bd4265904a087e75e464f14d1347cf2ac8afe1e4c16b266e0561cc5df29382d3c5f80044 checksum: 10c0/e53481aaf1aa2c4082b5342be6b6d8ad9dfe387bc92ce197a66dea08bd4265904a087e75e464f14d1347cf2ac8afe1e4c16b266e0561cc5df29382d3c5f80044
@@ -9835,7 +9846,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"domutils@npm:^3.0.1, domutils@npm:^3.1.0": "domutils@npm:^3.0.1, domutils@npm:^3.1.0, domutils@npm:^3.2.2":
version: 3.2.2 version: 3.2.2
resolution: "domutils@npm:3.2.2" resolution: "domutils@npm:3.2.2"
dependencies: dependencies:
@@ -10101,6 +10112,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"entities@npm:^7.0.1":
version: 7.0.1
resolution: "entities@npm:7.0.1"
checksum: 10c0/b4fb9937bb47ecb00aaaceb9db9cdd1cc0b0fb649c0e843d05cf5dbbd2e9d2df8f98721d8b1b286445689c72af7b54a7242fc2d63ef7c9739037a8c73363e7ca
languageName: node
linkType: hard
"env-paths@npm:^2.2.0": "env-paths@npm:^2.2.0":
version: 2.2.1 version: 2.2.1
resolution: "env-paths@npm:2.2.1" resolution: "env-paths@npm:2.2.1"
@@ -12261,6 +12279,18 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"htmlparser2@npm:^10.1, htmlparser2@npm:^10.1.0":
version: 10.1.0
resolution: "htmlparser2@npm:10.1.0"
dependencies:
domelementtype: "npm:^2.3.0"
domhandler: "npm:^5.0.3"
domutils: "npm:^3.2.2"
entities: "npm:^7.0.1"
checksum: 10c0/36394e29b80cfcc5e78e0fa4d3aa21fdaac3e6778d23e5c933e625c290987cd9a724a2eb0753ab60ed0c69dfaba0ab115f0ee50fb112fd8f0c4d522e7e0089a2
languageName: node
linkType: hard
"htmlparser2@npm:^5.0.0": "htmlparser2@npm:^5.0.0":
version: 5.0.1 version: 5.0.1
resolution: "htmlparser2@npm:5.0.1" resolution: "htmlparser2@npm:5.0.1"
@@ -12762,6 +12792,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"is-plain-object@npm:^5.0.0":
version: 5.0.0
resolution: "is-plain-object@npm:5.0.0"
checksum: 10c0/893e42bad832aae3511c71fd61c0bf61aa3a6d853061c62a307261842727d0d25f761ce9379f7ba7226d6179db2a3157efa918e7fe26360f3bf0842d9f28942c
languageName: node
linkType: hard
"is-promise@npm:^4.0.0": "is-promise@npm:^4.0.0":
version: 4.0.0 version: 4.0.0
resolution: "is-promise@npm:4.0.0" resolution: "is-promise@npm:4.0.0"
@@ -15618,6 +15655,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"parse-srcset@npm:^1.0.2":
version: 1.0.2
resolution: "parse-srcset@npm:1.0.2"
checksum: 10c0/2f268e3d110d4c53d06ed2a8e8ee61a7da0cee13bf150819a6da066a8ca9b8d15b5600d6e6cae8be940e2edc50ee7c1e1052934d6ec858324065ecef848f0497
languageName: node
linkType: hard
"parse5-htmlparser2-tree-adapter@npm:^7.0.0": "parse5-htmlparser2-tree-adapter@npm:^7.0.0":
version: 7.1.0 version: 7.1.0
resolution: "parse5-htmlparser2-tree-adapter@npm:7.1.0" resolution: "parse5-htmlparser2-tree-adapter@npm:7.1.0"
@@ -15933,6 +15977,17 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"postcss@npm:^8.3.11":
version: 8.5.14
resolution: "postcss@npm:8.5.14"
dependencies:
nanoid: "npm:^3.3.11"
picocolors: "npm:^1.1.1"
source-map-js: "npm:^1.2.1"
checksum: 10c0/48138207cf5ef5581be1bfe2cb65ccfe0ac75e43888ba045afc8ed6043d7b56aeb3b9a9fe5b353ff554be943cd0cc15d826ccb991525159175971e5ee8ab0237
languageName: node
linkType: hard
"postcss@npm:^8.4.23, postcss@npm:^8.4.33, postcss@npm:^8.4.41, postcss@npm:^8.5.6": "postcss@npm:^8.4.23, postcss@npm:^8.4.33, postcss@npm:^8.4.41, postcss@npm:^8.5.6":
version: 8.5.6 version: 8.5.6
resolution: "postcss@npm:8.5.6" resolution: "postcss@npm:8.5.6"
@@ -17160,6 +17215,20 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"sanitize-html@npm:^2.17.3":
version: 2.17.3
resolution: "sanitize-html@npm:2.17.3"
dependencies:
deepmerge: "npm:^4.2.2"
escape-string-regexp: "npm:^4.0.0"
htmlparser2: "npm:^10.1.0"
is-plain-object: "npm:^5.0.0"
parse-srcset: "npm:^1.0.2"
postcss: "npm:^8.3.11"
checksum: 10c0/8afa59bed125b38bf4b437f9b5a3289a4307f42d720e45105de5a0b3d665be70e27d1722d223121993be2e54a2b99304cd9c54317fb2d251fd7f4abf06b68d27
languageName: node
linkType: hard
"sax@npm:^1.2.4": "sax@npm:^1.2.4":
version: 1.4.3 version: 1.4.3
resolution: "sax@npm:1.4.3" resolution: "sax@npm:1.4.3"