nodemailer 8.0.8 → 8.0.10
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/CHANGELOG.md +14 -0
- package/SECURITY.md +59 -0
- package/lib/mailer/index.js +32 -24
- package/lib/mailer/mail-message.js +25 -19
- package/lib/shared/index.js +34 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
|
+
## [8.0.10](https://github.com/nodemailer/nodemailer/compare/v8.0.9...v8.0.10) (2026-05-29)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* fall back to lower-severity handler when custom logger lacks a level method ([6d849df](https://github.com/nodemailer/nodemailer/commit/6d849df59a56184b48844ed10b5fb7b8e9f74634))
|
|
9
|
+
|
|
10
|
+
## [8.0.9](https://github.com/nodemailer/nodemailer/compare/v8.0.8...v8.0.9) (2026-05-26)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* two pending security advisories (jsonTransport access bypass, List-* CRLF injection) ([#1820](https://github.com/nodemailer/nodemailer/issues/1820)) ([5f69497](https://github.com/nodemailer/nodemailer/commit/5f694977da2e0e13dc947037566e8e689a01217e))
|
|
16
|
+
|
|
3
17
|
## [8.0.8](https://github.com/nodemailer/nodemailer/compare/v8.0.7...v8.0.8) (2026-05-23)
|
|
4
18
|
|
|
5
19
|
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Security Policy
|
|
2
|
+
|
|
3
|
+
Nodemailer is a widely deployed, zero-dependency e-mail library. We take security
|
|
4
|
+
reports seriously and aim to respond quickly.
|
|
5
|
+
|
|
6
|
+
## Supported Versions
|
|
7
|
+
|
|
8
|
+
Security fixes are released only against the latest major version. We do not
|
|
9
|
+
backport patches to older majors — upgrading to the current release line is the
|
|
10
|
+
supported way to receive security updates.
|
|
11
|
+
|
|
12
|
+
| Version | Supported |
|
|
13
|
+
| ------- | ------------------ |
|
|
14
|
+
| 8.x | :white_check_mark: |
|
|
15
|
+
| < 8.0 | :x: |
|
|
16
|
+
|
|
17
|
+
If you are on an older major, please upgrade. See the migration notes at
|
|
18
|
+
<https://nodemailer.com/> before updating.
|
|
19
|
+
|
|
20
|
+
## Reporting a Vulnerability
|
|
21
|
+
|
|
22
|
+
**Please do not report security vulnerabilities through public GitHub issues,
|
|
23
|
+
pull requests, or discussions.**
|
|
24
|
+
|
|
25
|
+
Report privately through one of the following channels:
|
|
26
|
+
|
|
27
|
+
1. **GitHub Security Advisories (preferred).** Open a private report at
|
|
28
|
+
<https://github.com/nodemailer/nodemailer/security/advisories/new>. This keeps
|
|
29
|
+
the discussion private until a fix is published and lets us coordinate a CVE
|
|
30
|
+
and credit you.
|
|
31
|
+
2. **Email.** Send details to **andris@reinman.eu** (the contact listed in
|
|
32
|
+
[`SECURITY.txt`](SECURITY.txt)). Encrypt sensitive details if possible.
|
|
33
|
+
|
|
34
|
+
When reporting, please include as much of the following as you can:
|
|
35
|
+
|
|
36
|
+
- The affected version(s) and environment (Node.js version, OS).
|
|
37
|
+
- The component involved (e.g. SMTP connection, address parsing, MIME/header
|
|
38
|
+
generation, DKIM).
|
|
39
|
+
- A clear description of the issue and its impact (e.g. header/SMTP command
|
|
40
|
+
injection, information disclosure, DoS).
|
|
41
|
+
- A minimal proof of concept or reproduction steps.
|
|
42
|
+
- Any suggested remediation, if you have one.
|
|
43
|
+
|
|
44
|
+
Nodemailer is maintained by a single person, so there is no guaranteed response
|
|
45
|
+
time — sometimes reports are handled within hours, sometimes they take longer.
|
|
46
|
+
Accepted issues are fixed in a new release and coordinated through a GitHub
|
|
47
|
+
Security Advisory / CVE, and reporters who wish to be named are credited.
|
|
48
|
+
|
|
49
|
+
## Scope
|
|
50
|
+
|
|
51
|
+
In scope: the `nodemailer` package source in this repository — message and MIME
|
|
52
|
+
generation, SMTP/LMTP client behaviour, address parsing, header handling, DKIM
|
|
53
|
+
signing, and the bundled transports.
|
|
54
|
+
|
|
55
|
+
Out of scope: vulnerabilities in your own application code, misconfiguration of
|
|
56
|
+
your mail server or credentials, social-engineering reports, and issues in
|
|
57
|
+
third-party services Nodemailer connects to.
|
|
58
|
+
|
|
59
|
+
Thank you for helping keep Nodemailer and its users safe.
|
package/lib/mailer/index.js
CHANGED
|
@@ -407,31 +407,39 @@ class Mail extends EventEmitter {
|
|
|
407
407
|
if ((!this.options.attachDataUrls && !mail.data.attachDataUrls) || !mail.data.html) {
|
|
408
408
|
return callback();
|
|
409
409
|
}
|
|
410
|
-
mail.resolveContent(
|
|
411
|
-
|
|
412
|
-
|
|
410
|
+
mail.resolveContent(
|
|
411
|
+
mail.data,
|
|
412
|
+
'html',
|
|
413
|
+
{ disableFileAccess: mail.data.disableFileAccess, disableUrlAccess: mail.data.disableUrlAccess },
|
|
414
|
+
(err, html) => {
|
|
415
|
+
if (err) {
|
|
416
|
+
return callback(err);
|
|
417
|
+
}
|
|
418
|
+
let cidCounter = 0;
|
|
419
|
+
html = (html || '')
|
|
420
|
+
.toString()
|
|
421
|
+
.replace(
|
|
422
|
+
/(<img\b[^<>]{0,1024} src\s{0,20}=[\s"']{0,20})(data:([^;]+);[^"'>\s]+)/gi,
|
|
423
|
+
(match, prefix, dataUri, mimeType) => {
|
|
424
|
+
const cid = crypto.randomBytes(10).toString('hex') + '@localhost';
|
|
425
|
+
if (!mail.data.attachments) {
|
|
426
|
+
mail.data.attachments = [];
|
|
427
|
+
}
|
|
428
|
+
if (!Array.isArray(mail.data.attachments)) {
|
|
429
|
+
mail.data.attachments = [].concat(mail.data.attachments || []);
|
|
430
|
+
}
|
|
431
|
+
mail.data.attachments.push({
|
|
432
|
+
path: dataUri,
|
|
433
|
+
cid,
|
|
434
|
+
filename: 'image-' + ++cidCounter + '.' + mimeTypes.detectExtension(mimeType)
|
|
435
|
+
});
|
|
436
|
+
return prefix + 'cid:' + cid;
|
|
437
|
+
}
|
|
438
|
+
);
|
|
439
|
+
mail.data.html = html;
|
|
440
|
+
callback();
|
|
413
441
|
}
|
|
414
|
-
|
|
415
|
-
html = (html || '')
|
|
416
|
-
.toString()
|
|
417
|
-
.replace(/(<img\b[^<>]{0,1024} src\s{0,20}=[\s"']{0,20})(data:([^;]+);[^"'>\s]+)/gi, (match, prefix, dataUri, mimeType) => {
|
|
418
|
-
const cid = crypto.randomBytes(10).toString('hex') + '@localhost';
|
|
419
|
-
if (!mail.data.attachments) {
|
|
420
|
-
mail.data.attachments = [];
|
|
421
|
-
}
|
|
422
|
-
if (!Array.isArray(mail.data.attachments)) {
|
|
423
|
-
mail.data.attachments = [].concat(mail.data.attachments || []);
|
|
424
|
-
}
|
|
425
|
-
mail.data.attachments.push({
|
|
426
|
-
path: dataUri,
|
|
427
|
-
cid,
|
|
428
|
-
filename: 'image-' + ++cidCounter + '.' + mimeTypes.detectExtension(mimeType)
|
|
429
|
-
});
|
|
430
|
-
return prefix + 'cid:' + cid;
|
|
431
|
-
});
|
|
432
|
-
mail.data.html = html;
|
|
433
|
-
callback();
|
|
434
|
-
});
|
|
442
|
+
);
|
|
435
443
|
}
|
|
436
444
|
|
|
437
445
|
set(key, value) {
|
|
@@ -111,25 +111,29 @@ class MailMessage {
|
|
|
111
111
|
if (!args[0] || !args[0][args[1]]) {
|
|
112
112
|
return resolveNext();
|
|
113
113
|
}
|
|
114
|
-
shared.resolveContent(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
114
|
+
shared.resolveContent(
|
|
115
|
+
...args,
|
|
116
|
+
{ disableFileAccess: this.data.disableFileAccess, disableUrlAccess: this.data.disableUrlAccess },
|
|
117
|
+
(err, value) => {
|
|
118
|
+
if (err) {
|
|
119
|
+
return callback(err);
|
|
120
|
+
}
|
|
118
121
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
122
|
+
const node = {
|
|
123
|
+
content: value
|
|
124
|
+
};
|
|
125
|
+
if (args[0][args[1]] && typeof args[0][args[1]] === 'object' && !Buffer.isBuffer(args[0][args[1]])) {
|
|
126
|
+
Object.keys(args[0][args[1]]).forEach(key => {
|
|
127
|
+
if (!(key in node) && !['content', 'path', 'href', 'raw'].includes(key)) {
|
|
128
|
+
node[key] = args[0][args[1]][key];
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
129
132
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
+
args[0][args[1]] = node;
|
|
134
|
+
resolveNext();
|
|
135
|
+
}
|
|
136
|
+
);
|
|
133
137
|
};
|
|
134
138
|
|
|
135
139
|
setImmediate(() => resolveNext());
|
|
@@ -269,7 +273,8 @@ class MailMessage {
|
|
|
269
273
|
if (value && value.url) {
|
|
270
274
|
if (key.toLowerCase().trim() === 'id') {
|
|
271
275
|
// List-ID: "comment" <domain>
|
|
272
|
-
|
|
276
|
+
// strip CR/LF so a comment can't inject extra header lines
|
|
277
|
+
let comment = (value.comment || '').toString().replace(/\r?\n|\r/g, ' ');
|
|
273
278
|
if (mimeFuncs.isPlainText(comment)) {
|
|
274
279
|
comment = '"' + comment + '"';
|
|
275
280
|
} else {
|
|
@@ -280,7 +285,8 @@ class MailMessage {
|
|
|
280
285
|
}
|
|
281
286
|
|
|
282
287
|
// List-*: <http://domain> (comment)
|
|
283
|
-
|
|
288
|
+
// strip CR/LF so a comment can't inject extra header lines
|
|
289
|
+
let comment = (value.comment || '').toString().replace(/\r?\n|\r/g, ' ');
|
|
284
290
|
if (!mimeFuncs.isPlainText(comment)) {
|
|
285
291
|
comment = mimeFuncs.encodeWord(comment);
|
|
286
292
|
}
|
package/lib/shared/index.js
CHANGED
|
@@ -6,6 +6,7 @@ const urllib = require('url');
|
|
|
6
6
|
const util = require('util');
|
|
7
7
|
const fs = require('fs');
|
|
8
8
|
const nmfetch = require('../fetch');
|
|
9
|
+
const errors = require('../errors');
|
|
9
10
|
const dns = require('dns');
|
|
10
11
|
const net = require('net');
|
|
11
12
|
const os = require('os');
|
|
@@ -366,7 +367,16 @@ module.exports._logFunc = (logger, level, defaults, data, message, ...args) => {
|
|
|
366
367
|
const entry = Object.assign({}, defaults || {}, data || {});
|
|
367
368
|
delete entry.level;
|
|
368
369
|
|
|
369
|
-
|
|
370
|
+
let logLevel = level;
|
|
371
|
+
if (typeof logger[logLevel] !== 'function') {
|
|
372
|
+
// Provided logger does not implement this level. Fall back to a
|
|
373
|
+
// lower-severity handler instead of throwing.
|
|
374
|
+
logLevel = ['info', 'debug', 'log', 'trace', 'warn', 'error'].find(name => typeof logger[name] === 'function');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (logLevel) {
|
|
378
|
+
logger[logLevel](entry, message, ...args);
|
|
379
|
+
}
|
|
370
380
|
};
|
|
371
381
|
|
|
372
382
|
/**
|
|
@@ -501,9 +511,17 @@ module.exports.parseDataURI = uri => {
|
|
|
501
511
|
*
|
|
502
512
|
* @param {Object} data An object or an Array you want to resolve an element for
|
|
503
513
|
* @param {String|Number} key Property name or an Array index
|
|
514
|
+
* @param {Object} [options] Optional access policy: { disableFileAccess, disableUrlAccess }
|
|
504
515
|
* @param {Function} callback Callback function with (err, value)
|
|
505
516
|
*/
|
|
506
|
-
module.exports.resolveContent = (data, key, callback) => {
|
|
517
|
+
module.exports.resolveContent = (data, key, options, callback) => {
|
|
518
|
+
// options is optional; support the legacy resolveContent(data, key, callback) signature
|
|
519
|
+
if (!callback && typeof options === 'function') {
|
|
520
|
+
callback = options;
|
|
521
|
+
options = false;
|
|
522
|
+
}
|
|
523
|
+
options = options || {};
|
|
524
|
+
|
|
507
525
|
let promise;
|
|
508
526
|
|
|
509
527
|
if (!callback) {
|
|
@@ -538,6 +556,13 @@ module.exports.resolveContent = (data, key, callback) => {
|
|
|
538
556
|
callback(null, value);
|
|
539
557
|
});
|
|
540
558
|
} else if (/^https?:\/\//i.test(content.path || content.href)) {
|
|
559
|
+
if (options.disableUrlAccess) {
|
|
560
|
+
return setImmediate(() => {
|
|
561
|
+
const err = new Error('Url access rejected for ' + (content.path || content.href));
|
|
562
|
+
err.code = errors.EURLACCESS;
|
|
563
|
+
callback(err);
|
|
564
|
+
});
|
|
565
|
+
}
|
|
541
566
|
return resolveStream(nmfetch(content.path || content.href), callback);
|
|
542
567
|
} else if (/^data:/i.test(content.path || content.href)) {
|
|
543
568
|
const parsedDataUri = module.exports.parseDataURI(content.path || content.href);
|
|
@@ -547,6 +572,13 @@ module.exports.resolveContent = (data, key, callback) => {
|
|
|
547
572
|
}
|
|
548
573
|
return callback(null, parsedDataUri.data);
|
|
549
574
|
} else if (content.path) {
|
|
575
|
+
if (options.disableFileAccess) {
|
|
576
|
+
return setImmediate(() => {
|
|
577
|
+
const err = new Error('File access rejected for ' + content.path);
|
|
578
|
+
err.code = errors.EFILEACCESS;
|
|
579
|
+
callback(err);
|
|
580
|
+
});
|
|
581
|
+
}
|
|
550
582
|
return resolveStream(fs.createReadStream(content.path), callback);
|
|
551
583
|
}
|
|
552
584
|
}
|