diff --git a/apps/api/package.json b/apps/api/package.json index 134647c..eb1f1d3 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -39,6 +39,7 @@ "mailparser": "^3.9.8", "morgan": "^1.10.0", "multer": "^2.1.1", + "sanitize-html": "^2.17.3", "signale": "^1.4.0", "stripe": "^20.0.0" }, @@ -52,6 +53,7 @@ "@types/mailparser": "^3.4.6", "@types/morgan": "^1.9.9", "@types/multer": "^2.0.0", + "@types/sanitize-html": "^2.16.1", "@types/signale": "^1.4.7", "concurrently": "^9.2.1", "tsx": "^4.20.6" diff --git a/apps/api/src/controllers/Webhooks.ts b/apps/api/src/controllers/Webhooks.ts index 70734bc..a36b965 100644 --- a/apps/api/src/controllers/Webhooks.ts +++ b/apps/api/src/controllers/Webhooks.ts @@ -3,6 +3,7 @@ import type {Prisma} from '@plunk/db'; import {EmailSourceType, EmailStatus} from '@plunk/db'; import type {Request, Response} from 'express'; import {simpleParser} from 'mailparser'; +import sanitizeHtml from 'sanitize-html'; import signale from 'signale'; import type Stripe from 'stripe'; @@ -157,19 +158,31 @@ export class Webhooks { // Parse email content if available let htmlBody: string | undefined; - if (body.content) { + if (body.content && typeof body.content === 'string') { try { const isBase64 = body.receipt?.action?.encoding === 'BASE64'; const emailBuffer = isBase64 - ? Buffer.from(body.content as string, 'base64') - : Buffer.from(body.content as string); + ? Buffer.from(body.content, 'base64') + : Buffer.from(body.content); const parsed = await simpleParser(emailBuffer); - htmlBody = + const raw = (parsed.html ? String(parsed.html) : undefined) ?? parsed.textAsHtml ?? parsed.text ?? 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) { signale.error('[WEBHOOK] Failed to parse email content:', parseError); } diff --git a/yarn.lock b/yarn.lock index aa2bd1d..fc23a6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7086,6 +7086,15 @@ __metadata: languageName: node 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:*": version: 1.2.1 resolution: "@types/send@npm:1.2.1" @@ -8072,6 +8081,7 @@ __metadata: "@types/mailparser": "npm:^3.4.6" "@types/morgan": "npm:^1.9.9" "@types/multer": "npm:^2.0.0" + "@types/sanitize-html": "npm:^2.16.1" "@types/signale": "npm:^1.4.7" "@zootools/email-spell-checker": "npm:^1.12.0" bcrypt: "npm:^6.0.0" @@ -8091,6 +8101,7 @@ __metadata: mailparser: "npm:^3.9.8" morgan: "npm:^1.10.0" multer: "npm:^2.1.1" + sanitize-html: "npm:^2.17.3" signale: "npm:^1.4.0" stripe: "npm:^20.0.0" tsx: "npm:^4.20.6" @@ -9606,7 +9617,7 @@ __metadata: languageName: node linkType: hard -"deepmerge@npm:^4.3.1": +"deepmerge@npm:^4.2.2, deepmerge@npm:^4.3.1": version: 4.3.1 resolution: "deepmerge@npm:4.3.1" checksum: 10c0/e53481aaf1aa2c4082b5342be6b6d8ad9dfe387bc92ce197a66dea08bd4265904a087e75e464f14d1347cf2ac8afe1e4c16b266e0561cc5df29382d3c5f80044 @@ -9835,7 +9846,7 @@ __metadata: languageName: node 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 resolution: "domutils@npm:3.2.2" dependencies: @@ -10101,6 +10112,13 @@ __metadata: languageName: node 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": version: 2.2.1 resolution: "env-paths@npm:2.2.1" @@ -12261,6 +12279,18 @@ __metadata: languageName: node 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": version: 5.0.1 resolution: "htmlparser2@npm:5.0.1" @@ -12762,6 +12792,13 @@ __metadata: languageName: node 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": version: 4.0.0 resolution: "is-promise@npm:4.0.0" @@ -15618,6 +15655,13 @@ __metadata: languageName: node 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": version: 7.1.0 resolution: "parse5-htmlparser2-tree-adapter@npm:7.1.0" @@ -15933,6 +15977,17 @@ __metadata: languageName: node 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": version: 8.5.6 resolution: "postcss@npm:8.5.6" @@ -17160,6 +17215,20 @@ __metadata: languageName: node 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": version: 1.4.3 resolution: "sax@npm:1.4.3"