How Message-Level Raw Option Bypass Happens in Node.js Nodemailer and How to Fix It
The Problem with "Safe" Security Flags That Aren't
When developers reach for disableFileAccess and disableUrlAccess in Nodemailer, they're doing the right thing: they're explicitly telling the library not to resolve local file paths or make outbound HTTP requests while composing email messages. This is a critical safeguard for any application that lets user-influenced data flow into outgoing emails.
The problem? In Nodemailer 6.x, those flags had a blind spot — and that blind spot was the message-level raw option.
In a project's pnpm-lock.yaml, Trivy flagged nodemailer@6.10.1 as carrying GHSA-p6gq-j5cr-w38f: a high-severity vulnerability where the raw option completely bypasses both disableFileAccess and disableUrlAccess, opening the door to arbitrary local file reads and full-response SSRF embedded directly in delivered email messages.
The Vulnerability Explained
What raw Does — and What It Skipped
Nodemailer's raw option is a power-user feature. Instead of letting Nodemailer compose a MIME message from structured fields (to, from, html, attachments, etc.), you hand it a pre-built raw RFC 2822 message string or stream. Nodemailer is then supposed to deliver it as-is.
The security controls disableFileAccess and disableUrlAccess are enforced during the message composition phase — when Nodemailer processes structured fields and resolves any embedded references (like path: '/etc/passwd' in an attachment, or href: 'http://internal-service/' in an HTML body). Here's an example of how those flags are typically set:
// Intended to be safe — but wasn't in 6.x when using `raw`
const transporter = nodemailer.createTransport({
host: 'smtp.example.com',
port: 587,
disableFileAccess: true, // Should block local file reads
disableUrlAccess: true, // Should block HTTP fetches
});
await transporter.sendMail({
from: 'app@example.com',
to: user.email,
raw: userControlledRawMessage, // ← bypasses both flags entirely in 6.x
});
In Nodemailer 6.10.1, when you use the raw field, the library short-circuits the normal composition pipeline and delivers the content without running it through the access-control checks that disableFileAccess and disableUrlAccess enforce. The flags are set, the developer believes the application is protected, but the raw path is a completely separate code route that never consults those flags.
The Concrete Attack Scenario
Consider an application that:
- Accepts user-provided email content (e.g., a "send this report to a colleague" feature)
- Passes that content through to Nodemailer using the
rawoption for formatting flexibility - Has
disableFileAccess: trueanddisableUrlAccess: trueset on the transporter, believing the application is hardened
An attacker who can influence the value passed to raw — even partially, through a template injection, a deserialization flaw, or a misconfigured API endpoint — can craft a raw MIME message that references:
- Local files: e.g., embedding a reference to
/etc/passwd,/app/.env, or any secrets file readable by the Node.js process - Internal URLs: e.g., pointing to
http://169.254.169.254/latest/meta-data/(AWS IMDS), internal microservices, or other SSRF targets
Because the raw path skips the access-control checks, Nodemailer resolves those references and includes their full contents in the delivered email message. The attacker receives the exfiltrated data in their inbox.
This is not a theoretical primitive. It is a direct, high-severity data exfiltration and SSRF path that bypasses what developers reasonably believe is a working security control.
Why This Is Especially Dangerous
The danger is compounded by a false sense of security. Developers who set disableFileAccess: true have taken an affirmative action to protect their application. They've read the documentation, they've done the right thing — but in 6.x, that action provides no protection when raw is in play. Automated exploit-development tooling increasingly chains exactly these kinds of "protection that doesn't protect" primitives with other weaknesses to build end-to-end exploits.
The Fix
Upgrading from 6.10.1 to 9.0.1
The fix is a direct version upgrade. In package.json, the specifier changed from ^6.9.14 to ^9.0.1:
- "nodemailer": "^6.9.14",
+ "nodemailer": "^9.0.1",
And in pnpm-lock.yaml, the resolved version moved from 6.10.1 to 9.0.1:
nodemailer:
- specifier: ^6.9.14
- version: 6.10.1
+ specifier: ^9.0.1
+ version: 9.0.1
The integrity hash also changed, confirming a completely different package artifact is now being installed:
- nodemailer@6.10.1:
- resolution: {integrity: sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==}
+ nodemailer@9.0.1:
+ resolution: {integrity: sha512-Gwv8SQewT616ZM/URn0H54b8PWo/Wum7md3EW2aWy1lO27+WZCX+Xyak3J+NlmHUJDh5ME+uesJUDRbR3Ye8Bw==}
What Changed in Nodemailer 9.x
In Nodemailer 9.0.1, the raw option is no longer a bypass route. The access-control enforcement for disableFileAccess and disableUrlAccess is applied consistently across all message composition paths, including raw. This means:
- If
disableFileAccess: trueis set, file references inrawmessages are blocked - If
disableUrlAccess: trueis set, URL fetches triggered byrawmessage content are blocked - The security model developers expected is now the security model they actually get
Why Two Files Were Changed
Both package.json and pnpm-lock.yaml required changes because pnpm uses a deterministic lockfile. Changing only package.json would update the version range specifier but leave the old resolved version (6.10.1) pinned in the lockfile. Both files must be updated together to ensure the correct version is actually installed in all environments, including CI/CD pipelines and production deployments.
Prevention & Best Practices
1. Treat Dependency Security Flags as Contracts — and Verify Them
When a library exposes security configuration like disableFileAccess, treat it as a contract that needs verification. Write integration tests that confirm the flag actually prevents the behavior you expect, especially after upgrades or when using non-standard API options like raw.
2. Never Pass Unvalidated User Input to raw
Even with the fix in place, the raw option should be treated with the same caution as eval(). If any part of a raw MIME message is influenced by user input, validate and sanitize it rigorously. Prefer structured Nodemailer fields (html, text, attachments) over raw whenever possible, as they are processed through Nodemailer's full security pipeline.
3. Use Dependency Scanning in CI/CD
This vulnerability was detected by Trivy scanning pnpm-lock.yaml. Integrate a dependency vulnerability scanner (Trivy, Snyk, npm audit, or similar) into your CI/CD pipeline so that known CVEs and GHSA advisories are caught before they reach production.
# Example: Trivy filesystem scan
trivy fs --scanners vuln .
# Example: pnpm audit
pnpm audit --audit-level high
4. Pin Major Versions Carefully
The jump from ^6.9.14 to ^9.0.1 is a major version bump (6 → 9). Major version bumps may include breaking changes. Review the Nodemailer changelog before upgrading in production, and run your full test suite to confirm behavior is preserved. In this case, the PR notes that "the change only tightens handling of untrusted input and leaves valid inputs unaffected."
5. Apply the Principle of Least Privilege to Email Composition
The Node.js process sending emails should run with the minimum filesystem permissions necessary. Even if a file-read bypass were exploited, a process that can only read a narrow set of files limits the blast radius.
Relevant Standards
- OWASP A10:2021 — Server-Side Request Forgery (SSRF): https://owasp.org/Top10/A10_2021-Server-Side_Request_Forgery_%28SSRF%29/
- CWE-284: Improper Access Control: The root cause — a security control that exists but is not consistently enforced across all code paths.
Key Takeaways
disableFileAccessanddisableUrlAccessin Nodemailer 6.x are not enforced when therawmessage option is used — setting them does not protect you ifrawis in your code path.- The
rawoption is a high-risk API surface: any user-influenced data reachingrawin Nodemailer 6.x is a direct path to file read and SSRF, bypassing all configured security controls. - The
pnpm-lock.yamllockfile must be updated alongsidepackage.json— updating only the version range inpackage.jsonleaves the vulnerable6.10.1pinned in the lockfile and installed in practice. - False security is worse than acknowledged insecurity: the presence of
disableFileAccess: truein code using vulnerable Nodemailer versions may delay detection of the actual attack surface. - Trivy's dependency scanning caught this in
pnpm-lock.yaml— scanning lockfiles (not justpackage.json) is essential because lockfiles contain the actual resolved versions installed in production.
How Orbis AppSec Detected This
- Source: User-influenced content passed to the
rawfield in atransporter.sendMail()call — any code path where external or user-provided data reaches therawmessage option in Nodemailer 6.x. - Sink: Nodemailer's internal message composition pipeline in
nodemailer@6.10.1, specifically therawoption handler that resolves file paths and URLs without consultingdisableFileAccessordisableUrlAccess. - Missing control: The
disableFileAccessanddisableUrlAccessflags were not applied to therawmessage composition code path in Nodemailer 6.x, creating an unguarded route to filesystem access and outbound HTTP requests. - CWE: CWE-284 — Improper Access Control (a security control exists but is inconsistently enforced).
- Fix: Upgraded
nodemailerfrom6.10.1to9.0.1in bothpackage.jsonandpnpm-lock.yaml, where therawoption is subject to the same access restrictions as all other message fields.
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
GHSA-p6gq-j5cr-w38f is a reminder that security controls are only as strong as their consistent enforcement. Nodemailer's disableFileAccess and disableUrlAccess flags are the right tool for the job — but in versions prior to 9.0.1, the raw option silently voided both of them. For any application composing emails with user-influenced content, this was a direct path to arbitrary file exfiltration and SSRF.
The fix is simple: upgrade to Nodemailer 9.0.1. The lesson is broader: when you rely on a library's security configuration, verify that it applies uniformly across all the API surfaces you use — not just the ones documented in the "safe usage" examples. Automated dependency scanning, as demonstrated here with Trivy detecting this in pnpm-lock.yaml, is an essential layer in catching these gaps before attackers do.