Skip to content

Commit 6fdef90

Browse files
committed
Merged PR 22110: [3.1] 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 d0fb237 commit 6fdef90

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
@@ -99,7 +99,7 @@ public string GetRequestCookie(HttpContext context, string key)
9999
var chunksCount = ParseChunksCount(value);
100100
if (chunksCount > 0)
101101
{
102-
var chunks = new string[chunksCount];
102+
var chunks = new List<string>(10); // chunksCount may be wrong, don't trust it.
103103
for (var chunkId = 1; chunkId <= chunksCount; chunkId++)
104104
{
105105
var chunk = requestCookies[key + ChunkKeySuffix + chunkId.ToString(CultureInfo.InvariantCulture)];
@@ -124,7 +124,7 @@ public string GetRequestCookie(HttpContext context, string key)
124124
return value;
125125
}
126126

127-
chunks[chunkId - 1] = chunk;
127+
chunks.Add(chunk);
128128
}
129129

130130
return string.Join(string.Empty, chunks);
@@ -241,13 +241,22 @@ public void DeleteCookie(HttpContext context, string key, CookieOptions options)
241241
var keys = new List<string>();
242242
keys.Add(key + "=");
243243

244-
var requestCookie = context.Request.Cookies[key];
245-
var chunks = ParseChunksCount(requestCookie);
244+
var requestCookies = context.Request.Cookies;
245+
var requestCookie = requestCookies[key];
246+
long chunks = ParseChunksCount(requestCookie);
246247
if (chunks > 0)
247248
{
248249
for (int i = 1; i <= chunks + 1; i++)
249250
{
250251
var subkey = key + ChunkKeySuffix + i.ToString(CultureInfo.InvariantCulture);
252+
253+
// Only delete cookies we received. We received the chunk count cookie so we should have received the others too.
254+
if (string.IsNullOrEmpty(requestCookies[subkey]))
255+
{
256+
chunks = i - 1;
257+
break;
258+
}
259+
251260
keys.Add(subkey + "=");
252261
}
253262
}

0 commit comments

Comments
 (0)