Skip to content

Commit 4a9ec10

Browse files
committed
Re-implement SameSite for 2019 #12125
1 parent a3fd923 commit 4a9ec10

File tree

5 files changed

+178
-22
lines changed

5 files changed

+178
-22
lines changed

src/Http/Headers/src/SetCookieHeaderValue.cs

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,14 @@ public class SetCookieHeaderValue
2020
private const string SecureToken = "secure";
2121
// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00
2222
private const string SameSiteToken = "samesite";
23+
private static readonly string SameSiteNoneToken = SameSiteMode.None.ToString().ToLower();
2324
private static readonly string SameSiteLaxToken = SameSiteMode.Lax.ToString().ToLower();
2425
private static readonly string SameSiteStrictToken = SameSiteMode.Strict.ToString().ToLower();
26+
27+
// True (old): https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-3.1
28+
// False (new): https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.1
29+
internal static bool UseSameSite2016Compat;
30+
2531
private const string HttpOnlyToken = "httponly";
2632
private const string SeparatorToken = "; ";
2733
private const string EqualsToken = "=";
@@ -36,6 +42,14 @@ private static readonly HttpHeaderParser<SetCookieHeaderValue> MultipleValuePars
3642
private StringSegment _name;
3743
private StringSegment _value;
3844

45+
static SetCookieHeaderValue()
46+
{
47+
if (AppContext.TryGetSwitch("Microsoft.Net.Http.Headers.SetCookieHeaderValue.UseSameSite2016Compat", out var enabled))
48+
{
49+
UseSameSite2016Compat = enabled;
50+
}
51+
}
52+
3953
private SetCookieHeaderValue()
4054
{
4155
// Used by the parser to create a new instance of this type.
@@ -92,16 +106,17 @@ public StringSegment Value
92106

93107
public bool Secure { get; set; }
94108

95-
public SameSiteMode SameSite { get; set; }
109+
public SameSiteMode SameSite { get; set; } = UseSameSite2016Compat ? SameSiteMode.None : (SameSiteMode)(-1); // Unspecified
96110

97111
public bool HttpOnly { get; set; }
98112

99-
// name="value"; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={Strict|Lax}; httponly
113+
// name="value"; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={strict|lax|none}; httponly
100114
public override string ToString()
101115
{
102116
var length = _name.Length + EqualsToken.Length + _value.Length;
103117

104118
string maxAge = null;
119+
string sameSite = null;
105120

106121
if (Expires.HasValue)
107122
{
@@ -129,9 +144,20 @@ public override string ToString()
129144
length += SeparatorToken.Length + SecureToken.Length;
130145
}
131146

132-
if (SameSite != SameSiteMode.None)
147+
// Allow for Unspecified (-1) to skip SameSite
148+
if (SameSite == SameSiteMode.None && !UseSameSite2016Compat)
149+
{
150+
sameSite = SameSiteNoneToken;
151+
length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length;
152+
}
153+
else if (SameSite == SameSiteMode.Lax)
133154
{
134-
var sameSite = SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken;
155+
sameSite = SameSiteLaxToken;
156+
length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length;
157+
}
158+
else if (SameSite == SameSiteMode.Strict)
159+
{
160+
sameSite = SameSiteStrictToken;
135161
length += SeparatorToken.Length + SameSiteToken.Length + EqualsToken.Length + sameSite.Length;
136162
}
137163

@@ -180,9 +206,9 @@ public override string ToString()
180206
AppendSegment(ref span, SecureToken, null);
181207
}
182208

183-
if (headerValue.SameSite != SameSiteMode.None)
209+
if (sameSite != null)
184210
{
185-
AppendSegment(ref span, SameSiteToken, headerValue.SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken);
211+
AppendSegment(ref span, SameSiteToken, sameSite);
186212
}
187213

188214
if (headerValue.HttpOnly)
@@ -248,9 +274,18 @@ public void AppendToStringBuilder(StringBuilder builder)
248274
AppendSegment(builder, SecureToken, null);
249275
}
250276

