TL;DR
v6.9.16
After reading Splitting the Email Atom research, I set out to find a zero-day bug in Nodemailer for one of my CTF challenges. I discovered that Nodemailer was not correctly parsing certain valid email addresses. Consider this example:
"test@0xhexr4.htb"@google.com
According to RFC 5322, this is a valid email address. The portion "test@0xhexr4.htb" is a quoted local-part, which allows the @ character. The domain is google.com. Thus, the mailbox is "test@0xhexr4.htb" at the domain google.com.
"test@0xhexr4.htb"
@
google.com
However, Nodemailer’s parser attempts to identify the address by splitting at the first @ found outside of a properly recognized quoted local-part. Here’s a snippet (taken from Nodemailer’s code) showing how it separates tokens into different lists:
data = { address: [], comment: [], group: [], text: [] };
When Nodemailer encounters "test@0xhexr4.htb"@google.com, it correctly identifies the quoted string "test@0xhexr4.htb" as the local-part, but then treats the subsequent @google.com segment as non-quoted “text.” It fails to recognize that google.com is the domain. Instead, it might parse the input as:
@google.com
[{ name: "@google.com", address: "test@0xhexr4.htb" }]
This results in Nodemailer interpreting "test@0xhexr4.htb" as the entire mailbox and @google.com as a display name, effectively losing the actual domain information (google.com).
I reported this to the Nodemailer team, hoping it might be treated as a security issue, since incorrect parsing could lead to emails being sent to the wrong domain. However, they considered it a regular bug rather than a security flaw. They fixed this issue in version 6.9.16 without providing credit.
Accepting their stance, I moved on and found another problem. I found out that it is still vulnerable to the same kind of issue. For example, consider this payload:
"test@email.htb x"@interstellar.htb
It exhibits similar behavior, and the email is sent to the test mailbox on email.htb rather than interstellar.htb.
test
email.htb
interstellar.htb
If we parse the following email using a library such as Email-Addresses
Welcome to Node.js v23.3.0. Type ".help" for more information. > addrs = require("email-addresses") [Function: parse5322] { parseOneAddress: [Function: parseOneAddressSimple], parseAddressList: [Function: parseAddressListSimple], parseFrom: [Function: parseFromSimple], parseSender: [Function: parseSenderSimple], parseReplyTo: [Function: parseReplyToSimple] } > addrs.parseOneAddress(`"test@email.htb x"@interstellar.htb`) { parts: { name: null, address: { name: 'addr-spec', tokens: '"test@email.htb x"@interstellar.htb', semantic: 'test@email.htb x@interstellar.htb', children: [Array] }, local: { name: 'local-part', tokens: '"test@email.htb x"', semantic: 'test@email.htb x', children: [Array] }, domain: { name: 'domain', tokens: 'interstellar.htb', semantic: 'interstellar.htb', children: [Array] }, comments: [] }, type: 'mailbox', name: null, address: 'test@email.htb x@interstellar.htb', local: 'test@email.htb x', domain: 'interstellar.htb', comments: '', groupName: null } >
The domain is "interstellar.htb". I haven't reported it because it’s considered a normal bug :)
The latest version as for now v6.9.16 is still vulnerable to this. I showcased this second bug in the University CTF 2024 Intergalactic Bounty web challenge. Despite the Nodemailer team’s response to the first bug, I hope that highlighting these issues will help raise awareness about how subtle parsing errors can lead to unexpected behavior.