Skip to content

[release/2.3] Merge from internal #62614

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions eng/PatchConfig.props
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ Later on, this will be checked using this condition:
</PropertyGroup>
<PropertyGroup Condition=" '$(VersionPrefix)' == '2.3.4' ">
<PackagesInPatch>
Microsoft.Net.Http.Headers;
</PackagesInPatch>
</PropertyGroup>
</Project>
28 changes: 25 additions & 3 deletions src/Http/Headers/src/CookieHeaderParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,17 @@ public sealed override bool TryParseValue(StringSegment value, ref int index, ou
CookieHeaderValue result = null;
if (!CookieHeaderValue.TryGetCookieLength(value, ref current, out result))
{
var separatorIndex = value.IndexOf(';', current);
if (separatorIndex > 0)
{
// Skip the invalid values and keep trying.
index = separatorIndex;
}
else
{
// No more separators, so we're done.
index = value.Length;
}
return false;
}

Expand All @@ -55,6 +66,17 @@ public sealed override bool TryParseValue(StringSegment value, ref int index, ou
// If we support multiple values and we've not reached the end of the string, then we must have a separator.
if ((separatorFound && !SupportsMultipleValues) || (!separatorFound && (current < value.Length)))
{
var separatorIndex = value.IndexOf(';', current);
if (separatorIndex > 0)
{
// Skip the invalid values and keep trying.
index = separatorIndex;
}
else
{
// No more separators, so we're done.
index = value.Length;
}
return false;
}

Expand All @@ -71,7 +93,7 @@ private static int GetNextNonEmptyOrWhitespaceIndex(StringSegment input, int sta
separatorFound = false;
var current = startIndex + HttpRuleParser.GetWhitespaceLength(input, startIndex);

if ((current == input.Length) || (input[current] != ',' && input[current] != ';'))
if ((current == input.Length) || (input[current] != ';'))
{
return current;
}
Expand All @@ -84,8 +106,8 @@ private static int GetNextNonEmptyOrWhitespaceIndex(StringSegment input, int sta

if (skipEmptyValues)
{
// Most headers only split on ',', but cookies primarily split on ';'
while ((current < input.Length) && ((input[current] == ',') || (input[current] == ';')))
// Cookies are split on ';'
while ((current < input.Length) && (input[current] == ';'))
{
current++; // skip delimiter.
current = current + HttpRuleParser.GetWhitespaceLength(input, current);
Expand Down
12 changes: 12 additions & 0 deletions src/Http/Headers/src/CookieHeaderValue.cs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,18 @@ public static bool TryParseStrictList(IList<string> inputs, out IList<CookieHead
return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues);
}

/*
* https://www.rfc-editor.org/rfc/rfc6265#section-4.1.1
* cookie-pair = cookie-name "=" cookie-value
* cookie-name = token
* token = 1*<any CHAR except CTLs or separators>
separators = "(" | ")" | "<" | ">" | "@"
| "," | ";" | ":" | "\" | <">
| "/" | "[" | "]" | "?" | "="
| "{" | "}" | SP | HT
CTL = <any US-ASCII control character
(octets 0 - 31) and DEL (127)>
*/
// name=value; name="value"
internal static bool TryGetCookieLength(StringSegment input, ref int offset, out CookieHeaderValue parsedValue)
{
Expand Down
38 changes: 25 additions & 13 deletions src/Http/Headers/test/CookieHeaderValueTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public static TheoryData<string> InvalidCookieValues
}
}

public static TheoryData<IList<CookieHeaderValue>, string[]> ListOfCookieHeaderDataSet
public static TheoryData<IList<CookieHeaderValue>, string[]> ListOfStrictCookieHeaderDataSet
{
get
{
Expand All @@ -99,19 +99,30 @@ public static TheoryData<IList<CookieHeaderValue>, string[]> ListOfCookieHeaderD

dataset.Add(new[] { header1 }.ToList(), new[] { string1 });
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, string1 });
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, null, "", " ", ";", " , ", string1 });
dataset.Add(new[] { header2 }.ToList(), new[] { string2 });
dataset.Add(new[] { header1, header2 }.ToList(), new[] { string1, string2 });
dataset.Add(new[] { header1, header2 }.ToList(), new[] { string1 + ", " + string2 });
dataset.Add(new[] { header2, header1 }.ToList(), new[] { string2 + "; " + string1 });
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string1, string2, string3, string4 });
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, string3, string4) });
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(";", string1, string2, string3, string4) });

return dataset;
}
}

public static TheoryData<IList<CookieHeaderValue>, string[]> ListOfCookieHeaderDataSet
{
get
{
var header1 = new CookieHeaderValue("name1", "n1=v1&n2=v2&n3=v3");
var string1 = "name1=n1=v1&n2=v2&n3=v3";

var dataset = new TheoryData<IList<CookieHeaderValue>, string[]>();
dataset.Concat(ListOfStrictCookieHeaderDataSet);
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, null, "", " ", ";", " , ", string1 });
return dataset;
}
}

