Skip to content

Commit acb3968

Browse files
authored
fix: blacklisted emails and invalid attachments getting escaped (#1233)
## Problem Error emails sent by Plumber for blacklisted emails and invalid attachments are having `<li>` escaped. This arose from the fix for the VAPT finding of html injection via pipe names. ## Solution Introduced a `trustedHtml` for the blacklisted emails and invalid attachments, while still escaping the email and attachment name themselves to be safe. ## Tests - [ ] Trigger an error email when one or more blacklisted recipients are detected; the list should be rendered properly - [ ] Trigger an error email when one or more invalid attachments are detected; the list should be rendered properly ## Screenshots <img width="1120" height="459" alt="Screenshot 2025-09-25 at 11 36 35 PM" src="https://github.com/user-attachments/assets/2c2a196f-1957-45a5-a984-3f593aeb241b" /> <img width="1112" height="515" alt="Screenshot 2025-09-25 at 11 37 42 PM" src="https://github.com/user-attachments/assets/162b13eb-34d6-4ac0-a7a7-e4f8a60360c3" />
1 parent 4a11cc1 commit acb3968

File tree

4 files changed

+136
-8
lines changed

4 files changed

+136
-8
lines changed

packages/backend/src/apps/postman/common/send-blacklist-email.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { DateTime } from 'luxon'
22

33
import { redisClient as pipeErrorRedisClient } from '@/helpers/generate-error-email'
4-
import { safeHtml } from '@/helpers/html-utils'
4+
import { escapeHtml, safeHtml, trustedHtml } from '@/helpers/html-utils'
55
import { sendEmail } from '@/helpers/send-email'
66

77
const MAX_LENGTH = 80
@@ -58,7 +58,11 @@ function createBodyErrorMessage(props: BlacklistEmailProps): string {
5858
<br>
5959
We have detected that your pipe <strong>${flowName}</strong> has attempted to send an email to one or more blacklisted email addresses:
6060
<ul>
61-
${blacklistedRecipients.map((email) => `<li>${email}</li>`).join('\n')}
61+
${trustedHtml(
62+
blacklistedRecipients
63+
.map((email) => `<li>${escapeHtml(email)}</li>`)
64+
.join('\n'),
65+
)}
6266
</ul>
6367
Emails could be blacklisted for one of the following reasons:
6468
<ul>

packages/backend/src/apps/postman/common/send-invalid-attachments-email.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { truncateFlowName } from '@/helpers/generate-error-email'
2-
import { safeHtml } from '@/helpers/html-utils'
2+
import { escapeHtml, safeHtml, trustedHtml } from '@/helpers/html-utils'
33
import { sendEmail } from '@/helpers/send-email'
44

55
interface SendInvalidAttachmentsEmailProps {
@@ -23,7 +23,9 @@ export function createInvalidAttachmentsMessage(props: CreateMessageProps) {
2323
const bodyMessage = safeHtml`
2424
We have detected that your pipe <strong>${flowName}</strong> has attempted to send an email with one or more attachments that are not supported:
2525
<ul>
26-
${invalidAttachments.map((a) => `<li>${a}</li>`).join('\n')}
26+
${trustedHtml(
27+
invalidAttachments.map((a) => `<li>${escapeHtml(a)}</li>`).join('\n'),
28+
)}
2729
</ul>
2830
The details of the affected execution are as follows:
2931
<ul>

packages/backend/src/helpers/__tests__/html-utils.test.ts

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { describe, expect, it } from 'vitest'
22

3-
import { escapeHtml, safeHtml } from '../html-utils'
3+
import { escapeHtml, safeHtml, trustedHtml } from '../html-utils'
44

55
describe('html utils', () => {
66
describe('escapeHtml', () => {
@@ -49,6 +49,26 @@ describe('html utils', () => {
4949
})
5050
})
5151

52+
describe('trustedHtml', () => {
53+
it('should create a trusted HTML object', () => {
54+
const html = '<div>Hello World</div>'
55+
const trusted = trustedHtml(html)
56+
57+
expect(trusted).toEqual({
58+
__html: html,
59+
__trusted: true,
60+
})
61+
})
62+
63+
it('should preserve HTML tags in trusted content', () => {
64+
const html = '<script>alert("XSS")</script><b>Bold</b>'
65+
const trusted = trustedHtml(html)
66+
67+
expect(trusted.__html).toBe(html)
68+
expect(trusted.__trusted).toBe(true)
69+
})
70+
})
71+
5272
describe('safeHtml', () => {
5373
it('should escape multiple HTML values in a string', () => {
5474
const testVariable = '<b>Hello</b>'
@@ -63,4 +83,94 @@ describe('safeHtml', () => {
6383
With a new line: ${testVariable2}
6484
`).toBe(expected)
6585
})
86+
87+
it('should not escape trusted HTML content', () => {
88+
const trustedContent = trustedHtml('<li>Item 1</li><li>Item 2</li>')
89+
const regularContent = '<script>alert("XSS")</script>'
90+
91+
const result = safeHtml`
92+
<ul>
93+
${trustedContent}
94+
</ul>
95+
<p>Regular content: ${regularContent}</p>
96+
`
97+
98+
expect(result).toContain('<li>Item 1</li><li>Item 2</li>')
99+
expect(result).toContain(
100+
'&lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;',
101+
)
102+
})
103+
104+
it('should handle mixed trusted and regular content', () => {
105+
const trustedList = trustedHtml(
106+
'<li>Safe item</li><li>Another safe item</li>',
107+
)
108+
const dangerousContent = '<script>alert("XSS")</script>'
109+
const safeText = 'Hello World'
110+
111+
const result = safeHtml`
112+
<div>
113+
<h1>${safeText}</h1>
114+
<ul>
115+
${trustedList}
116+
</ul>
117+
<p>Dangerous: ${dangerousContent}</p>
118+
</div>
119+
`
120+
121+
expect(result).toContain('<h1>Hello World</h1>')
122+
expect(result).toContain('<li>Safe item</li><li>Another safe item</li>')
123+
expect(result).toContain(
124+
'&lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;',
125+
)
126+
})
127+
128+
it('should handle trusted HTML with escaped content inside', () => {
129+
const userInput = '<script>alert("XSS")</script>'
130+
const escapedUserInput = escapeHtml(userInput)
131+
const trustedContent = trustedHtml(`<li>${escapedUserInput}</li>`)
132+
133+
const result = safeHtml`
134+
<ul>
135+
${trustedContent}
136+
</ul>
137+
`
138+
139+
expect(result).toContain(
140+
'<li>&lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;</li>',
141+
)
142+
expect(result).not.toContain('<script>')
143+
})
144+
145+
it('should handle multiple trusted HTML objects', () => {
146+
const trusted1 = trustedHtml('<strong>Bold text</strong>')
147+
const trusted2 = trustedHtml('<em>Italic text</em>')
148+
const regularText = 'Normal text'
149+
150+
const result = safeHtml`
151+
<p>${trusted1} and ${trusted2} with ${regularText}</p>
152+
`
153+
154+
expect(result).toContain('<strong>Bold text</strong>')
155+
expect(result).toContain('<em>Italic text</em>')
156+
expect(result).toContain('Normal text')
157+
})
158+
159+
it('should handle null and undefined values gracefully', () => {
160+
const trustedContent = trustedHtml('<div>Content</div>')
161+
const nullValue = null as null
162+
const undefinedValue = undefined as undefined
163+
164+
const result = safeHtml`
165+
<div>
166+
${trustedContent}
167+
<p>Null: ${nullValue}</p>
168+
<p>Undefined: ${undefinedValue}</p>
169+
</div>
170+
`
171+
172+
expect(result).toContain('<div>Content</div>')
173+
expect(result).toContain('<p>Null: </p>')
174+
expect(result).toContain('<p>Undefined: </p>')
175+
})
66176
})

packages/backend/src/helpers/html-utils.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,31 @@ function escapeHtml(unsafe: string): string {
99
}
1010

1111
// tag function for safe HTML templates
12-
function safeHtml(strings: TemplateStringsArray, ...values: string[]): string {
12+
function safeHtml(
13+
strings: TemplateStringsArray,
14+
...values: (string | { __html: string; __trusted: boolean })[]
15+
): string {
1316
let result = ''
1417

1518
// Interleave strings and escaped values
1619
for (let i = 0; i < strings.length; i++) {
1720
result += strings[i]
1821

1922
if (i < values.length) {
20-
result += escapeHtml(values[i])
23+
const value = values[i]
24+
if (value && typeof value === 'object' && value.__trusted) {
25+
result += value.__html
26+
} else if (value) {
27+
result += escapeHtml(String(value))
28+
}
2129
}
2230
}
2331

2432
return result
2533
}
2634

27-
export { escapeHtml, safeHtml }
35+
function trustedHtml(html: string) {
36+
return { __html: html, __trusted: true }
37+
}
38+
39+
export { escapeHtml, safeHtml, trustedHtml }

0 commit comments

Comments
 (0)