Skip to content

Commit bc40f40

Browse files
Tratchercampersau
andauthored
Log for SameSite=None without Secure (#24970)
* Log for SameSite=None without Secure * Update src/Http/Http/src/Internal/EventIds.cs Co-authored-by: campersau <buchholz.bastian@googlemail.com> Co-authored-by: campersau <buchholz.bastian@googlemail.com>
1 parent edf25b7 commit bc40f40

File tree

4 files changed

+115
-32
lines changed

4 files changed

+115
-32
lines changed

src/Http/Http/src/Features/ResponseCookiesFeature.cs

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public class ResponseCookiesFeature : IResponseCookiesFeature
1515
// Lambda hoisted to static readonly field to improve inlining https://github.com/dotnet/roslyn/issues/13624
1616
private readonly static Func<IFeatureCollection, IHttpResponseFeature?> _nullResponseFeature = f => null;
1717

18-
private FeatureReferences<IHttpResponseFeature> _features;
18+
private readonly IFeatureCollection _features;
1919
private IResponseCookies? _cookiesCollection;
2020

2121
/// <summary>
@@ -27,12 +27,7 @@ public class ResponseCookiesFeature : IResponseCookiesFeature
2727
/// </param>
2828
public ResponseCookiesFeature(IFeatureCollection features)
2929
{
30-
if (features == null)
31-
{
32-
throw new ArgumentNullException(nameof(features));
33-
}
34-
35-
_features.Initalize(features);
30+
_features = features ?? throw new ArgumentNullException(nameof(features));
3631
}
3732

3833
/// <summary>
@@ -46,25 +41,17 @@ public ResponseCookiesFeature(IFeatureCollection features)
4641
[Obsolete("This constructor is obsolete and will be removed in a future version.")]
4742
public ResponseCookiesFeature(IFeatureCollection features, ObjectPool<StringBuilder>? builderPool)
4843
{
49-
if (features == null)
50-
{
51-
throw new ArgumentNullException(nameof(features));
52-
}
53-
54-
_features.Initalize(features);
44+
_features = features ?? throw new ArgumentNullException(nameof(features));
5545
}
5646

57-
private IHttpResponseFeature HttpResponseFeature => _features.Fetch(ref _features.Cache, _nullResponseFeature)!;
58-
5947
/// <inheritdoc />
6048
public IResponseCookies Cookies
6149
{
6250
get
6351
{
6452
if (_cookiesCollection == null)
6553
{
66-
var headers = HttpResponseFeature.Headers;
67-
_cookiesCollection = new ResponseCookies(headers);
54+
_cookiesCollection = new ResponseCookies(_features);
6855
}
6956

7057
return _cookiesCollection;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.Extensions.Logging;
5+
6+
namespace Microsoft.AspNetCore.Http
7+
{
8+
internal static class EventIds
9+
{
10+
public static readonly EventId SameSiteNotSecure = new EventId(1, "SameSiteNotSecure");
11+
}
12+
}

src/Http/Http/src/Internal/ResponseCookies.cs

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using Microsoft.AspNetCore.Http.Features;
7+
using Microsoft.Extensions.DependencyInjection;
8+
using Microsoft.Extensions.Logging;
69
using Microsoft.Extensions.Primitives;
710
using Microsoft.Net.Http.Headers;
811

@@ -16,18 +19,16 @@ internal class ResponseCookies : IResponseCookies
1619
internal const string EnableCookieNameEncoding = "Microsoft.AspNetCore.Http.EnableCookieNameEncoding";
1720
internal bool _enableCookieNameEncoding = AppContext.TryGetSwitch(EnableCookieNameEncoding, out var enabled) && enabled;
1821

22+
private readonly IFeatureCollection _features;
23+
private ILogger? _logger;
24+
1925
/// <summary>
2026
/// Create a new wrapper.
2127
/// </summary>
22-
/// <param name="headers">The <see cref="IHeaderDictionary"/> for the response.</param>
23-
public ResponseCookies(IHeaderDictionary headers)
28+
internal ResponseCookies(IFeatureCollection features)
2429
{
25-
if (headers == null)
26-
{
27-
throw new ArgumentNullException(nameof(headers));
28-
}
29-
30-
Headers = headers;
30+
_features = features;
31+
Headers = _features.Get<IHttpResponseFeature>().Headers;
3132
}
3233

3334
private IHeaderDictionary Headers { get; set; }
@@ -54,6 +55,21 @@ public void Append(string key, string value, CookieOptions options)
5455
throw new ArgumentNullException(nameof(options));
5556
}
5657

58+
// SameSite=None cookies must be marked as Secure.
59+
if (!options.Secure && options.SameSite == SameSiteMode.None)
60+
{
61+
if (_logger == null)
62+
{
63+
var services = _features.Get<Features.IServiceProvidersFeature>()?.RequestServices;
64+
_logger = services?.GetService<ILogger<ResponseCookies>>();
65+
}
66+
67+
if (_logger != null)
68+
{
69+
Log.SameSiteCookieNotSecure(_logger, key);
70+
}
71+
}
72+
5773
var setCookieHeaderValue = new SetCookieHeaderValue(
5874
_enableCookieNameEncoding ? Uri.EscapeDataString(key) : key,
5975
Uri.EscapeDataString(value))
@@ -135,5 +151,18 @@ public void Delete(string key, CookieOptions options)
135151
SameSite = options.SameSite
136152
});
137153
}
154+
155+
private static class Log
156+
{
157+
private static readonly Action<ILogger, string, Exception?> _samesiteNotSecure = LoggerMessage.Define<string>(
158+
LogLevel.Warning,
159+
EventIds.SameSiteNotSecure,
160+
"The cookie '{name}' has set 'SameSite=None' and must also set 'Secure'.");
161+
162+
public static void SameSiteCookieNotSecure(ILogger logger, string name)
163+
{
164+
_samesiteNotSecure(logger, name, null);
165+
}
166+
}
138167
}
139168
}

