diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddress.cs b/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddress.cs index a4cc338453b8ed..1b26b83856921d 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddress.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddress.cs @@ -141,6 +141,20 @@ private static bool TryParse([NotNullWhen(true)] string? address, string? displa return false; } + // Reject CR/LF in caller-supplied display names. Such characters would corrupt + // the encoded mail header when the address is serialized into a message + // (e.g. From/To headers written by SmtpClient). + if (MailBnfHelper.HasCROrLF(displayName)) + { + if (throwExceptionIfFail) + { + throw new FormatException(SR.MailAddressInvalidFormat); + } + + parsedData = default; + return false; + } + if (displayName.Length >= 2 && displayName.StartsWith('\"') && displayName.EndsWith('\"')) { // Peel bounding quotes, they'll get re-added later. diff --git a/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddressParser.cs b/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddressParser.cs index c3479598be7466..ef9e3bb6a30995 100644 --- a/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddressParser.cs +++ b/src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddressParser.cs @@ -69,6 +69,23 @@ private static bool TryParseAddress(string data, bool expectMultipleAddresses, r Debug.Assert(!string.IsNullOrEmpty(data)); Debug.Assert(index >= 0 && index < data.Length, $"Index out of range: {index}, {data.Length}"); + // Reverse parsing starts from the last character of the input in the public entry points. + // ParseMultipleAddresses decrements index after each parsed address, so only this initial + // reverse-parse call performs the O(n) CR/LF scan for the full input string. + if (index == data.Length - 1) + { + if (MailBnfHelper.HasCROrLF(data)) + { + if (throwExceptionIfFail) + { + throw new FormatException(SR.MailAddressInvalidFormat); + } + + parseAddressInfo = default; + return false; + } + } + // Parsed components to be assembled as a MailAddress later string? displayName; diff --git a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientSendMailTest.cs b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientSendMailTest.cs index 00da56ee4f646f..95a1beb9c68d3f 100644 --- a/src/libraries/System.Net.Mail/tests/Functional/SmtpClientSendMailTest.cs +++ b/src/libraries/System.Net.Mail/tests/Functional/SmtpClientSendMailTest.cs @@ -147,13 +147,9 @@ public async Task TestMultipleMailDelivery() [Theory] [MemberData(nameof(SendMail_MultiLineDomainLiterals_Data))] - public async Task MultiLineDomainLiterals_Disabled_Throws(string from, string to) + public void MultiLineDomainLiterals_Disabled_Throws(string from, string to) { - Smtp.Credentials = new NetworkCredential("Foo", "Bar"); - - using var msg = new MailMessage(@from, @to, "subject", "body"); - - await SendMail(msg); + Assert.Throws(() => new MailMessage(@from, @to, "subject", "body")); } public static IEnumerable SendMail_MultiLineDomainLiterals_Data() diff --git a/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressDisplayNameTest.cs b/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressDisplayNameTest.cs index 198ff9c24aad8a..54e40c1a06ad83 100644 --- a/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressDisplayNameTest.cs +++ b/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressDisplayNameTest.cs @@ -61,5 +61,21 @@ public void MailAddress_Ctor_FullString_Succeeds(string address, string displayN Assert.Equal(expectedDisplayName ?? displayName, mailAddress.DisplayName); Assert.Equal(expectedToString, mailAddress.ToString()); } + + [Theory] + [InlineData("Display\rName")] + [InlineData("Display\nName")] + [InlineData("Display\r\nName")] + [InlineData("DisplayName\r")] + [InlineData("DisplayName\n")] + [InlineData("DisplayName\r\n")] + [InlineData("\rDisplayName")] + [InlineData("\nDisplayName")] + [InlineData("\"Display\r\nName\"")] + public void MailAddress_Ctor_DisplayNameContainsCRLF_Throws(string displayName) + { + Assert.Throws(() => new MailAddress(Address, displayName)); + Assert.False(MailAddress.TryCreate(Address, displayName, out _)); + } } } diff --git a/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressParserTest.cs b/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressParserTest.cs index b52a60ee14ef2f..1648320bc5a6fb 100644 --- a/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressParserTest.cs +++ b/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressParserTest.cs @@ -533,5 +533,39 @@ public void ParseAddresses_WithManyComplexAddresses_ShouldReadCorrectly() Assert.Equal("\" asciin;,oqu o.tesws \"", result[5].User); Assert.Equal("this.test.this", result[5].Host); } + + [Theory] + [InlineData("test\r@example.com")] + [InlineData("test\n@example.com")] + [InlineData("test\r\n@example.com")] + [InlineData("Display\r\nName ")] + [InlineData("test@example\r.com")] + [InlineData("test@example\n.com")] + [InlineData("test@example.com\r\n")] + public void TryParseAddress_WithCROrLF_ShouldThrow(string address) + { + Assert.Throws(() => MailAddressParser.TryParseAddress(address, out _, throwExceptionIfFail: true)); + } + + [Theory] + [InlineData("test\r@example.com")] + [InlineData("test\n@example.com")] + [InlineData("test\r\n@example.com")] + [InlineData("Display\r\nName ")] + [InlineData("test@example\r.com")] + [InlineData("test@example\n.com")] + [InlineData("test@example.com\r\n")] + public void TryParseAddress_WithCROrLF_ShouldReturnFalse(string address) + { + Assert.False(MailAddressParser.TryParseAddress(address, out _, throwExceptionIfFail: false)); + } + + [Fact] + public void ParseMultipleAddresses_WithCROrLF_ShouldThrow() + { + Assert.Throws(() => MailAddressParser.ParseMultipleAddresses("a@b.com, test\r\n@example.com")); + Assert.Throws(() => MailAddressParser.ParseMultipleAddresses("test\n@example.com, a@b.com")); + Assert.Throws(() => MailAddressParser.ParseMultipleAddresses("a@b.com, c@d.com, e\r@f.com")); + } } } diff --git a/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressParsingTest.cs b/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressParsingTest.cs index b0436e77476e8c..18e7203df1db28 100644 --- a/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressParsingTest.cs +++ b/src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressParsingTest.cs @@ -36,8 +36,7 @@ public static IEnumerable GetValidEmailTestData() yield return new object[] { "testuser@[mail.com]" }; yield return new object[] { "testuser@[ mail.com] " }; yield return new object[] { "testuser@[mail.com ]" }; - yield return new object[] { "testuser@[mail.com \r\n ]" }; - yield return new object[] { "testuser@[ \r\n mail.com]" }; + yield return new object[] { "testuser@[mail.com ]" }; yield return new object[] { "testuser@[ mail.com]" }; yield return new object[] { "testuser " }; yield return new object[] { "Test\u3044\u3069 User " }; @@ -88,8 +87,8 @@ public static IEnumerable GetValidEmailTestData() yield return new object[] { "\"disp \f lay\" <\"testsome\"@NCLMailTest.com>" }; yield return new object[] { "\"EscapedUnicode \\\u3044\\\u3069 display\" " }; yield return new object[] { "(Unicode \u3044 Comment) <\"testsome\"@NCLMailTest.com>" }; - yield return new object[] { "\"display \r\n name\" <\"folding\"@domain.com>" }; - yield return new object[] { "\"test\r\n test\"@mail.com" }; + yield return new object[] { "\"display name\" <\"folding\"@domain.com>" }; + yield return new object[] { "\"test test\"@mail.com" }; // Email Address Internationalization (EAI) yield return new object[] { "UnicodeUserName \"Test\u3044\u3069\"@NCLMailTest.com" }; yield return new object[] { "<\"EscapedUnicode \\\u3044\\\u3069 User\"@NCLMailTest.com>" }; @@ -125,9 +124,12 @@ public static IEnumerable GetInvalidEmailTestData() yield return new object[] { "Bob \"display\" " }; yield return new object[] { "testuser@[mail.com \r ]" }; yield return new object[] { "testuser@[mail.com \n ]" }; + yield return new object[] { "testuser@[mail.com \r\n ]" }; + yield return new object[] { "testuser@[ \r\n mail.com]" }; + yield return new object[] { "\"display \r\n name\" <\"folding\"@domain.com>" }; + yield return new object[] { "\"test\r\n test\"@mail.com" }; yield return new object[] { "testuser@[mail\u3069.com]" }; // No unicode allowed in square brackets yield return new object[] { "invalid@unicode\uD800.com" }; // D800 is a high surrogate - yield return new object[] { "invalid@unicode\uD800.com" }; // D800 is a high surrogate yield return new object[] { "invalid\uD800@unicode.com" }; // D800 is a high surrogate yield return new object[] { "\uD800 invalid@unicode.com" }; // D800 is a high surrogate yield return new object[] { null };