Skip to content

Commit df1ae78

Browse files
liveansjozkee
authored andcommitted
Merged PR 58740: [internal/release/9.0] Reject mail addresses containing CR or LF in MailAddressParser
Adds early validation in MailAddressParser.TryParseAddress to reject email addresses containing CR or LF characters, preventing SMTP header injection via crafted mail address strings. This fix has already been merged in .NET Framework and needs to ship together with it. ---- #### AI description (iteration 1) #### PR Classification This pull request is a bug fix that strengthens input validation for email addresses by rejecting any address containing CR or LF characters. #### PR Summary The changes add a validation check in the mail address parser to throw a FormatException (or return false) when CR or LF characters are detected, and update tests accordingly to enforce the new behavior. - `src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddressParser.cs`: Introduced a new check using MailBnfHelper.HasCROrLF to detect and reject mail addresses with CR or LF. - `src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressParserTest.cs`: Added tests that verify the parser throws an exception or returns false based on the throwExceptionIfFail flag. - `src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressParsingTest.cs`: Updated test cases to remove or adjust mail addresses containing CR or LF characters. <!-- GitOpsUserAgent=GitOps.Apps.Server.pullrequestcopilot -->
1 parent fc1278e commit df1ae78

4 files changed

Lines changed: 60 additions & 29 deletions

File tree

src/libraries/System.Net.Mail/src/System/Net/Mail/MailAddressParser.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,20 @@ private static bool TryParseAddress(string data, bool expectMultipleAddresses, r
6969
Debug.Assert(!string.IsNullOrEmpty(data));
7070
Debug.Assert(index >= 0 && index < data.Length, $"Index out of range: {index}, {data.Length}");
7171

72+
// Check for CR or LF characters which are not allowed in mail addresses.
73+
// Only scan on the first call (index == data.Length - 1) to avoid repeated O(n) scans
74+
// when parsing multiple addresses from the same string.
75+
if (index == data.Length - 1 && MailBnfHelper.HasCROrLF(data))
76+
{
77+
if (throwExceptionIfFail)
78+
{
79+
throw new FormatException(SR.MailAddressInvalidFormat);
80+
}
81+
82+
parseAddressInfo = default;
83+
return false;
84+
}
85+
7286
// Parsed components to be assembled as a MailAddress later
7387
string? displayName;
7488

src/libraries/System.Net.Mail/tests/Functional/SmtpClientTest.cs

Lines changed: 5 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -580,37 +580,17 @@ public void TestGssapiAuthentication()
580580

581581
[Theory]
582582
[MemberData(nameof(SendMail_MultiLineDomainLiterals_Data))]
583-
public async Task SendMail_MultiLineDomainLiterals_Disabled_Throws(string from, string to, bool asyncSend)
583+
public void SendMail_MultiLineDomainLiterals_Disabled_Throws(string from, string to)
584584
{
585-
using var server = new LoopbackSmtpServer();
586-
587-
using SmtpClient client = server.CreateClient();
588-
client.Credentials = new NetworkCredential("Foo", "Bar");
589-
590-
using var msg = new MailMessage(@from, @to, "subject", "body");
591-
592-
await Assert.ThrowsAsync<SmtpException>(async () =>
593-
{
594-
if (asyncSend)
595-
{
596-
await client.SendMailAsync(msg).WaitAsync(TimeSpan.FromSeconds(30));
597-
}
598-
else
599-
{
600-
client.Send(msg);
601-
}
602-
});
585+
Assert.Throws<FormatException>(() => new MailMessage(@from, @to, "subject", "body"));
603586
}
604587

605588
public static IEnumerable<object[]> SendMail_MultiLineDomainLiterals_Data()
606589
{
607-
foreach (bool async in new[] { true, false })
590+
foreach (string address in new[] { "foo@[\r\n bar]", "foo@[bar\r\n ]", "foo@[bar\r\n baz]" })
608591
{
609-
foreach (string address in new[] { "foo@[\r\n bar]", "foo@[bar\r\n ]", "foo@[bar\r\n baz]" })
610-
{
611-
yield return new object[] { address, "foo@example.com", async };
612-
yield return new object[] { "foo@example.com", address, async };
613-
}
592+
yield return new object[] { address, "foo@example.com" };
593+
yield return new object[] { "foo@example.com", address };
614594
}
615595
}
616596
}

