ASP.NET CoreでCookieのSameCookie属性を明示的にNoneに設定しようとしています。
私がこれを試みた方法は、次のようにCookieOptionsのプロパティ値を設定することでした:
var options = new CookieOptions
{
SameSite = SameSiteMode.None
};
(他の属性は簡潔にするために省略されています)
ただし、サーバーの応答ヘッダー(サーバーがCookieをSameSite = Noneに設定することになっている場合)を調べると、SameSiteが省略されていることがわかります。逆に、Value、Expires、PathもSecureが明示的に述べられているのを見ることができます。
C#コードでSameSiteをLaxまたはStrictに設定すると、Set-Cookieヘッダーに明示的に含まれることがわかります。なしに設定した場合-できません。
2つのブラウザー(FirefoxとChrome= 77)を確認しました(このバージョンでSameSiteに導入された変更を認識しています)。
SameSite = Noneを含めるハックがあります。 CookieOptionsのPathプロパティに次の行を追加するだけです。
options.Path += "; samesite=None";
次に、応答のSet-Cookieヘッダーにあります。
Kestrelを構成する方法はありますか(IISホスティングに使用、Kestrelのみ))このようにハッキングせずにヘッダーにSameSite = Noneを含めるには?
この問題は、.NET Frameworkおよび.NET Coreの最新リリースで修正されています。
私はすでにこの他の投稿で投稿したように https://stackoverflow.com/a/58998232/906046 、cookieオプションSameSiteMode.None
は意図したとおりに機能しています。
問題のように見えますが、SameSite
EnumにはNone
値がありますが、これは単にSameSite
属性を提供しないというデフォルト値として解釈されます。これは code for SetCookieHeaderValue
で確認できます。これには、Strict
とLax
のトークン値のみが含まれます。
SameSite=None; Secure
Cookieを設定するには、Set-Cookie
ヘッダーを自分で送信する必要があります。
(補足:コアのプルリクエストを整理して、適切なNone
サポートを追加します)
サイドロードオプションを必要とする可能性がある人のために、IIS HTTPリクエストパイプラインにIHttpModule
としてプラグインする簡単なソリューションを作成、テスト、リリースしました。ソリューションは基本的にCookieを2回追加します。1つはSameSiteあり、1つはなしです。これにより、SameSite = Noneを理解するブラウザーとして100%のブラウザー互換性が提供されます。もともとGoogle自身が提案し、Auth0が自社製品(別の形式)に実装したソリューション。
コードの要点は以下のとおりです。
using System;
using System.Linq;
using System.Web;
namespace SameSiteHttpModule
{
public class SameSiteDoomsdayModule : IHttpModule
{
/// <summary>
/// Set up the event handlers.
/// </summary>
public void Init(HttpApplication context)
{
// This one is the OUTBOUND side; we add the extra cookie
context.PreSendRequestHeaders += OnEndRequest;
// This one is the INBOUND side; we coalesce the cookies.
context.BeginRequest += OnBeginRequest;
}
/// <summary>
/// The OUTBOUND LEG; we add the extra cookie.
/// </summary>
private void OnEndRequest(object sender, EventArgs e)
{
HttpApplication application = (HttpApplication)sender;
HttpContext context = application.Context;
// IF NEEDED: Add URL filter here
for (int i = 0; i < context.Response.Cookies.Count; i++)
{
HttpCookie responseCookie = context.Response.Cookies[i];
context.Response.Headers.Add("Set-Cookie", $"{responseCookie.Name}-same-site={responseCookie.Value};SameSite=None; Secure");
}
}
/// <summary>
/// The INBOUND LEG; we coalesce the cookies.
/// </summary>
private void OnBeginRequest(object sender, EventArgs e)
{
HttpApplication application = (HttpApplication)sender;
HttpContext context = application.Context;
// IF NEEDED: Add URL filter here
string[] keys = context.Request.Cookies.AllKeys;
for (int i = 0; i < context.Request.Cookies.Count; i++)
{
HttpCookie inboundCookie = context.Request.Cookies[i];
if (!inboundCookie.Name.Contains("-same-site"))
{
continue; // Not interested in this cookie.
}
// Check to see if we have a root cookie without the -same-site
string actualName = inboundCookie.Name.Replace("-same-site", string.Empty);
if (keys.Contains(actualName))
{
continue; // We have the actual key, so we are OK; just continue.
}
// We don't have the actual name, so we need to inject it as if it were the original
// https://support.Microsoft.com/en-us/help/2666571/cookies-added-by-a-managed-httpmodule-are-not-available-to-native-ihtt
// HttpCookie expectedCookie = new HttpCookie(actualName, inboundCookie.Value);
context.Request.Headers.Add("Cookie", $"{actualName}={inboundCookie.Value}");
}
}
public void Dispose()
{
}
}
}
これは他のHTTPモジュールのようにインストールされます:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<modules>
<add type="SameSiteHttpModule.SameSiteDoomsdayModule, SameSiteHttpModule" name="SameSiteDoomsdayModule"/>
</modules>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath=".\IC.He.IdentityServices.exe" arguments="" forwardWindowsAuthToken="false" requestTimeout="00:10:00" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" />
</system.webServer>
</configuration>
詳細はこちら: https://charliedigital.com/2020/01/22/adventures-in-single-sign-on-samesite-doomsday/
元のソースコードを所有しているかどうかに関係なく、.NETバージョン、.NET Coreバージョン、シナリオの修正が提供されます。
Charles Chenによって概説されたアプローチ -ハンドラーを使用して、SameSite=None
およびSecure
が設定された各Cookieのコピーを作成します。 SameSite=None
を正しくサポートしないブラウザとの互換性への簡単なアプローチ。私の状況-古い.NETバージョンをサポートする-のアプローチは命の恩人ですが、Charlesのコードを使おうとすると、いくつかの問題が発生し、「そのまま」動作しませんでした。
ここに私が遭遇した問題に対処する更新されたコードがあります:
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Web;
namespace SameSiteHttpModule
{
public class SameSiteModule : IHttpModule
{
// Suffix includes a randomly generated code to minimize possibility of cookie copies colliding with original names
private const string SuffixForCookieCopy = "-same-site-j4J6bSt0";
private Regex _cookieNameRegex;
private Regex _cookieSameSiteAttributeRegex;
private Regex _cookieSecureAttributeRegex;
/// <inheritdoc />
/// <summary>
/// Set up the event handlers.
/// </summary>
public void Init(HttpApplication context)
{
// Initialize regular expressions used for making a cookie copy
InitializeMatchExpressions();
// This one is the OUTBOUND side; we add the extra cookies
context.PreSendRequestHeaders += OnPreSendRequestHeaders;
// This one is the INBOUND side; we coalesce the cookies
context.BeginRequest += OnBeginRequest;
}
/// <summary>
/// The OUTBOUND LEG; we add the extra cookie
/// </summary>
private void OnPreSendRequestHeaders(object sender, EventArgs e)
{
var application = (HttpApplication) sender;
var response = application.Context.Response;
var cookieCopies = CreateCookieCopiesToSave(response);
SaveCookieCopies(response, cookieCopies);
}
/// <summary>
/// The INBOUND LEG; we coalesce the cookies
/// </summary>
private void OnBeginRequest(object sender, EventArgs e)
{
var application = (HttpApplication) sender;
var request = application.Context.Request;
var cookiesToRestore = CreateCookiesToRestore(request);
RestoreCookies(request, cookiesToRestore);
}
#region Supporting code for saving cookies
private IEnumerable<string> CreateCookieCopiesToSave(HttpResponse response)
{
var cookieStrings = response.Headers.GetValues("set-cookie") ?? new string[0];
var cookieCopies = new List<string>();
foreach (var cookieString in cookieStrings)
{
bool createdCopy;
var cookieStringCopy = TryMakeSameSiteCookieCopy(cookieString, out createdCopy);
if (!createdCopy) continue;
cookieCopies.Add(cookieStringCopy);
}
return cookieCopies;
}
private static void SaveCookieCopies(HttpResponse response, IEnumerable<string> cookieCopies)
{
foreach (var cookieCopy in cookieCopies)
{
response.Headers.Add("set-cookie", cookieCopy);
}
}
private void InitializeMatchExpressions()
{
_cookieNameRegex = new Regex(@"
(?'prefix' # Group 1: Everything prior to cookie name
^\s* # Start of value followed by optional whitespace
)
(?'cookie_name' # Group 2: Cookie name
[^\s=]+ # One or more characters that are not whitespace or equals
)
(?'suffix' # Group 3: Everything after the cookie name
.*$ # Arbitrary characters followed by end of value
)",
RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace);
_cookieSameSiteAttributeRegex = new Regex(@"
(?'prefix' # Group 1: Everything prior to SameSite attribute value
^.* # Start of value followed by 0 or more arbitrary characters
;\s* # Semicolon followed by optional whitespace
SameSite # SameSite attribute name
\s*=\s* # Equals sign (with optional whitespace around it)
)
(?'attribute_value' # Group 2: SameSite attribute value
[^\s;]+ # One or more characters that are not whitespace or semicolon
)
(?'suffix' # Group 3: Everything after the SameSite attribute value
.*$ # Arbitrary characters followed by end of value
)",
RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace);
_cookieSecureAttributeRegex = new Regex(@"
;\s* # Semicolon followed by optional whitespace
Secure # Secure attribute value
\s* # Optional whitespace
(?:;|$) # Semicolon or end of value",
RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace);
}
private string TryMakeSameSiteCookieCopy(string cookie, out bool success)
{
if (!AddNameSuffix(ref cookie))
{
// could not add the name suffix so unable to copy cookie (generally should not happen)
success = false;
return null;
}
var addedSameSiteNone = AddSameSiteNone(ref cookie);
var addedSecure = AddSecure(ref cookie);
if (!addedSameSiteNone && !addedSecure)
{
// cookie already has SameSite and Secure attributes so don't make copy
success = false;
return null;
}
success = true;
return cookie;
}
private bool AddNameSuffix(ref string cookie)
{
var match = _cookieNameRegex.Match(cookie);
if (!match.Success)
{
// Could not find the cookie name in order to modify it
return false;
}
var groups = match.Groups;
var nameForCopy = groups["cookie_name"] + SuffixForCookieCopy;
cookie = string.Concat(groups["prefix"].Value, nameForCopy, groups["suffix"].Value);
return true;
}
private bool AddSameSiteNone(ref string cookie)
{
var match = _cookieSameSiteAttributeRegex.Match(cookie);
if (!match.Success)
{
cookie += "; SameSite=None";
return true;
}
var groups = match.Groups;
if (groups["attribute_value"].Value.Equals("None", StringComparison.OrdinalIgnoreCase))
{
// SameSite=None is already present, so we will not add it
return false;
}
// Replace existing SameSite value with "None"
cookie = string.Concat(groups["prefix"].Value, "None", groups["suffix"].Value);
return true;
}
private bool AddSecure(ref string cookie)
{
if (_cookieSecureAttributeRegex.IsMatch(cookie))
{
// Secure is already present so we will not add it
return false;
}
cookie += "; Secure";
return true;
}
#endregion
#region Supporting code for restoring cookies
private static IEnumerable<HttpCookie> CreateCookiesToRestore(HttpRequest request)
{
var cookiesToRestore = new List<HttpCookie>();
for (var i = 0; i < request.Cookies.Count; i++)
{
var inboundCookie = request.Cookies[i];
if (inboundCookie == null) continue;
var cookieName = inboundCookie.Name;
if (!cookieName.EndsWith(SuffixForCookieCopy, StringComparison.OrdinalIgnoreCase))
{
continue; // Not interested in this cookie since it is not a copied cookie.
}
var originalName = cookieName.Substring(0, cookieName.Length - SuffixForCookieCopy.Length);
if (request.Cookies[originalName] != null)
{
continue; // We have the original cookie, so we are OK; just continue.
}
cookiesToRestore.Add(new HttpCookie(originalName, inboundCookie.Value));
}
return cookiesToRestore;
}
private static void RestoreCookies(HttpRequest request, IEnumerable<HttpCookie> cookiesToRestore)
{
// We need to inject cookies as if they were the original.
foreach (var cookie in cookiesToRestore)
{
// Add to the cookie header for non-managed modules
// https://support.Microsoft.com/en-us/help/2666571/cookies-added-by-a-managed-httpmodule-are-not-available-to-native-ihtt
if (request.Headers["cookie"] == null)
{
request.Headers.Add("cookie", $"{cookie.Name}={cookie.Value}");
}
else
{
request.Headers["cookie"] += $"; {cookie.Name}={cookie.Value}";
}
// Also add to the request cookies collection for managed modules.
request.Cookies.Add(cookie);
}
}
#endregion
public void Dispose()
{
}
}
}
このコードによって渡されるいくつかの懸念:
Path
やExpires
などの属性を保持します。これらはサイトが正しく機能するために必要な場合があります。Cookie
ヘッダーに追加するだけでなく、.NET HttpRequest.Cookies
コレクションにも追加されます。これは、たとえばASP.NETセッションが失われないようにするために必要です。Cookie
ヘッダーが作成される可能性を回避します。これは RFC 6265 に反し、アプリケーションで問題を引き起こす可能性があります。展開のいくつかのオプション:
構成(web.configなど):
<system.webServer>
...
<modules>
<add name="SameSiteModule" type="SameSiteHttpModule.SameSiteModule, CustomSameSiteModule" />
pSチャールズ、私はvar
のファンです、ごめんなさい:)