Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
afa0562
Initial plan
Copilot Jun 4, 2026
5839a32
Tighten DecodeEncoding to prevent header parameter injection
Copilot Jun 18, 2026
ac45139
Use SearchValues for encoded-word token/data validation
Copilot Aug 14, 2026
0bb60b5
Merge branch 'main' into copilot/better-contenttype-name-validation
mrek-msft Aug 14, 2026
3505d03
Minor fix
mrek-msft Aug 24, 2026
b7ffb80
Add tests exercising RFC 2047 spec.
mrek-msft Aug 24, 2026
51f414a
some leftover test TODOs
mrek-msft Aug 25, 2026
09759e8
Merge MimeBasePart.DecodeHeaderValue and DecodeEncoding into a single…
Copilot Aug 31, 2026
5522557
Merge copilot changes to local.
mrek-msft Sep 2, 2026
44e4bda
Modified handling ambigous encoding and improved readability of foldi…
mrek-msft Sep 2, 2026
8ff78a5
Improve test coverage and fix failing tests.
mrek-msft Sep 2, 2026
05a1d10
Fix silent truncation of malformed Q-encoded escape sequences
Copilot Sep 2, 2026
1763e49
Revert changes in QEncodedStream
mrek-msft Sep 3, 2026
8984cb8
Improve test data
mrek-msft Sep 3, 2026
b02417c
Improve test data
mrek-msft Sep 3, 2026
66e2cb7
Allow multiple ecnoded words with different encodings.
mrek-msft Sep 3, 2026
8e2fd28
Return backward compatible behaviour and promote bad encoding exceptions
mrek-msft Sep 3, 2026
ee5dedf
Fixed wrong character in length test. Added support for checking enco…
mrek-msft Sep 4, 2026
a6d8757
Fix length validation and tests.
mrek-msft Sep 4, 2026
e7e2396
Reject over-length encoded-words before charset resolution
Copilot Sep 4, 2026
b61e200
Remove useless long comment binding statements about order of checks.
mrek-msft Sep 4, 2026
6de02d0
Add test case
mrek-msft Sep 7, 2026
16995ef
Add test case
mrek-msft Sep 7, 2026
f69c6cf
Catch unsupported encoding exceptions same as format.
mrek-msft Sep 7, 2026
cc1f419
Handle unsupported charset in ContentType
mrek-msft Sep 7, 2026
4880dae
Remove unused using directive for InteropServices
mrek-msft Sep 7, 2026
7b77d0d
Fix typo in comment
mrek-msft Sep 7, 2026
15df36c
Add test case
mrek-msft Sep 7, 2026
8febeb2
Remove wrapping encoded word in quoted string (RFC 2047 prohibits this)
mrek-msft Sep 7, 2026
007fa8c
Fix assert expect/actual param order.
mrek-msft Sep 7, 2026
f3b391b
Improving RFC2047 compliance
mrek-msft Sep 7, 2026
9776cb0
Merge branch 'copilot/better-contenttype-name-validation' of https://…
mrek-msft Sep 7, 2026
f79bdfc
Wrap mime fields in quotes again despite its against RFC.
mrek-msft Sep 8, 2026
5bf01f2
Add test case
mrek-msft Sep 8, 2026
4ccac87
Fix handling " in encoded word for MIME headers
mrek-msft Sep 8, 2026
596fe00
Merge branch 'copilot/better-contenttype-name-validation' of https://…
mrek-msft Sep 8, 2026
132dc9f
Fix logical operator
mrek-msft Sep 9, 2026
1c7cd96
Enhance comments
mrek-msft Sep 9, 2026
3bb35a5
Merge 'main' into 'copilot/better-contenttype-name-validation'
mrek-msft Sep 10, 2026
b3befd4
Merge copilot enhancements
mrek-msft Sep 10, 2026
0bfac4a
Merge branch 'main' into copilot/better-contenttype-name-validation
mrek-msft Sep 30, 2026
7e1424c
Merge branch 'main' into copilot/better-contenttype-name-validation
mrek-msft Oct 12, 2026
87e4874
Remove unused import
mrek-msft Oct 12, 2026
c4f96f9
Remove bad test, fix leaking disposable
mrek-msft Oct 12, 2026
0929cd4
Add CRLF validations
mrek-msft Oct 13, 2026
bff8159
Cover ContentId by more tests.
mrek-msft Oct 13, 2026
d676b70
Cover AlternateView.BaseUri (aka ContentLocation) by more tests.
mrek-msft Oct 13, 2026
5d901b3
Improve backward compatibility of ContentDisposition validation
mrek-msft Oct 14, 2026
b962c67
Consolidate similar tests
mrek-msft Oct 15, 2026
938caf5
Improve test comment
mrek-msft Oct 15, 2026
4d5a14e
Unify similar tests and improve comments clarity
mrek-msft Oct 15, 2026
78df3cc
Remove property checked by other test
mrek-msft Oct 15, 2026
bc42b6e
Better ContainsAny
mrek-msft Oct 18, 2026
50847df
Better ContainsAny
mrek-msft Oct 21, 2026
8193d5a
Merge branch 'copilot/better-contenttype-name-validation' of https://…
mrek-msft Oct 21, 2026
5072de2
Improve exception message
mrek-msft Oct 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/libraries/System.Net.Mail/src/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,13 @@
<value>The specified port is invalid. The port must be greater than 0.</value>
</data>
<data name="MailHeaderInvalidCID" xml:space="preserve">
<value>The ContentID cannot contain a '&lt;' or '&gt;' character.</value>
<value>The ContentID cannot contain a '&lt;', '&gt;', CR, or LF character.</value>
</data>
<data name="MailHeaderInvalid" xml:space="preserve">
<value>The value cannot contain a CR or LF character.</value>
</data>
<data name="DispositionTypeInvalid" xml:space="preserve">
<value>The value cannot contain disallowed characters.</value>
</data>
<data name="MailServerResponse" xml:space="preserve">
<value>The server response was: {0}</value>
Expand Down
17 changes: 15 additions & 2 deletions src/libraries/System.Net.Mail/src/System/Net/Mail/Attachment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ public string ContentId
}
else
{
if (value.AsSpan().ContainsAny('<', '>')) // invalid chars
if (value.AsSpan().ContainsAny("<>\r\n"))
{
throw new ArgumentException(SR.MailHeaderInvalidCID, nameof(value));
}
Expand Down Expand Up @@ -268,7 +268,20 @@ internal Uri? ContentLocation

set
{
_part.ContentLocation = value == null ? null : value.IsAbsoluteUri ? value.AbsoluteUri : value.OriginalString;
if (value == null)
{
_part.ContentLocation = null;
return;
}

string valueCandidate = value.IsAbsoluteUri ? value.AbsoluteUri : value.OriginalString;

if (MailBnfHelper.HasCROrLF(valueCandidate))
{
throw new ArgumentException(SR.MailHeaderInvalid, nameof(value));
}

_part.ContentLocation = valueCandidate;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ public string DispositionType
{
ArgumentException.ThrowIfNullOrEmpty(value);

// RFC allows IANA registered token or X- extension token. We do not validate extensively.
// For backward compatibility we restrict only chars which can interfere with other part of syntax.
if (!MimeBasePart.IsAscii(value, false) || value.AsSpan().ContainsAny(';', '"'))
{
throw new ArgumentException(SR.DispositionTypeInvalid, nameof(value));
}

_isChanged = true;
_dispositionType = value;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ private static void EncodeToBuffer(string value, StringBuilder builder, bool all
{
if (MimeBasePart.IsFullyEncoded(value)) // Manually encoded elsewhere, pass through
{
builder.Append('\"').Append(value).Append('"');
builder.Append('"').Append(value).Append('"');
}
else if ((allowUnicode && !MailBnfHelper.HasCROrLF(value)) // Unicode without CL or LF's
|| MimeBasePart.IsAscii(value, false)) // Ascii
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,5 +72,38 @@ public void CreateAlternateViewFromStringEncodingNull()
AlternateView av = AlternateView.CreateAlternateViewFromString("<p>test message</p>", null, "text/html");
Assert.Equal(Text.Encoding.ASCII.BodyName, av.ContentType.CharSet);
}

[Theory]
[InlineData("", UriKind.Relative)]
[InlineData("aaaa", UriKind.Relative)]
[InlineData("aaaň", UriKind.Relative)]
[InlineData("aa-aa", UriKind.Relative)]
[InlineData("aa_aa", UriKind.Relative)]
[InlineData("aa\"aa", UriKind.Relative)]
[InlineData("aa\\aa", UriKind.Relative)]
[InlineData("aa/aa", UriKind.Relative)]
[InlineData("aa@aa", UriKind.Relative)]
[InlineData("aa; bb=cc", UriKind.Relative)]
[InlineData("C:\\foo.txt", UriKind.Absolute)]
public void ContentLocation_Validation_Valid(string uri, UriKind kind)
{
AlternateView av = AlternateView.CreateAlternateViewFromString("foo");
av.BaseUri = new Uri(uri, kind);
}

[Theory]
[InlineData("aa\raa", UriKind.Relative)]
[InlineData("aa\naa", UriKind.Relative)]
[InlineData("aa\r\naa", UriKind.Relative)]
[InlineData("aa\r\n aa", UriKind.Relative)]
public void ContentLocation_Validation_Invalid(string uri, UriKind kind)
{
AlternateView av = AlternateView.CreateAlternateViewFromString("foo");
Uri u = new Uri(uri, kind);
Assert.Throws<ArgumentException>(() =>
{
av.BaseUri = u;
});
}
}
}
205 changes: 201 additions & 4 deletions src/libraries/System.Net.Mail/tests/Functional/AttachmentTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,155 @@ public void ContentType()
[Fact]
public void NameEncoding()
{
Attachment attach = Attachment.CreateAttachmentFromString("test", "attachment-name");
Assert.Null(attach.NameEncoding);
Attachment a = new Attachment(new MemoryStream(), "myname");
Attachment a;

a = Attachment.CreateAttachmentFromString("test", "attachment-name");
Assert.Null(a.NameEncoding);

a = new Attachment(new MemoryStream(), "attachmentname");
Assert.Null(a.NameEncoding);
a = new Attachment(new MemoryStream(), "myname\u3067");

a = new Attachment(new MemoryStream(), "attachmentname\u3067");
Assert.Null(a.NameEncoding);
}

[Fact]
public void NameParsingAndEncodingDetection_Basics()
{
Attachment a;

// smoke test
a = new Attachment(new MemoryStream(), "=?ISO-8859-1?Q?attachmentname?=");
Assert.Equal("attachmentname", a.Name);
Assert.Equal(Encoding.Latin1, a.NameEncoding);

// lower case charset
a = new Attachment(new MemoryStream(), "=?iso-8859-1?Q?attachmentname?=");
Assert.Equal("attachmentname", a.Name);
Assert.Equal(Encoding.Latin1, a.NameEncoding);

// Q encoding
a = new Attachment(new MemoryStream(), "=?ISO-8859-1?Q?attachment=20name?=");
Assert.Equal("attachment name", a.Name);
Assert.Equal(Encoding.Latin1, a.NameEncoding);

// Q encoding (lowercase)
a = new Attachment(new MemoryStream(), "=?ISO-8859-1?q?attachment=20name?=");
Assert.Equal("attachment name", a.Name);
Assert.Equal(Encoding.Latin1, a.NameEncoding);

// B encoding
a = new Attachment(new MemoryStream(), "=?ISO-8859-1?B?YXR0YWNobWVudCBuYW1l?=");
Assert.Equal("attachment name", a.Name);
Assert.Equal(Encoding.Latin1, a.NameEncoding);

// B encoding (lowercase)
a = new Attachment(new MemoryStream(), "=?ISO-8859-1?b?YXR0YWNobWVudCBuYW1l?=");
Assert.Equal("attachment name", a.Name);
Assert.Equal(Encoding.Latin1, a.NameEncoding);

// space alternate
a = new Attachment(new MemoryStream(), "=?ISO-8859-1?q?attachment_name?=");
Assert.Equal("attachment name", a.Name);
Assert.Equal(Encoding.Latin1, a.NameEncoding);

// question mark alternate
a = new Attachment(new MemoryStream(), "=?ISO-8859-1?q?attachment=3Fname?=");
Assert.Equal("attachment?name", a.Name);
Assert.Equal(Encoding.Latin1, a.NameEncoding);

// underscore alternate
a = new Attachment(new MemoryStream(), "=?ISO-8859-1?q?attachment=5Fname?=");
Assert.Equal("attachment_name", a.Name);
Assert.Equal(Encoding.Latin1, a.NameEncoding);

// multiple encoded-words
a = new Attachment(new MemoryStream(), "=?ISO-8859-1?Q?attachment=20?= =?ISO-8859-1?Q?name?=");
Assert.Equal("attachment name", a.Name);
Assert.Equal(Encoding.Latin1, a.NameEncoding);

// whitespace between encoded-words
a = new Attachment(new MemoryStream(), "=?ISO-8859-1?Q?attachment=20?= =?ISO-8859-1?Q?name?=");
Assert.Equal("attachment name", a.Name);
Assert.Equal(Encoding.Latin1, a.NameEncoding);

// tab between encoded-words
a = new Attachment(new MemoryStream(), "=?ISO-8859-1?Q?attachment=20?= \t =?ISO-8859-1?Q?name?=");
Assert.Equal("attachment name", a.Name);
Assert.Equal(Encoding.Latin1, a.NameEncoding);

// new-line whitespace between encoded-words
a = new Attachment(new MemoryStream(), "=?ISO-8859-1?Q?attachment=20?=\r\n =?ISO-8859-1?Q?name?=");
Assert.Equal("attachment name", a.Name);
Assert.Equal(Encoding.Latin1, a.NameEncoding);

// multiple different encodings
a = new Attachment(new MemoryStream(), "=?ISO-8859-1?Q?attachment=20?= =?UTF-8?Q?name?=");
Assert.Equal("attachment name", a.Name);
Assert.Equal(Encoding.Latin1, a.NameEncoding);
}

[Theory]
[InlineData("=?Q?foo?=")] // missing charset
[InlineData("=?ISO-8859-1?foo?=")] // missing encoding
[InlineData("=?ISO-8859-1?qb?foo?=")] // two letter encoding
[InlineData("=?ISO-8859-1?Q?foo?")] // missing end =
[InlineData("=?ISO-8859-1?Q?foo")] // missing whole end
[InlineData("=?ISO-8859-1?Q?foo_?A?=")] // broken Q encoding, ? in text
[InlineData("=?ISO-8859-1?Q?foo bar?=")] // broken Q encoding, space in text
[InlineData("=?ISO-8859-1?Q?foo\tbar?=")] // broken Q encoding, tab in text
[InlineData("=?ISO-8859-1?Q?foo\rbar?=")] // broken Q encoding, new line (CR) in text
[InlineData("=?ISO-8859-1?Q?foo\nbar?=")] // broken Q encoding, new line (LF) in text
[InlineData("=?ISO-8859-1?Q?foo\r\nbar?=")] // broken Q encoding, new line (CRLF) in text
[InlineData("=?ISO?8859-1?Q?foo_bar?=")] // prohibited char in charset
[InlineData("=?ISO(8859-1?Q?foo_bar?=")] // prohibited char in charset
[InlineData("=?ISO<8859-1?Q?foo_bar?=")] // prohibited char in charset
[InlineData("=?ISO@8859-1?Q?foo_bar?=")] // prohibited char in charset
[InlineData("=?ISO,8859-1?Q?foo_bar?=")] // prohibited char in charset
[InlineData("=?ISO;8859-1?Q?foo_bar?=")] // prohibited char in charset
[InlineData("=?ISO:8859-1?Q?foo_bar?=")] // prohibited char in charset
[InlineData("=?ISO/8859-1?Q?foo_bar?=")] // prohibited char in charset
[InlineData("=?ISO[8859-1?Q?foo_bar?=")] // prohibited char in charset
[InlineData("=?ISO.8859-1?Q?foo_bar?=")] // prohibited char in charset
[InlineData("=?ISO=8859-1?Q?foo_bar?=")] // prohibited char in charset
[InlineData("=?ISO\"8859-1?Q?foo_bar?=")] // prohibited char in charset
[InlineData("=?ISO-8859-1?Q??foo_bar?=")] // prohibited char in encoding
[InlineData("=?ISO-8859-1?Q(?foo_bar?=")] // prohibited char in encoding
[InlineData("=?ISO-8859-1?Q<?foo_bar?=")] // prohibited char in encoding
[InlineData("=?ISO-8859-1?Q@?foo_bar?=")] // prohibited char in encoding
[InlineData("=?ISO-8859-1?Q,?foo_bar?=")] // prohibited char in encoding
[InlineData("=?ISO-8859-1?Q;?foo_bar?=")] // prohibited char in encoding
[InlineData("=?ISO-8859-1?Q:?foo_bar?=")] // prohibited char in encoding
[InlineData("=?ISO-8859-1?Q/?foo_bar?=")] // prohibited char in encoding
[InlineData("=?ISO-8859-1?Q[?foo_bar?=")] // prohibited char in encoding
[InlineData("=?ISO-8859-1?Q.?foo_bar?=")] // prohibited char in encoding
[InlineData("=?ISO-8859-1?Q=?foo_bar?=")] // prohibited char in encoding
[InlineData("=?ISO-8859-1?Q\"?foo_bar?=")] // prohibited char in encoding
public void NameParsingAndEncodingDetection_BadInputs(string attachmentName)
{
Attachment a = new Attachment(new MemoryStream(), attachmentName);
Assert.Equal(attachmentName, a.Name);
Assert.Null(a.NameEncoding);
}

[Fact]
public void NameParsingAndEncodingDetection_BadInputsThrowing()
{
// Bad Base64 encoding
Assert.Throws<FormatException>(() =>
{
using MemoryStream ms = new();
new Attachment(ms, "=?ISO-8859-1?B?YXR0YWNobWV@@@@@@@@udCBuYW1l?=");
});

// broken Q encoding, invalid hex value
Assert.Throws<FormatException>(() =>
{
using MemoryStream ms = new();
new Attachment(ms, "=?ISO-8859-1?Q?foo_=XY?=");
});
}

[Fact]
public void ContentStream()
{
Expand All @@ -136,5 +277,61 @@ public void TransferEncodingTest()
Attachment attach = Attachment.CreateAttachmentFromString("test", "attachment-name");
Assert.Equal(TransferEncoding.QuotedPrintable, attach.TransferEncoding);
}

// For backward compatibility, we allows some non-compliant with RFC2045
[Theory]
[InlineData("a")]
[InlineData("fň")]
[InlineData("foo-_:;()[]bar")]
[InlineData("foo\"bar")]
[InlineData("foo=bar")]
[InlineData("foo@bar")]
public void ContentId_Validation_Valid(string contentId)
{
Attachment attach = Attachment.CreateAttachmentFromString("test", "attachment-name");
attach.ContentId = contentId;
Assert.Equal(contentId, attach.ContentId);
}

[Theory]
[InlineData("foo<bar")]
[InlineData("foo>bar")]
[InlineData("<foobar>")]
[InlineData("foo\rbar")]
[InlineData("foo\nbar")]
[InlineData("foo\r\nbar")]
public void ContentId_Validation_Invalid(string contentId)
{
Attachment attach = Attachment.CreateAttachmentFromString("test", "attachment-name");
Assert.Throws<ArgumentException>(() =>
{
attach.ContentId = contentId;
});
}

[Fact]
public void ContentId_Validation_Empty()
{
// toggles between spcified and random generated content id

Attachment attach = Attachment.CreateAttachmentFromString("test", "attachment-name");

attach.ContentId = "foo";
Assert.Equal("foo", attach.ContentId);

attach.ContentId = "";
var guid1 = attach.ContentId;
Guid.ParseExact(guid1, "D");

attach.ContentId = "bar";
Assert.Equal("bar", attach.ContentId);

attach.ContentId = null;
var guid2 = attach.ContentId;
Guid.ParseExact(guid2, "D");

Assert.NotEqual(guid1, guid2);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -355,5 +355,59 @@ public void CultureSensitiveSetDateViaProperty_ShouldPersistToProprtyAndParamete
CultureInfo.CurrentCulture = origCulture;
}
}

[Theory]
[InlineData("=?utf-8?B?SGVsbG8=?=")]
[InlineData("=?utf-8?Q?Hello=20world?=")]
[InlineData("=?utf-8?B?SGVsbG8=?=\r\n =?utf-8?B?d29ybGQ=?=")]
[InlineData("=?utf-8?Q?foo?=?bar\"; filename=\"foo.txt")]
[InlineData("=?utf-8?B?Zm9v?=\"; filename=\"foo.txt")]
[InlineData("=?utf-8?Q?foo\"; filename=\"foo.txt?=")]
Comment thread
Copilot marked this conversation as resolved.
[InlineData("=?utf-8?Q?foo\"bar?=")]
[InlineData("=?utf-8?B?Zm9v?=trailing")]
public static void FileName_ParsingSerialization_Roundtrip(string encoded)
{
var cd = new ContentDisposition();
cd.FileName = encoded;

// Property roundtrip
Assert.Equal(encoded, cd.FileName);

// Serialize/deserialize roundtrip
string serialized = cd.ToString();
var reparsed = new ContentDisposition(serialized);
Assert.Equal(encoded, reparsed.FileName);

Assert.Equal(cd.Parameters.Count, reparsed.Parameters.Count);
}

[Theory]
[InlineData("inline")]
[InlineData("attachment")]
[InlineData("foo")]
[InlineData("foo_bar")]
[InlineData("foo@bar")]
public void ContentDisposition_Type_Validation_Valid(string dispositionType)
{
var cd = new ContentDisposition();
cd.DispositionType = dispositionType;
Assert.Equal(dispositionType, cd.DispositionType);
}

[Theory]
[InlineData("foo\rbar")]
[InlineData("foo\nbar")]
[InlineData("foo\r\nbar")]
[InlineData("foo; abc=bar")] // You can pass this to ctor, but not to property of already parsed disposition
[InlineData("foo\"bar")]
[InlineData("fň")]
public void ContentDisposition_Type_Validation_Invalid(string dispositionType)
{
var cd = new ContentDisposition();
Assert.Throws<ArgumentException>(() =>
{
cd.DispositionType = dispositionType;
});
}
}
}
Loading