src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressParserTest.cs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,5 +559,39 @@ public void ParseAddresses_WithManyComplexAddresses_ShouldReadCorrectly()
559559
Assert.Equal("\" asciin;,oqu o.tesws \"", result[6].User);
560560
Assert.Equal("this.test.this", result[6].Host);
561561
}
562+
563+
[Theory]
564+
[InlineData("test\r@example.com")]
565+
[InlineData("test\n@example.com")]
566+
[InlineData("test\r\n@example.com")]
567+
[InlineData("Display\r\nName <test@example.com>")]
568+
[InlineData("test@example\r.com")]
569+
[InlineData("test@example\n.com")]
570+
[InlineData("test@example.com\r\n")]
571+
public void TryParseAddress_WithCROrLF_ShouldThrow(string address)
572+
{
573+
Assert.Throws<FormatException>(() => MailAddressParser.TryParseAddress(address, out _, throwExceptionIfFail: true));
574+
}
575+
576+
[Theory]
577+
[InlineData("test\r@example.com")]
578+
[InlineData("test\n@example.com")]
579+
[InlineData("test\r\n@example.com")]
580+
[InlineData("Display\r\nName <test@example.com>")]
581+
[InlineData("test@example\r.com")]
582+
[InlineData("test@example\n.com")]
583+
[InlineData("test@example.com\r\n")]
584+
public void TryParseAddress_WithCROrLF_ShouldReturnFalse(string address)
585+
{
586+
Assert.False(MailAddressParser.TryParseAddress(address, out _, throwExceptionIfFail: false));
587+
}
588+
589+
[Fact]
590+
public void ParseMultipleAddresses_WithCROrLF_ShouldThrow()
591+
{
592+
Assert.Throws<FormatException>(() => MailAddressParser.ParseMultipleAddresses("a@b.com, test\r\n@example.com"));
593+
Assert.Throws<FormatException>(() => MailAddressParser.ParseMultipleAddresses("test\n@example.com, a@b.com"));
594+
Assert.Throws<FormatException>(() => MailAddressParser.ParseMultipleAddresses("a@b.com, c@d.com, e\r@f.com"));
595+
}
562596
}
563597
}

src/libraries/System.Net.Mail/tests/Unit/MailAddressTests/MailAddressParsingTest.cs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,7 @@ public static IEnumerable<object[]> GetValidEmailTestData()
3636
yield return new object[] { "testuser@[mail.com]" };
3737
yield return new object[] { "testuser@[ mail.com] " };
3838
yield return new object[] { "testuser@[mail.com ]" };
39-
yield return new object[] { "testuser@[mail.com \r\n ]" };
40-
yield return new object[] { "testuser@[ \r\n mail.com]" };
39+
yield return new object[] { "testuser@[mail.com ]" };
4140
yield return new object[] { "testuser@[ mail.com]" };
4241
yield return new object[] { "testuser <testuser@mail.com>" };
4342
yield return new object[] { "Test\u3044\u3069 User <testUser1@NCLMailTest.com>" };
@@ -90,8 +89,8 @@ public static IEnumerable<object[]> GetValidEmailTestData()
9089
yield return new object[] { "\"disp \f lay\" <\"testsome\"@NCLMailTest.com>" };
9190
yield return new object[] { "\"EscapedUnicode \\\u3044\\\u3069 display\" <testUser1@NCLMailTest.com>" };
9291
yield return new object[] { "(Unicode \u3044 Comment) <\"testsome\"@NCLMailTest.com>" };
93-
yield return new object[] { "\"display \r\n name\" <\"folding\"@domain.com>" };
94-
yield return new object[] { "\"test\r\n test\"@mail.com" };
92+
yield return new object[] { "\"display name\" <\"folding\"@domain.com>" };
93+
yield return new object[] { "\"test test\"@mail.com" };
9594
// Email Address Internationalization (EAI)
9695
yield return new object[] { "UnicodeUserName \"Test\u3044\u3069\"@NCLMailTest.com" };
9796
yield return new object[] { "<\"EscapedUnicode \\\u3044\\\u3069 User\"@NCLMailTest.com>" };
@@ -127,6 +126,10 @@ public static IEnumerable<object[]> GetInvalidEmailTestData()
127126
yield return new object[] { "Bob \"display\" <user@host>" };
128127
yield return new object[] { "testuser@[mail.com \r ]" };
129128
yield return new object[] { "testuser@[mail.com \n ]" };
129+
yield return new object[] { "testuser@[mail.com \r\n ]" };
130+
yield return new object[] { "testuser@[ \r\n mail.com]" };
131+
yield return new object[] { "\"display \r\n name\" <\"folding\"@domain.com>" };
132+
yield return new object[] { "\"test\r\n test\"@mail.com" };
130133
yield return new object[] { "testuser@[mail\u3069.com]" }; // No unicode allowed in square brackets
131134
yield return new object[] { "invalid@unicode\uD800.com" }; // D800 is a high surrogate
132135
yield return new object[] { "invalid@unicode\uD800.com" }; // D800 is a high surrogate

0 commit comments

Comments
 (0)