251-
if (SameSite != SameSiteMode.None)
277+
// Allow for Unspecified (-1) to skip SameSite
278+
if (SameSite == SameSiteMode.None && !UseSameSite2016Compat)
279+
{
280+
AppendSegment(builder, SameSiteToken, SameSiteNoneToken);
281+
}
282+
else if (SameSite == SameSiteMode.Lax)
252283
{
253-
AppendSegment(builder, SameSiteToken, SameSite == SameSiteMode.Lax ? SameSiteLaxToken : SameSiteStrictToken);
284+
AppendSegment(builder, SameSiteToken, SameSiteLaxToken);
285+
}
286+
else if (SameSite == SameSiteMode.Strict)
287+
{
288+
AppendSegment(builder, SameSiteToken, SameSiteStrictToken);
254289
}
255290

256291
if (HttpOnly)
@@ -302,7 +337,7 @@ public static bool TryParseStrictList(IList<string> inputs, out IList<SetCookieH
302337
return MultipleValueParser.TryParseStrictValues(inputs, out parsedValues);
303338
}
304339

305-
// name=value; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={Strict|Lax}; httponly
340+
// name=value; expires=Sun, 06 Nov 1994 08:49:37 GMT; max-age=86400; domain=domain1; path=path1; secure; samesite={Strict|Lax|None}; httponly
306341
private static int GetSetCookieLength(StringSegment input, int startIndex, out SetCookieHeaderValue parsedValue)
307342
{
308343
Contract.Requires(startIndex >= 0);
@@ -437,25 +472,34 @@ private static int GetSetCookieLength(StringSegment input, int startIndex, out S
437472
{
438473
result.Secure = true;
439474
}
440-
// samesite-av = "SameSite" / "SameSite=" samesite-value
441-
// samesite-value = "Strict" / "Lax"
475+
// samesite-av = "SameSite=" samesite-value
476+
// samesite-value = "Strict" / "Lax" / "None"
442477
else if (StringSegment.Equals(token, SameSiteToken, StringComparison.OrdinalIgnoreCase))
443478
{
444479
if (!ReadEqualsSign(input, ref offset))
445480
{
446-
result.SameSite = SameSiteMode.Strict;
481+
result.SameSite = UseSameSite2016Compat ? SameSiteMode.Strict : (SameSiteMode)(-1); // Unspecified
447482
}
448483
else
449484
{
450485
var enforcementMode = ReadToSemicolonOrEnd(input, ref offset);
451486

452-
if (StringSegment.Equals(enforcementMode, SameSiteLaxToken, StringComparison.OrdinalIgnoreCase))
487+
if (StringSegment.Equals(enforcementMode, SameSiteStrictToken, StringComparison.OrdinalIgnoreCase))
488+
{
489+
result.SameSite = SameSiteMode.Strict;
490+
}
491+
else if (StringSegment.Equals(enforcementMode, SameSiteLaxToken, StringComparison.OrdinalIgnoreCase))
453492
{
454493
result.SameSite = SameSiteMode.Lax;
455494
}
495+
else if (!UseSameSite2016Compat
496+
&& StringSegment.Equals(enforcementMode, SameSiteNoneToken, StringComparison.OrdinalIgnoreCase))
497+
{
498+
result.SameSite = SameSiteMode.None;
499+
}
456500
else
457501
{
458-
result.SameSite = SameSiteMode.Strict;
502+
result.SameSite = UseSameSite2016Compat ? SameSiteMode.Strict : (SameSiteMode)(-1); // Unspecified
459503
}
460504
}
461505
}

src/Http/Headers/test/SetCookieHeaderValueTest.cs

Lines changed: 117 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public static TheoryData<SetCookieHeaderValue, string> SetCookieHeaderDataSet
5757
{
5858
SameSite = SameSiteMode.None,
5959
};
60-
dataset.Add(header7, "name7=value7");
60+
dataset.Add(header7, "name7=value7; samesite=none");
6161

6262

6363
return dataset;
@@ -155,9 +155,20 @@ public static TheoryData<IList<SetCookieHeaderValue>, string[]> ListOfSetCookieH
155155
{
156156
SameSite = SameSiteMode.Strict
157157
};
158-
var string6a = "name6=value6; samesite";
159-
var string6b = "name6=value6; samesite=Strict";
160-
var string6c = "name6=value6; samesite=invalid";
158+
var string6 = "name6=value6; samesite=Strict";
159+
160+
var header7 = new SetCookieHeaderValue("name7", "value7")
161+
{
162+
SameSite = SameSiteMode.None
163+
};
164+
var string7 = "name7=value7; samesite=None";
165+
166+
var header8 = new SetCookieHeaderValue("name8", "value8")
167+
{
168+
SameSite = (SameSiteMode)(-1) // Unspecified
169+
};
170+
var string8a = "name8=value8; samesite";
171+
var string8b = "name8=value8; samesite=invalid";
161172

162173
dataset.Add(new[] { header1 }.ToList(), new[] { string1 });
163174
dataset.Add(new[] { header1, header1 }.ToList(), new[] { string1, string1 });
@@ -170,9 +181,10 @@ public static TheoryData<IList<SetCookieHeaderValue>, string[]> ListOfSetCookieH
170181
dataset.Add(new[] { header1, header2, header3, header4 }.ToList(), new[] { string.Join(",", string1, string2, string3, string4) });
171182
dataset.Add(new[] { header5 }.ToList(), new[] { string5a });
172183
dataset.Add(new[] { header5 }.ToList(), new[] { string5b });
173-
dataset.Add(new[] { header6 }.ToList(), new[] { string6a });
174-
dataset.Add(new[] { header6 }.ToList(), new[] { string6b });
175-
dataset.Add(new[] { header6 }.ToList(), new[] { string6c });
184+
dataset.Add(new[] { header6 }.ToList(), new[] { string6 });
185+
dataset.Add(new[] { header7 }.ToList(), new[] { string7 });
186+
dataset.Add(new[] { header8 }.ToList(), new[] { string8a });
187+
dataset.Add(new[] { header8 }.ToList(), new[] { string8b });
176188

177189
return dataset;
178190
}
@@ -301,6 +313,28 @@ public void SetCookieHeaderValue_ToString(SetCookieHeaderValue input, string exp
301313
Assert.Equal(expectedValue, input.ToString());
302314
}
303315

316+
[Fact]
317+
public void SetCookieHeaderValue_ToString_SameSite2016Compat()
318+
{
319+
SetCookieHeaderValue.UseSameSite2016Compat = true;
320+
321+
var input = new SetCookieHeaderValue("name", "value")
322+
{
323+
SameSite = SameSiteMode.None,
324+
};
325+
326+
Assert.Equal("name=value", input.ToString());
327+
328+
SetCookieHeaderValue.UseSameSite2016Compat = false;
329+
330+
var input2 = new SetCookieHeaderValue("name", "value")
331+
{
332+
SameSite = SameSiteMode.None,
333+
};
334+
335+
Assert.Equal("name=value; samesite=none", input2.ToString());
336+
}
337+
304338
[Theory]
305339
[MemberData(nameof(SetCookieHeaderDataSet))]
306340
public void SetCookieHeaderValue_AppendToStringBuilder(SetCookieHeaderValue input, string expectedValue)
@@ -312,6 +346,32 @@ public void SetCookieHeaderValue_AppendToStringBuilder(SetCookieHeaderValue inpu
312346
Assert.Equal(expectedValue, builder.ToString());
313347
}
314348

349+
[Fact]
350+
public void SetCookieHeaderValue_AppendToStringBuilder_SameSite2016Compat()
351+
{
352+
SetCookieHeaderValue.UseSameSite2016Compat = true;
353+
354+
var builder = new StringBuilder();
355+
var input = new SetCookieHeaderValue("name", "value")
356+
{
357+
SameSite = SameSiteMode.None,
358+
};
359+
360+
input.AppendToStringBuilder(builder);
361+
Assert.Equal("name=value", builder.ToString());
362+
363+
SetCookieHeaderValue.UseSameSite2016Compat = false;
364+
365+
var builder2 = new StringBuilder();
366+
var input2 = new SetCookieHeaderValue("name", "value")
367+
{
368+
SameSite = SameSiteMode.None,
369+
};
370+
371+
input2.AppendToStringBuilder(builder2);
372+
Assert.Equal("name=value; samesite=none", builder2.ToString());
373+
}
374+
315375
[Theory]
316376
[MemberData(nameof(SetCookieHeaderDataSet))]
317377
public void SetCookieHeaderValue_Parse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue)
@@ -322,6 +382,31 @@ public void SetCookieHeaderValue_Parse_AcceptsValidValues(SetCookieHeaderValue c
322382
Assert.Equal(expectedValue, header.ToString());
323383
}
324384

