Skip to content

Commit 0561e78

Browse files
committed
Merged PR 22111: [5.0] Fix partial chunked cookies 70242
# Fix partial chunked cookies MSRC # 70242: Fix exceptions and allocations when the cookie chunk count is not accurate Backported from 6.0: https://dev.azure.com/dnceng/internal/_git/dotnet-aspnetcore/pullrequest/21247 ## Description Browsers have limits on how long cookies can be, as low as 4kb. It's common for TempData and CookieAuth to get above that limit, so cookies are split into chunks with the following format: MyCookie=chunks-3 MyCookieC1=(Base64EncodedData) MyCookieC2=(Base64EncodedData) MyCookieC3=(Base64EncodedData) Fixes MSRC # 70242 ## Customer Impact A malicious client could send `MyCookie=chunks-2147483647` without the actual cookie chunks and cause large allocations, exceptions, and excess CPU utilization on the server when it tried to read or delete that many chunks. This flaw comes from the original implementation in Microsoft.Owin, but is much worse in AspNetCore when adopted by TempData due to it automatically calling Delete if reading the cookie fails. ## Regression? - [ ] Yes - [x] No ## Risk - [ ] High - [ ] Medium - [x] Low Easy to reproduce and test. ## Verification - [x] Manual (required) - [x] Automated ## Packaging changes reviewed? - [ ] Yes - [ ] No - [x] N/A
1 parent 9ab95ae commit 0561e78

File tree

2 files changed

+103
-6
lines changed

2 files changed

+103
-6
lines changed

src/Security/CookiePolicy/test/CookieChunkingTests.cs

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using Microsoft.AspNetCore.Http;
6+
using Microsoft.Net.Http.Headers;
67
using Xunit;
78

89
namespace Microsoft.AspNetCore.Internal
@@ -30,7 +31,7 @@ public void AppendLargeCookie_WithOptions_Appended()
3031
{
3132
Domain = "foo.com",
3233
HttpOnly = true,
33-
SameSite = SameSiteMode.Strict,
34+
SameSite = Http.SameSiteMode.Strict,
3435
Path = "/bar",
3536
Secure = true,
3637
Expires = now.AddMinutes(5),
@@ -128,7 +129,7 @@ public void GetLargeChunkedCookieWithMissingChunk_ThrowingDisabled_NotReassemble
128129
public void DeleteChunkedCookieWithOptions_AllDeleted()
129130
{
130131
HttpContext context = new DefaultHttpContext();
131-
context.Request.Headers.Append("Cookie", "TestCookie=chunks-7");
132+
context.Request.Headers.Append("Cookie", "TestCookie=chunks-7;TestCookieC1=1;TestCookieC2=2;TestCookieC3=3;TestCookieC4=4;TestCookieC5=5;TestCookieC6=6;TestCookieC7=7");
132133

133134
new ChunkingCookieManager().DeleteCookie(context, "TestCookie", new CookieOptions() { Domain = "foo.com", Secure = true });
134135
var cookies = context.Response.Headers["Set-Cookie"];
@@ -145,5 +146,92 @@ public void DeleteChunkedCookieWithOptions_AllDeleted()
145146
"TestCookieC7=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; secure",
146147
}, cookies);
147148
}
149+
150+
[Fact]
151+
public void DeleteChunkedCookieWithMissingRequestCookies_OnlyPresentCookiesDeleted()
152+
{
153+
HttpContext context = new DefaultHttpContext();
154+
context.Request.Headers.Append("Cookie", "TestCookie=chunks-7;TestCookieC1=1;TestCookieC2=2");
155+
156+
new ChunkingCookieManager().DeleteCookie(context, "TestCookie", new CookieOptions() { Domain = "foo.com", Secure = true });
157+
var cookies = context.Response.Headers["Set-Cookie"];
158+
Assert.Equal(3, cookies.Count);
159+
Assert.Equal(new[]
160+
{
161+
"TestCookie=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; secure",
162+
"TestCookieC1=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; secure",
163+
"TestCookieC2=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; secure",
164+
}, cookies);
165+
}
166+
167+
[Fact]
168+
public void DeleteChunkedCookieWithMissingRequestCookies_StopsAtMissingChunk()
169+
{
170+
HttpContext context = new DefaultHttpContext();
171+
// C3 is missing so we don't try to delete C4 either.
172+
context.Request.Headers.Append("Cookie", "TestCookie=chunks-7;TestCookieC1=1;TestCookieC2=2;TestCookieC4=4");
173+
174+
new ChunkingCookieManager().DeleteCookie(context, "TestCookie", new CookieOptions() { Domain = "foo.com", Secure = true });
175+
var cookies = context.Response.Headers["Set-Cookie"];
176+
Assert.Equal(3, cookies.Count);
177+
Assert.Equal(new[]
178+
{
179+
"TestCookie=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; secure",
180+
"TestCookieC1=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; secure",
181+
"TestCookieC2=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; secure",
182+
}, cookies);
183+
}
184+
185+
[Fact]
186+
public void DeleteChunkedCookieWithOptionsAndResponseCookies_AllDeleted()
187+
{
188+
var chunkingCookieManager = new ChunkingCookieManager();
189+
HttpContext httpContext = new DefaultHttpContext();
190+
191+
httpContext.Request.Headers["Cookie"] = new[]
192+
{
193+
"TestCookie=chunks-7",
194+
"TestCookieC1=abcdefghi",
195+
"TestCookieC2=jklmnopqr",
196+
"TestCookieC3=stuvwxyz0",
197+
"TestCookieC4=123456789",
198+
"TestCookieC5=ABCDEFGHI",
199+
"TestCookieC6=JKLMNOPQR",
200+
"TestCookieC7=STUVWXYZ"
201+
};
202+
203+
var cookieOptions = new CookieOptions()
204+
{
205+
Domain = "foo.com",
206+
Path = "/",
207+
Secure = true
208+
};
209+
210+
httpContext.Response.Headers[HeaderNames.SetCookie] = new[]
211+
{
212+
"TestCookie=chunks-7; domain=foo.com; path=/; secure",
213+
"TestCookieC1=STUVWXYZ; domain=foo.com; path=/; secure",
214+
"TestCookieC2=123456789; domain=foo.com; path=/; secure",
215+
"TestCookieC3=stuvwxyz0; domain=foo.com; path=/; secure",
216+
"TestCookieC4=123456789; domain=foo.com; path=/; secure",
217+
"TestCookieC5=ABCDEFGHI; domain=foo.com; path=/; secure",
218+
"TestCookieC6=JKLMNOPQR; domain=foo.com; path=/; secure",
219+
"TestCookieC7=STUVWXYZ; domain=foo.com; path=/; secure"
220+
};
221+
222+
chunkingCookieManager.DeleteCookie(httpContext, "TestCookie", cookieOptions);
223+
Assert.Equal(8, httpContext.Response.Headers[HeaderNames.SetCookie].Count);
224+
Assert.Equal(new[]
225+
{
226+
"TestCookie=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; secure",
227+
"TestCookieC1=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; secure",
228+
"TestCookieC2=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; secure",
229+
"TestCookieC3=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; secure",
230+
"TestCookieC4=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; secure",
231+
"TestCookieC5=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; secure",
232+
"TestCookieC6=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; secure",
233+
"TestCookieC7=; expires=Thu, 01 Jan 1970 00:00:00 GMT; domain=foo.com; path=/; secure"
234+
}, httpContext.Response.Headers[HeaderNames.SetCookie]);
235+
}
148236
}
149237
}

