High severityOSV Advisory· Published Dec 18, 2025· Updated Jan 8, 2026
Nodemailer: nodemailer: denial of service via crafted email address header
CVE-2025-14874
Description
A flaw was found in Nodemailer. This vulnerability allows a denial of service (DoS) via a crafted email address header that triggers infinite recursion in the address parser.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
nodemailernpm | < 7.0.11 | 7.0.11 |
Affected products
1- Range: v0.1, v0.1.1, v0.1.10, …
Patches
1b61b9c0cfd68fix: prevent stack overflow DoS in addressparser with deeply nested groups
2 files changed · +154 −3
lib/addressparser/index.js+19 −3 modified@@ -4,9 +4,10 @@ * Converts tokens for a single address into an address object * * @param {Array} tokens Tokens object + * @param {Number} depth Current recursion depth for nested group protection * @return {Object} Address object */ -function _handleAddress(tokens) { +function _handleAddress(tokens, depth) { let isGroup = false; let state = 'text'; let address; @@ -87,7 +88,7 @@ function _handleAddress(tokens) { // Parse group members, but flatten any nested groups (RFC 5322 doesn't allow nesting) let groupMembers = []; if (data.group.length) { - let parsedGroup = addressparser(data.group.join(',')); + let parsedGroup = addressparser(data.group.join(','), { _depth: depth + 1 }); // Flatten: if any member is itself a group, extract its members into the sequence parsedGroup.forEach(member => { if (member.group) { @@ -299,6 +300,13 @@ class Tokenizer { } } +/** + * Maximum recursion depth for parsing nested groups. + * RFC 5322 doesn't allow nested groups, so this is a safeguard against + * malicious input that could cause stack overflow. + */ +const MAX_NESTED_GROUP_DEPTH = 50; + /** * Parses structured e-mail addresses from an address field * @@ -311,10 +319,18 @@ class Tokenizer { * [{name: 'Name', address: 'address@domain'}] * * @param {String} str Address field + * @param {Object} options Optional options object + * @param {Number} options._depth Internal recursion depth counter (do not set manually) * @return {Array} An array of address objects */ function addressparser(str, options) { options = options || {}; + let depth = options._depth || 0; + + // Prevent stack overflow from deeply nested groups (DoS protection) + if (depth > MAX_NESTED_GROUP_DEPTH) { + return []; + } let tokenizer = new Tokenizer(str); let tokens = tokenizer.tokenize(); @@ -339,7 +355,7 @@ function addressparser(str, options) { } addresses.forEach(address => { - address = _handleAddress(address); + address = _handleAddress(address, depth); if (address.length) { parsedAddresses = parsedAddresses.concat(address); }
test/addressparser/addressparser-test.js+135 −0 modified@@ -774,4 +774,139 @@ describe('#addressparser', () => { assert.strictEqual(result.length, 2); }); }); + + // DoS protection tests for deeply nested groups (CVE-like vulnerability fix) + describe('Nested group DoS protection', () => { + /** + * Helper to build deeply nested group structure + * e.g., depth=3 produces: "g0: g1: g2: user@example.com;" + */ + function buildDeepGroup(depth) { + let parts = []; + for (let i = 0; i < depth; i++) { + parts.push(`g${i}:`); + } + return parts.join(' ') + ' user@example.com;'; + } + + it('should handle moderately nested groups (depth 10)', () => { + let input = buildDeepGroup(10); + let result = addressparser(input); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].name, 'g0'); + assert.ok(result[0].group); + // Should successfully extract the email from nested structure + assert.strictEqual(result[0].group.length, 1); + assert.strictEqual(result[0].group[0].address, 'user@example.com'); + }); + + it('should handle nested groups at depth limit (depth 50)', () => { + let input = buildDeepGroup(50); + let result = addressparser(input); + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].name, 'g0'); + assert.ok(result[0].group); + // At the limit, should still work + assert.strictEqual(result[0].group.length, 1); + assert.strictEqual(result[0].group[0].address, 'user@example.com'); + }); + + it('should safely truncate groups exceeding depth limit (depth 100)', () => { + let input = buildDeepGroup(100); + let result = addressparser(input); + // Should not throw stack overflow + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].name, 'g0'); + assert.ok(result[0].group); + // Group is truncated due to depth limit - members beyond limit are dropped + }); + + it('should not crash with malicious deeply nested input (depth 3000)', () => { + // This would previously cause "Maximum call stack size exceeded" + let input = buildDeepGroup(3000); + let start = Date.now(); + let result; + + // Must not throw + assert.doesNotThrow(() => { + result = addressparser(input); + }); + + let elapsed = Date.now() - start; + // Should complete quickly (under 1 second), not hang + assert.ok(elapsed < 1000, `Parser took too long: ${elapsed}ms`); + + // Should return a valid result structure + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].name, 'g0'); + assert.ok(result[0].group); + }); + + it('should not crash with extreme nesting depth (depth 10000)', () => { + let input = buildDeepGroup(10000); + let start = Date.now(); + let result; + + assert.doesNotThrow(() => { + result = addressparser(input); + }); + + let elapsed = Date.now() - start; + assert.ok(elapsed < 2000, `Parser took too long: ${elapsed}ms`); + assert.ok(Array.isArray(result)); + }); + + it('should handle multiple deeply nested groups in same input', () => { + let input = buildDeepGroup(100) + ', ' + buildDeepGroup(100); + let result; + + assert.doesNotThrow(() => { + result = addressparser(input); + }); + + // Should parse both groups + assert.strictEqual(result.length, 2); + assert.strictEqual(result[0].name, 'g0'); + assert.strictEqual(result[1].name, 'g0'); + }); + + it('should handle mixed normal and deeply nested addresses', () => { + let input = 'normal@example.com, ' + buildDeepGroup(200) + ', another@test.com'; + let result; + + assert.doesNotThrow(() => { + result = addressparser(input); + }); + + assert.strictEqual(result.length, 3); + assert.strictEqual(result[0].address, 'normal@example.com'); + assert.strictEqual(result[1].name, 'g0'); + assert.strictEqual(result[2].address, 'another@test.com'); + }); + + it('should preserve normal functionality while protecting against DoS', () => { + // Normal nested groups (allowed up to depth limit) should work correctly + let input = 'Outer: Inner: deep@example.com; ;'; + let result = addressparser(input); + + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].name, 'Outer'); + assert.ok(result[0].group); + // Inner group should be flattened + assert.strictEqual(result[0].group.length, 1); + assert.strictEqual(result[0].group[0].address, 'deep@example.com'); + }); + + it('should work correctly with flatten option on deeply nested input', () => { + let input = buildDeepGroup(100); + let result; + + assert.doesNotThrow(() => { + result = addressparser(input, { flatten: true }); + }); + + // Should return flattened array without crashing + assert.ok(Array.isArray(result)); + }); + }); });
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-rcmh-qjqh-p98vghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-14874ghsaADVISORY
- access.redhat.com/security/cve/CVE-2025-14874ghsavdb-entryx_refsource_REDHATWEB
- bugzilla.redhat.com/show_bug.cgighsaissue-trackingx_refsource_REDHATWEB
- github.com/nodemailer/nodemailer/commit/b61b9c0cfd682b6f647754ca338373b68336a150ghsaWEB
- github.com/nodemailer/nodemailer/security/advisories/GHSA-rcmh-qjqh-p98vghsaWEB
News mentions
0No linked articles in our index yet.