Technologyglobalverified · 90%

Nodemailer jsonTransport bypasses disableFileAccess and disableUrlAccess during message normalization

When
Where
Global (internet)
Category
cyber_advisory · npm

### Summary Nodemailer's `disableFileAccess` and `disableUrlAccess` options are intended to prevent message content and attachments from reading local files or fetching URLs. The normal MIME streaming path enforces those options in `MimeNode._getStream()`. However, `jsonTransport` serializes messages by calling `mail.normalize()`, which resolves `html`, `text`, alternatives, calendar events, and attachments through `shared.resolveContent()` before MIME generation. `shared.resolveContent()` reads local files and fetches HTTP(S) URLs directly, without receiving or checking `disableFileAccess` or `disableUrlAccess`. As a result, applications that use `jsonTransport` as a safe serializer or queue payload generator while relying on `disableFileAccess` / `disableUrlAccess` can still be made to read local files into the generated JSON output or make outbound HTTP requests when an attacker controls message content fields such as attachment `path` or `text.href`. The same missing-enforcement root cause is also reachable before normal streaming when `attachDataUrls` causes `_convertDataImages()` to call `mail.resolveContent(mail.data, 'html', ...)`; this should be fixed with the same access-control check. ### Details Source-to-sink evidence: - `lib/nodemailer.js:42-45` selects `JSONTransport` when `createTransport({ jsonTransport: true, ... })` is used. - `lib/mailer/mail-message.js:34-39` copies transport-level `disableFileAccess` and `disableUrlAccess` options into `mail.data`. - `lib/json-transport/index.js:52-76` serializes mail by calling `mail.normalize((err, data) => ...)`. - `lib/mailer/mail-message.js:46-135` implements `resolveAll()` and calls `shared.resolveContent(...args, ...)` for `html`, `text`, `watchHtml`, `amp`, `icalEvent`, alternatives, and attachments. - `lib/shared/index.js:506-562` implements `resolveContent()`. - `lib/shared/index.js:540-541` fetches HTTP(S) content with `nmfetch(content.path || content.href)`. - `lib/shared/index.js:549-550` reads local files with `fs.createReadStream(content.path)`. - `shared.resolveContent()` does not check `disableFileAccess` or `disableUrlAccess` and does not receive those flags. Control path showing intended enforcement: - `lib/mail-composer/index.js:358-359`, `lib/mail-composer/index.js:367-368`, and sibling child-node creation paths pass `disableUrlAccess` and `disableFileAccess` into `MimeNode`. - `lib/mime-node/index.js:51-52` stores those flags. - `lib/mime-node/index.js:984-995` rejects file paths with `EFILEACCESS` when `disableFileAccess` is set. - `lib/mime-node/index.js:998-1009` rejects URLs with `EURLACCESS` when `disableUrlAccess` is set. - `test/mail-composer/mail-composer-test.js:1028-1044` includes a normal MIME-streaming test that expects file access to be blocked when `disableFileAccess: true`. Additional same-root-cause variant: - `lib/mailer/index.js:406-434` implements `_convertDataImages()` for `attachDataUrls`. - `lib/mailer/index.js:407-410` calls `mail.resolveContent(mail.data, 'html', ...)` when `attachDataUrls` is enabled and `mail.data.html` is present. - Because `mail.resolveContent()` delegates to `shared.resolveContent()` at `lib/mailer/mail-message.js:42-44`, an object-form `html: { path: ... }` or `html: { href: ... }` can be resolved before the later MIME streaming enforcement sees the content. - This variant requires `attachDataUrls` to be enabled, so the main reportable default/common path is `jsonTransport`; both should be fixed by enforcing access flags inside the pre-resolution helper or passing policy into it. Default/common exposure evidence: - `jsonTransport` is a shipped runtime transport selected by public `createTransport` options. - `test/json-transport/json-transport-test.js:9-83` demonstrates that `jsonTransport` intentionally resolves file-backed `html` and attachments into JSON output. - `disableFileAccess` and `disableUrlAccess` are documented by code and tests as security controls and are copied from transport options into message data for all transports. - The bypass does not require test-only code, external infrastructure, unsupported configuration, or maintainer-only APIs. False-positive screening and negative controls: - The local PoC used the same `disableFileAccess: true` and `disableUrlAccess: true` transport options for both `jsonTransport` and normal `streamTransport` controls. - `jsonTransport` read the temporary local fixture file and embedded the content in JSON despite `disableFileAccess: true`. - `streamTransport` with the same attachment and `disableFileAccess: true` rejected with `EFILEACCESS`. - `jsonTransport` fetched a local HTTP listener despite `disableUrlAccess: true`. - `streamTransport` with the same URL and `disableUrlAccess: true` rejected with `EURLACCESS`. - The local URL proof used only `127.0.0.1` and did not contact external infrastructure. Affected version evidence and uncertainty: - Confirmed vulnerable: `nodemailer` 8.0.8 at commit `15138a84c543c20aa399218534cdbbfa2ea1ce55`. - Git history shows `jsonTransport` has existed since commit `d78b63b` (2017-02-09, "Added test for json transport"), and `disableFileAccess` appears in historical setup commit `6218b8d` (2017-01-31), but older versions were not dynamically tested during this audit. - Affected range is therefore recorded as unknown beyond the confirmed current version. - No patched version was identified in this checkout. Severity rationale: - AV: The vulnerable library path is typically reached through an application-level message submission or rendering/queueing feature. - AC: A single message field using `path` or `href` triggers the bypass when `jsonTransport` is used. - PR: Conservative assumption that the attacker is a lower-privileged user of an application that accepts partially user-controlled message objects. Some deployments may expose this unauthenticated, but that was not assumed. - UI: No user interaction is required after the application accepts the message object. - S: The impact remains in the embedding application/library security scope. - C: Local file contents can be copied into the generated JSON output when the application later stores, logs, returns, or forwards that JSON. - I: The attacker can induce outbound HTTP requests to attacker-chosen or internal URLs from the application host when URL access was intended to be disabled. - A: No availability impact was demonstrated; the PoC used bounded local files and a localhost listener only. Final self-review: - Reproduction evidence was generated locally from this checkout using only a temporary file under the OS temp directory and a local `127.0.0.1` HTTP listener. - The PoC included positive proof for file read and URL fetch, plus negative controls showing normal `streamTransport` rejects the same inputs with `EFILEACCESS` and `EURLACCESS`. - The proof is non-destructive, performs no external network traffic, and deletes its temporary fixture. - Reachability, package exposure, policy-enforcement bypass, same-root-cause variant, and false-positive controls were checked as described above. - The affected range is not overclaimed; only the current tested version is confirmed vulnerable. ### PoC From a clean checkout of `nodemailer` at commit `15138a84c543c20aa399218534cdbbfa2ea1ce55`, run: ```bash node <<'NODE' 'use strict'; const fs = require('fs'); const os = require('os'); const path = require('path'); const http = require('http'); const nodemailer = require('./'); const marker = 'NM_JSON_BYPASS_' + Date.now(); const fixture = path.join(os.tmpdir(), 'nodemailer-json-bypass-' + process.pid + '.txt'); fs.writeFileSync(fixture, marker); function sendMail(transport, data) { return new Promise((resolve, reject) => transport.sendMail(data, (err, info) => err ? reject(err) : resolve(info))); } (async () => { const jsonTransport = nodemailer.createTransport({ jsonTransport: true, disableFileAccess: true, disableUrlAccess: true }); const jsonInfo = await sendMail(json

Sources

Defaxon links out to the original reporting and never republishes article text.

Correlated events

Computed by the Defaxon correlation engine — linked by shared actors, co-location, and temporal proximity. Scored hypotheses, never causal claims.

← Back to the live map