src/Shared/ChunkingCookieManager/ChunkingCookieManager.cs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ private static int ParseChunksCount(string? value)
104104
var chunksCount = ParseChunksCount(value);
105105
if (chunksCount > 0)
106106
{
107-
var chunks = new string[chunksCount];
107+
var chunks = new List<string>(10); // chunksCount may be wrong, don't trust it.
108108
for (var chunkId = 1; chunkId <= chunksCount; chunkId++)
109109
{
110110
var chunk = requestCookies[key + ChunkKeySuffix + chunkId.ToString(CultureInfo.InvariantCulture)];
@@ -129,7 +129,7 @@ private static int ParseChunksCount(string? value)
129129
return value;
130130
}
131131

132-
chunks[chunkId - 1] = chunk;
132+
chunks.Add(chunk);
133133
}
134134

135135
return string.Join(string.Empty, chunks);
@@ -246,13 +246,22 @@ public void DeleteCookie(HttpContext context, string key, CookieOptions options)
246246
var keys = new List<string>();
247247
keys.Add(key + "=");
248248

249-
var requestCookie = context.Request.Cookies[key];
250-
var chunks = ParseChunksCount(requestCookie);
249+
var requestCookies = context.Request.Cookies;
250+
var requestCookie = requestCookies[key];
251+
long chunks = ParseChunksCount(requestCookie);
251252
if (chunks > 0)
252253
{
253254
for (int i = 1; i <= chunks + 1; i++)
254255
{
255256
var subkey = key + ChunkKeySuffix + i.ToString(CultureInfo.InvariantCulture);
257+
258+
// Only delete cookies we received. We received the chunk count cookie so we should have received the others too.
259+
if (string.IsNullOrEmpty(requestCookies[subkey]))
260+
{
261+
chunks = i - 1;
262+
break;
263+
}
264+
256265
keys.Add(subkey + "=");
257266
}
258267
}

0 commit comments

Comments
 (0)