Skip to content

Commit 0a1e208

Browse files
Tratcheranalogrelay
authored andcommitted
[3.1] Re-implement SameSite for 2019 (#13776)
1 parent 92f771f commit 0a1e208

18 files changed

+296
-66
lines changed

src/Http/Headers/ref/Microsoft.Net.Http.Headers.netcoreapp.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,7 @@ public RangeItemHeaderValue(long? from, long? to) { }
316316
}
317317
public enum SameSiteMode
318318
{
319+
Unspecified = -1,
319320
None = 0,
320321
Lax = 1,
321322
Strict = 2,

src/Http/Headers/src/SameSiteMode.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ namespace Microsoft.Net.Http.Headers
55
{
66
/// <summary>
77
/// Indicates if the client should include a cookie on "same-site" or "cross-site" requests.
8-
/// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-cookie-same-site-00
8+
/// RFC Draft: https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.1
99
/// </summary>
1010
// This mirrors Microsoft.AspNetCore.Http.SameSiteMode
1111
public enum SameSiteMode
1212
{
1313
/// <summary>No SameSite field will be set, the client should follow its default cookie policy.</summary>
14+
Unspecified = -1,
15+
/// <summary>Indicates the client should disable same-site restrictions.</summary>
1416
None = 0,
1517
/// <summary>Indicates the client should send the cookie with "same-site" requests, and with "cross-site" top-level navigations.</summary>
1618
Lax,

src/Http/Headers/src/SetCookieHeaderValue.cs

Lines changed: 60 additions & 16 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 SuppressSameSiteNone;
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.AspNetCore.SuppressSameSiteNone", out var enabled))
48+
{
49+
SuppressSameSiteNone = 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; } = SuppressSameSiteNone ? SameSiteMode.None : SameSiteMode.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 && !SuppressSameSiteNone)
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

@@ -140,9 +166,9 @@ public override string ToString()
140166
length += SeparatorToken.Length + HttpOnlyToken.Length;
141167
}
142168

143-
return string.Create(length, (this, maxAge), (span, tuple) =>
169+
return string.Create(length, (this, maxAge, sameSite), (span, tuple) =>
144170
{
145-
var (headerValue, maxAgeValue) = tuple;
171+
var (headerValue, maxAgeValue, sameSite) = tuple;
146172

147173
Append(ref span, headerValue._name);
148174
Append(ref span, EqualsToken);
@@ -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 && !SuppressSameSiteNone)
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 = SuppressSameSiteNone ? SameSiteMode.Strict : SameSiteMode.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 (!SuppressSameSiteNone
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 = SuppressSameSiteNone ? SameSiteMode.Strict : SameSiteMode.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.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_SameSiteNoneCompat()
318+
{
319+
SetCookieHeaderValue.SuppressSameSiteNone = 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.SuppressSameSiteNone = 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_SameSiteNoneCompat()
351+
{
352+
SetCookieHeaderValue.SuppressSameSiteNone = 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.SuppressSameSiteNone = 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_SameSiteNoneCompat()
387+
{
388+
SetCookieHeaderValue.SuppressSameSiteNone = 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.SuppressSameSiteNone = 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_SameSiteNoneCompat()
422+
{
423+
SetCookieHeaderValue.SuppressSameSiteNone = 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.SuppressSameSiteNone = 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/Http/Http.Abstractions/src/CookieBuilder.cs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,20 @@ namespace Microsoft.AspNetCore.Http
1111
/// </summary>
1212
public class CookieBuilder
1313
{
14+
// True (old): https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-3.1
15+
// False (new): https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-03#section-4.1.1
16+
internal static bool SuppressSameSiteNone;
17+
1418
private string _name;
1519

20+
static CookieBuilder()
21+
{
22+
if (AppContext.TryGetSwitch("Microsoft.AspNetCore.SuppressSameSiteNone", out var enabled))
23+
{
24+
SuppressSameSiteNone = enabled;
25+
}
26+
}
27+
1628
/// <summary>
1729
/// The name of the cookie.
1830
/// </summary>
@@ -49,12 +61,12 @@ public virtual string Name
4961
public virtual bool HttpOnly { get; set; }
5062

5163
/// <summary>
52-
/// The SameSite attribute of the cookie. The default value is <see cref="SameSiteMode.None"/>
64+
/// The SameSite attribute of the cookie. The default value is <see cref="SameSiteMode.Unspecified"/>
5365
/// </summary>
5466
/// <remarks>
5567
/// Determines the value that will set on <seealso cref="CookieOptions.SameSite"/>.
5668
/// </remarks>
57-
public virtual SameSiteMode SameSite { get; set; } = SameSiteMode.None;
69+
public virtual SameSiteMode SameSite { get; set; } = SuppressSameSiteNone ? SameSiteMode.None : SameSiteMode.Unspecified;
5870

5971
/// <summary>
6072
/// The policy that will be used to determine <seealso cref="CookieOptions.Secure"/>.

0 commit comments

Comments
 (0)