public static TheoryData<IList<CookieHeaderValue>, string[]> ListWithInvalidCookieHeaderDataSet
{
get
Expand All @@ -132,18 +143,19 @@ public static TheoryData<IList<CookieHeaderValue>, string[]> ListWithInvalidCook
dataset.Add(new[] { header1 }.ToList(), new[] { validString1, invalidString1 });
dataset.Add(new[] { header1 }.ToList(), new[] { validString1, null, "", " ", ";", " , ", invalidString1 });
dataset.Add(new[] { header1 }.ToList(), new[] { invalidString1, null, "", " ", ";", " , ", validString1 });
dataset.Add(new[] { header1 }.ToList(), new[] { validString1 + ", " + invalidString1 });
dataset.Add(new[] { header2 }.ToList(), new[] { invalidString1 + ", " + validString2 });
dataset.Add(null, new[] { validString1 + ", " });
dataset.Add(null, new[] { invalidString1 + ", " + validString2 });
dataset.Add(new[] { header1 }.ToList(), new[] { invalidString1 + "; " + validString1 });
dataset.Add(new[] { header2 }.ToList(), new[] { validString2 + "; " + invalidString1 });
dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { invalidString1, validString1, validString2, validString3 });
dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { invalidString1, validString1, validString2, validString3 });
dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { validString1, invalidString1, validString2, validString3 });
dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { validString1, validString2, invalidString1, validString3 });
dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { validString1, validString2, validString3, invalidString1 });
dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(",", invalidString1, validString1, validString2, validString3) });
dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(",", validString1, invalidString1, validString2, validString3) });
dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(",", validString1, validString2, invalidString1, validString3) });
dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(",", validString1, validString2, validString3, invalidString1) });
dataset.Add(null, new[] { string.Join(",", invalidString1, validString1, validString2, validString3) });
dataset.Add(null, new[] { string.Join(",", validString1, invalidString1, validString2, validString3) });
dataset.Add(null, new[] { string.Join(",", validString1, validString2, invalidString1, validString3) });
dataset.Add(null, new[] { string.Join(",", validString1, validString2, validString3, invalidString1) });
dataset.Add(null, new[] { string.Join(",", validString1, validString2, validString3) });
dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(";", invalidString1, validString1, validString2, validString3) });
dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(";", validString1, invalidString1, validString2, validString3) });
dataset.Add(new[] { header1, header2, header3 }.ToList(), new[] { string.Join(";", validString1, validString2, invalidString1, validString3) });
Expand Down Expand Up @@ -253,7 +265,7 @@ public void CookieHeaderValue_ParseList_AcceptsValidValues(IList<CookieHeaderVal
}

[Theory]
[MemberData(nameof(ListOfCookieHeaderDataSet))]
[MemberData(nameof(ListOfStrictCookieHeaderDataSet))]
public void CookieHeaderValue_ParseStrictList_AcceptsValidValues(IList<CookieHeaderValue> cookies, string[] input)
{
var results = CookieHeaderValue.ParseStrictList(input);
Expand All @@ -272,7 +284,7 @@ public void CookieHeaderValue_TryParseList_AcceptsValidValues(IList<CookieHeader
}

[Theory]
[MemberData(nameof(ListOfCookieHeaderDataSet))]
[MemberData(nameof(ListOfStrictCookieHeaderDataSet))]
public void CookieHeaderValue_TryParseStrictList_AcceptsValidValues(IList<CookieHeaderValue> cookies, string[] input)
{
var result = CookieHeaderValue.TryParseStrictList(input, out var results);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Http" />
<Reference Include="Microsoft.Extensions.DependencyInjection" />
<ProjectReference Include="..\..\Headers\src\Microsoft.Net.Http.Headers.csproj" />
</ItemGroup>

</Project>
40 changes: 40 additions & 0 deletions src/Http/Http/test/RequestCookiesCollectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,45 @@ public void AppContextSwitchUnEscapesKeyValues(string input, string expectedKey,
Assert.Equal(expectedKey, cookies.Keys.Single());
Assert.Equal(expectedValue, cookies[expectedKey]);
}

[Theory]
[InlineData(",", null)]
[InlineData(";", null)]
[InlineData("er=dd,cc,bb", null)]
[InlineData("er=dd,err=cc,errr=bb", null)]
[InlineData("errorcookie=dd,:(\"sa;", null)]
[InlineData("s;", null)]
[InlineData("a@a=a;", null)]
[InlineData("a@ a=a;", null)]
[InlineData("a a=a;", null)]
[InlineData(",a=a;", null)]
[InlineData(",a=a", null)]
[InlineData("a=a;,b=b", new []{ "a" })] // valid cookie followed by invalid cookie
[InlineData(",a=a;b=b", new[] { "b" })] // invalid cookie followed by valid cookie
public void ParseInvalidCookies(string cookieToParse, string[] expectedCookieValues)
{
var cookies = RequestCookieCollection.Parse(new StringValues(new[] { cookieToParse }));

if (expectedCookieValues == null)
{
Assert.Equal(0, cookies.Count);
return;
}

Assert.Equal(expectedCookieValues.Length, cookies.Count);
for (int i = 0; i < expectedCookieValues.Length; i++)
{
var value = expectedCookieValues[i];
Assert.Equal(value, cookies.ElementAt(i).Value);
}
}

[Fact]
public void ParseManyCookies()
{
var cookies = RequestCookieCollection.Parse(new StringValues(new[] { "a=a", "b=b", "c=c", "d=d", "e=e", "f=f", "g=g", "h=h", "i=i", "j=j", "k=k", "l=l" }));

Assert.Equal(12, cookies.Count);
}
}
}
Loading