src/Http/Http/test/ResponseCookiesTest.cs

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,67 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using Microsoft.AspNetCore.Http.Features;
6+
using Microsoft.Extensions.DependencyInjection;
7+
using Microsoft.Extensions.Logging;
8+
using Microsoft.Extensions.Logging.Testing;
59
using Microsoft.Net.Http.Headers;
610
using Xunit;
711

812
namespace Microsoft.AspNetCore.Http.Tests
913
{
1014
public class ResponseCookiesTest
1115
{
16+
private IFeatureCollection MakeFeatures(IHeaderDictionary headers)
17+
{
18+
var responseFeature = new HttpResponseFeature()
19+
{
20+
Headers = headers
21+
};
22+
var features = new FeatureCollection();
23+
features.Set<IHttpResponseFeature>(responseFeature);
24+
return features;
25+
}
26+
27+
[Fact]
28+
public void AppendSameSiteNoneWithoutSecureLogsWarning()
29+
{
30+
var headers = new HeaderDictionary();
31+
var features = MakeFeatures(headers);
32+
var services = new ServiceCollection();
33+
34+
var sink = new TestSink(TestSink.EnableWithTypeName<ResponseCookies>);
35+
var loggerFactory = new TestLoggerFactory(sink, enabled: true);
36+
services.AddLogging();
37+
services.AddSingleton<ILoggerFactory>(loggerFactory);
38+
39+
features.Set<IServiceProvidersFeature>(new ServiceProvidersFeature() { RequestServices = services.BuildServiceProvider() });
40+
41+
var cookies = new ResponseCookies(features);
42+
var testCookie = "TestCookie";
43+
44+
cookies.Append(testCookie, "value", new CookieOptions()
45+
{
46+
SameSite = SameSiteMode.None,
47+
});
48+
49+
var cookieHeaderValues = headers[HeaderNames.SetCookie];
50+
Assert.Single(cookieHeaderValues);
51+
Assert.StartsWith(testCookie, cookieHeaderValues[0]);
52+
Assert.Contains("path=/", cookieHeaderValues[0]);
53+
Assert.Contains("samesite=none", cookieHeaderValues[0]);
54+
Assert.DoesNotContain("secure", cookieHeaderValues[0]);
55+
56+
var writeContext = Assert.Single(sink.Writes);
57+
Assert.Equal("The cookie 'TestCookie' has set 'SameSite=None' and must also set 'Secure'.", writeContext.Message);
58+
}
59+
1260
[Fact]
1361
public void DeleteCookieShouldSetDefaultPath()
1462
{
1563
var headers = new HeaderDictionary();
16-
var cookies = new ResponseCookies(headers);
64+
var features = MakeFeatures(headers);
65+
var cookies = new ResponseCookies(features);
1766
var testCookie = "TestCookie";
1867

1968
cookies.Delete(testCookie);
@@ -29,7 +78,8 @@ public void DeleteCookieShouldSetDefaultPath()
2978
public void DeleteCookieWithCookieOptionsShouldKeepPropertiesOfCookieOptions()
3079
{
3180
var headers = new HeaderDictionary();
32-
var cookies = new ResponseCookies(headers);
81+
var features = MakeFeatures(headers);
82+
var cookies = new ResponseCookies(features);
3383
var testCookie = "TestCookie";
3484
var time = new DateTimeOffset(2000, 1, 1, 1, 1, 1, 1, TimeSpan.Zero);
3585
var options = new CookieOptions
@@ -58,7 +108,8 @@ public void DeleteCookieWithCookieOptionsShouldKeepPropertiesOfCookieOptions()
58108
public void NoParamsDeleteRemovesCookieCreatedByAdd()
59109
{
60110
var headers = new HeaderDictionary();
61-
var cookies = new ResponseCookies(headers);
111+
var features = MakeFeatures(headers);
112+
var cookies = new ResponseCookies(features);
62113
var testCookie = "TestCookie";
63114

64115
cookies.Append(testCookie, testCookie);
@@ -75,7 +126,8 @@ public void NoParamsDeleteRemovesCookieCreatedByAdd()
75126
public void ProvidesMaxAgeWithCookieOptionsArgumentExpectMaxAgeToBeSet()
76127
{
77128
var headers = new HeaderDictionary();
78-
var cookies = new ResponseCookies(headers);
129+
var features = MakeFeatures(headers);
130+
var cookies = new ResponseCookies(features);
79131
var cookieOptions = new CookieOptions();
80132
var maxAgeTime = TimeSpan.FromHours(1);
81133
cookieOptions.MaxAge = TimeSpan.FromHours(1);
@@ -96,7 +148,8 @@ public void ProvidesMaxAgeWithCookieOptionsArgumentExpectMaxAgeToBeSet()
96148
public void EscapesValuesBeforeSettingCookie(string value, string expected)
97149
{
98150
var headers = new HeaderDictionary();
99-
var cookies = new ResponseCookies(headers);
151+
var features = MakeFeatures(headers);
152+
var cookies = new ResponseCookies(features);
100153

101154
cookies.Append("key", value);
102155

@@ -111,7 +164,8 @@ public void EscapesValuesBeforeSettingCookie(string value, string expected)
111164
public void InvalidKeysThrow(string key)
112165
{
113166
var headers = new HeaderDictionary();
114-
var cookies = new ResponseCookies(headers);
167+
var features = MakeFeatures(headers);
168+
var cookies = new ResponseCookies(features);
115169

116170
Assert.Throws<ArgumentException>(() => cookies.Append(key, "1"));
117171
}
@@ -124,7 +178,8 @@ public void InvalidKeysThrow(string key)
124178
public void AppContextSwitchEscapesKeysAndValuesBeforeSettingCookie(string key, string value, string expected)
125179
{
126180
var headers = new HeaderDictionary();
127-
var cookies = new ResponseCookies(headers);
181+
var features = MakeFeatures(headers);
182+
var cookies = new ResponseCookies(features);
128183
cookies._enableCookieNameEncoding = true;
129184

130185
cookies.Append(key, value);

0 commit comments

Comments
 (0)