VYPR
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.

PackageAffected versionsPatched versions
nodemailernpm
< 7.0.117.0.11

Affected products

1

Patches

1
b61b9c0cfd68

fix: prevent stack overflow DoS in addressparser with deeply nested groups

https://github.com/nodemailer/nodemailerAndris ReinmanNov 26, 2025via ghsa
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

News mentions

0

No linked articles in our index yet.