Skip to content

Commit 08b800c

Browse files
committed
fix: prevent RFC 2047 encoded-word address fabrication in decodeAddresses
Guard recursive decode-and-reparse to require angle-bracket-delimited addresses before re-parsing decoded content. Detect and reject RFC 2047 encoded-words in addr-spec that produce invalid addresses after decoding.
1 parent 05db224 commit 08b800c

2 files changed

Lines changed: 115 additions & 11 deletions

File tree

lib/mail-parser.js

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -526,18 +526,30 @@ class MailParser extends Transform {
526526
address.name = (address.name || '').toString().trim();
527527

528528
if (!address.address && /^(=\?([^?]+)\?[Bb]\?[^?]*\?=)(\s*=\?([^?]+)\?[Bb]\?[^?]*\?=)*$/.test(address.name) && !processedAddress.has(address)) {
529-
let parsed = addressparser(this.libmime.decodeWords(address.name));
530-
if (parsed.length) {
531-
parsed.forEach(entry => {
532-
processedAddress.add(entry);
533-
addresses.push(entry);
534-
});
535-
}
529+
let decoded = this.libmime.decodeWords(address.name);
530+
// Security fix: only re-parse if decoded text contains angle-bracket-delimited
531+
// addresses (e.g. "Name <user@domain>"). Bare decoded text like "attacker@evil.com"
532+
// must not be re-interpreted as an address -- RFC 2047 Section 5 prohibits
533+
// encoded-words in addr-spec, and re-parsing fabricates addresses not present
534+
// in the original header.
535+
if (/<[^<>]+@[^<>]+>/.test(decoded)) {
536+
let parsed = addressparser(decoded);
537+
if (parsed.length) {
538+
parsed.forEach(entry => {
539+
processedAddress.add(entry);
540+
addresses.push(entry);
541+
});
542+
}
536543

537-
// remove current element
538-
addresses.splice(i, 1);
539-
i--;
540-
continue;
544+
// remove current element
545+
addresses.splice(i, 1);
546+
i--;
547+
continue;
548+
} else {
549+
// Treat decoded content as display name only
550+
address.name = decoded;
551+
continue;
552+
}
541553
}
542554

543555
if (address.name) {
@@ -547,6 +559,21 @@ class MailParser extends Transform {
547559
//ignore, keep as is
548560
}
549561
}
562+
// Security fix: detect RFC 2047 encoded-words in the address field.
563+
// Per RFC 2047 Section 5, encoded-words MUST NOT appear in addr-spec.
564+
// If found, decode and validate; reject if result is not a simple local@domain.
565+
if (address.address && /[=]\?[^?]+\?[BbQq]\?[^?]*\?[=]/.test(address.address)) {
566+
try {
567+
let decodedAddr = this.libmime.decodeWords(address.address);
568+
if (/^[^\s@]+@[^\s@]+$/.test(decodedAddr) && !/[=]\?/.test(decodedAddr)) {
569+
address.address = decodedAddr;
570+
} else {
571+
address.address = '';
572+
}
573+
} catch (E) {
574+
address.address = '';
575+
}
576+
}
550577
if (/@xn--/.test(address.address)) {
551578
try {
552579
address.address =

test/address-security-test.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
'use strict';
2+
3+
const simpleParser = require('../lib/simple-parser');
4+
5+
module.exports['Should not fabricate address from bare Base64 encoded email'] = test => {
6+
// attacker@evil.com encoded as Base64
7+
let source = Buffer.from(`From: =?utf-8?b?YXR0YWNrZXJAZXZpbC5jb20=?=\r\nTo: victim@example.com\r\n\r\ntest`);
8+
9+
simpleParser(source, {}, (err, mail) => {
10+
test.ifError(err);
11+
test.ok(mail);
12+
13+
test.equal(mail.from.value[0].address, '', 'Bare encoded email must not become an address');
14+
test.equal(mail.from.value[0].name, 'attacker@evil.com', 'Decoded text should be treated as display name');
15+
16+
test.done();
17+
});
18+
};
19+
20+
module.exports['Should still parse legitimate encoded Name <email> addresses'] = test => {
21+
// "Rydel" <Rydelkalot@17guagua.com> encoded as Base64
22+
let source = Buffer.from(`From: test@example.com\r\nTo: =?utf-8?B?IlJ5ZGVsIiA8UnlkZWxrYWxvdEAxN2d1YWd1YS5jb20+?=, andris@tr.ee\r\n\r\ntest`);
23+
24+
simpleParser(source, {}, (err, mail) => {
25+
test.ifError(err);
26+
test.ok(mail);
27+
28+
let toAddresses = mail.to.value;
29+
let rydel = toAddresses.find(a => a.address === 'Rydelkalot@17guagua.com');
30+
test.ok(rydel, 'Legitimate encoded address with angle brackets should still be parsed');
31+
test.equal(rydel.name, 'Rydel');
32+
33+
test.done();
34+
});
35+
};
36+
37+
module.exports['Should decode and reject encoded-words in addr-spec that produce invalid addresses'] = test => {
38+
// =40 decodes to @, producing @attacker.com@microsoft.com (two @ signs)
39+
let source = Buffer.from(`From: =?utf-8?q?=40attacker.com?=@microsoft.com\r\nTo: victim@example.com\r\n\r\ntest`);
40+
41+
simpleParser(source, {}, (err, mail) => {
42+
test.ifError(err);
43+
test.ok(mail);
44+
45+
test.equal(mail.from.value[0].address, '', 'Encoded-word in addr-spec producing invalid address should be cleared');
46+
47+
test.done();
48+
});
49+
};
50+
51+
module.exports['Should not touch normal addresses'] = test => {
52+
let source = Buffer.from(`From: "Sender" <sender@example.com>\r\nTo: recipient@example.com\r\n\r\ntest`);
53+
54+
simpleParser(source, {}, (err, mail) => {
55+
test.ifError(err);
56+
test.ok(mail);
57+
58+
test.equal(mail.from.value[0].address, 'sender@example.com');
59+
test.equal(mail.from.value[0].name, 'Sender');
60+
test.equal(mail.to.value[0].address, 'recipient@example.com');
61+
62+
test.done();
63+
});
64+
};
65+
66+
module.exports['Should not touch percent-hack addresses'] = test => {
67+
let source = Buffer.from(`From: user%attacker.com@microsoft.com\r\nTo: victim@example.com\r\n\r\ntest`);
68+
69+
simpleParser(source, {}, (err, mail) => {
70+
test.ifError(err);
71+
test.ok(mail);
72+
73+
test.equal(mail.from.value[0].address, 'user%attacker.com@microsoft.com', 'Percent-hack addresses should pass through as-is');
74+
75+
test.done();
76+
});
77+
};

0 commit comments

Comments
 (0)