385+
[Fact]
386+
public void SetCookieHeaderValue_Parse_AcceptsValidValues_SameSite2016Compat()
387+
{
388+
SetCookieHeaderValue.UseSameSite2016Compat = true;
389+
var header = SetCookieHeaderValue.Parse("name=value; samesite=none");
390+
391+
var cookie = new SetCookieHeaderValue("name", "value")
392+
{
393+
SameSite = SameSiteMode.Strict,
394+
};
395+
396+
Assert.Equal(cookie, header);
397+
Assert.Equal("name=value; samesite=strict", header.ToString());
398+
SetCookieHeaderValue.UseSameSite2016Compat = false;
399+
400+
var header2 = SetCookieHeaderValue.Parse("name=value; samesite=none");
401+
402+
var cookie2 = new SetCookieHeaderValue("name", "value")
403+
{
404+
SameSite = SameSiteMode.None,
405+
};
406+
Assert.Equal(cookie2, header2);
407+
Assert.Equal("name=value; samesite=none", header2.ToString());
408+
}
409+
325410
[Theory]
326411
[MemberData(nameof(SetCookieHeaderDataSet))]
327412
public void SetCookieHeaderValue_TryParse_AcceptsValidValues(SetCookieHeaderValue cookie, string expectedValue)
@@ -332,6 +417,31 @@ public void SetCookieHeaderValue_TryParse_AcceptsValidValues(SetCookieHeaderValu
332417
Assert.Equal(expectedValue, header.ToString());
333418
}
334419

