@mantiq/mail 0.5.23 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -17,6 +17,27 @@ export class SmtpTransport implements MailTransport {
|
|
|
17
17
|
this.config = config
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Security: validate that a value contains no CR, LF, or null bytes
|
|
22
|
+
* to prevent SMTP header/command injection.
|
|
23
|
+
*/
|
|
24
|
+
private assertNoInjection(value: string, field: string): void {
|
|
25
|
+
if (/[\r\n\0]/.test(value)) {
|
|
26
|
+
throw new MailError(
|
|
27
|
+
`SMTP injection detected in ${field}: value contains CR, LF, or null bytes`,
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Security: sanitize a subject line by stripping CR/LF characters
|
|
34
|
+
* to prevent SMTP header injection. Per RFC 2047, subjects should
|
|
35
|
+
* not contain bare newlines.
|
|
36
|
+
*/
|
|
37
|
+
private sanitizeSubject(subject: string): string {
|
|
38
|
+
return subject.replace(/[\r\n\0]/g, '')
|
|
39
|
+
}
|
|
40
|
+
|
|
20
41
|
async send(message: Message): Promise<{ id: string }> {
|
|
21
42
|
const { host, port, username, password, encryption = 'none' } = this.config
|
|
22
43
|
|
|
@@ -133,13 +154,19 @@ export class SmtpTransport implements MailTransport {
|
|
|
133
154
|
}
|
|
134
155
|
}
|
|
135
156
|
|
|
157
|
+
// Security: validate all email addresses against SMTP injection
|
|
158
|
+
this.assertNoInjection(message.from.address, 'from address')
|
|
159
|
+
const allRecipients = [...message.to, ...message.cc, ...message.bcc]
|
|
160
|
+
for (const recipient of allRecipients) {
|
|
161
|
+
this.assertNoInjection(recipient.address, 'recipient address')
|
|
162
|
+
}
|
|
163
|
+
|
|
136
164
|
// MAIL FROM
|
|
137
165
|
socket.write(`MAIL FROM:<${message.from.address}>\r\n`)
|
|
138
166
|
const mailFrom = await waitForResponse()
|
|
139
167
|
this.assertCode(mailFrom, 250, 'MAIL FROM')
|
|
140
168
|
|
|
141
169
|
// RCPT TO — all recipients
|
|
142
|
-
const allRecipients = [...message.to, ...message.cc, ...message.bcc]
|
|
143
170
|
for (const recipient of allRecipients) {
|
|
144
171
|
socket.write(`RCPT TO:<${recipient.address}>\r\n`)
|
|
145
172
|
const rcpt = await waitForResponse()
|
|
@@ -236,11 +263,14 @@ export class SmtpTransport implements MailTransport {
|
|
|
236
263
|
lines.push(`Reply-To: ${Message.formatAddresses(message.replyTo)}`)
|
|
237
264
|
}
|
|
238
265
|
|
|
239
|
-
|
|
266
|
+
// Security: sanitize subject to prevent SMTP header injection
|
|
267
|
+
lines.push(`Subject: ${this.sanitizeSubject(message.subject)}`)
|
|
240
268
|
lines.push(`MIME-Version: 1.0`)
|
|
241
269
|
|
|
242
|
-
// Custom headers
|
|
270
|
+
// Custom headers — validate against SMTP header injection
|
|
243
271
|
for (const [key, value] of Object.entries(message.headers)) {
|
|
272
|
+
this.assertNoInjection(key, 'custom header name')
|
|
273
|
+
this.assertNoInjection(value, `custom header '${key}'`)
|
|
244
274
|
lines.push(`${key}: ${value}`)
|
|
245
275
|
}
|
|
246
276
|
|
|
@@ -148,9 +148,19 @@ function sanitizeLinkUrl(url: string): string | null {
|
|
|
148
148
|
return trimmed
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Security: escape HTML special characters to prevent XSS when
|
|
153
|
+
* user-provided text is interpolated into HTML output.
|
|
154
|
+
*/
|
|
155
|
+
function escapeHtml(s: string): string {
|
|
156
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''')
|
|
157
|
+
}
|
|
158
|
+
|
|
151
159
|
/** Process inline markdown: bold, italic, code, links */
|
|
152
160
|
function inlineFormatting(text: string): string {
|
|
153
|
-
|
|
161
|
+
// Security: escape HTML in the raw text first to prevent XSS injection,
|
|
162
|
+
// then apply markdown formatting on the escaped output.
|
|
163
|
+
return escapeHtml(text)
|
|
154
164
|
// Bold
|
|
155
165
|
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
|
|
156
166
|
// Italic
|
|
@@ -164,6 +174,7 @@ function inlineFormatting(text: string): string {
|
|
|
164
174
|
// Security: strip dangerous link, render as plain text only
|
|
165
175
|
return label
|
|
166
176
|
}
|
|
177
|
+
// Security: label is already HTML-escaped from the escapeHtml() call above
|
|
167
178
|
return `<a href="${safeUrl}" class="email-link" style="color:#10b981;text-decoration:underline;">${label}</a>`
|
|
168
179
|
})
|
|
169
180
|
}
|