420+
[Fact]
421+
public void SetCookieHeaderValue_TryParse_AcceptsValidValues_SameSite2016Compat()
422+
{
423+
SetCookieHeaderValue.UseSameSite2016Compat = true;
424+
Assert.True(SetCookieHeaderValue.TryParse("name=value; samesite=none", out var header));
425+
var cookie = new SetCookieHeaderValue("name", "value")
426+
{
427+
SameSite = SameSiteMode.Strict,
428+
};
429+
430+
Assert.Equal(cookie, header);
431+
Assert.Equal("name=value; samesite=strict", header.ToString());
432+
433+
SetCookieHeaderValue.UseSameSite2016Compat = false;
434+
435+
Assert.True(SetCookieHeaderValue.TryParse("name=value; samesite=none", out var header2));
436+
var cookie2 = new SetCookieHeaderValue("name", "value")
437+
{
438+
SameSite = SameSiteMode.None,
439+
};
440+
441+
Assert.Equal(cookie2, header2);
442+
Assert.Equal("name=value; samesite=none", header2.ToString());
443+
}
444+
335445
[Theory]
336446
[MemberData(nameof(InvalidSetCookieHeaderDataSet))]
337447
public void SetCookieHeaderValue_Parse_RejectsInvalidValues(string value)

src/Security/Authentication/WsFederation/samples/WsFedSample/WsFedSample.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
<PropertyGroup>
44
<TargetFrameworks>netcoreapp3.0</TargetFrameworks>
5+
<AspNetCoreHostingModel>OutOfProcess</AspNetCoreHostingModel>
56
</PropertyGroup>
67

78
<ItemGroup>

src/Security/Authentication/test/CookieTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ public async Task CookieOptionsAlterSetCookieHeader()
229229
Assert.Contains(" path=/foo", setCookie1);
230230
Assert.Contains(" domain=another.com", setCookie1);
231231
Assert.Contains(" secure", setCookie1);
232-
Assert.DoesNotContain(" samesite", setCookie1);
232+
Assert.Contains(" samesite=none", setCookie1);
233233
Assert.Contains(" httponly", setCookie1);
234234

235235
var server2 = CreateServer(o =>

src/Security/Authentication/test/OpenIdConnect/OpenIdConnectChallengeTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,7 @@ public async Task ChallengeSetsNonceAndStateCookies(OpenIdConnectRedirectBehavio
437437
var server = settings.CreateTestServer();
438438
var transaction = await server.SendAsync(ChallengeEndpoint);
439439

440+
Assert.Contains("samesite=none", transaction.SetCookie.First());
440441
var challengeCookies = SetCookieHeaderValue.ParseList(transaction.SetCookie);
441442
var nonceCookie = challengeCookies.Where(cookie => cookie.Name.StartsWith(OpenIdConnectDefaults.CookieNoncePrefix, StringComparison.Ordinal)).Single();
442443
Assert.True(nonceCookie.Expires.HasValue);

0 commit comments

Comments
 (0)