web-dev-qa-db-ja.com

ASP.NET CoreでSameSite Cookie属性が省略されている

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を含めるには?

8
GrayCat

この問題は、.NET Frameworkおよび.NET Coreの最新リリースで修正されています。

私はすでにこの他の投稿で投稿したように https://stackoverflow.com/a/58998232/906046 、cookieオプションSameSiteMode.Noneは意図したとおりに機能しています。

3
Xavierh95

問題のように見えますが、SameSite EnumにはNone値がありますが、これは単にSameSite属性を提供しないというデフォルト値として解釈されます。これは code for SetCookieHeaderValue で確認できます。これには、StrictLaxのトークン値のみが含まれます。

SameSite=None; Secure Cookieを設定するには、Set-Cookieヘッダーを自分で送信する必要があります。

(補足:コアのプルリクエストを整理して、適切なNoneサポートを追加します)

3
rowan_m

サイドロードオプションを必要とする可能性がある人のために、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バージョン、シナリオの修正が提供されます。

1
Charles Chen

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()
        {
        }
    }
}

このコードによって渡されるいくつかの懸念:

  • コピーされたCookieは、PathExpiresなどの属性を保持します。これらはサイトが正しく機能するために必要な場合があります。
  • Cookieを復元する場合、Cookieヘッダーに追加するだけでなく、.NET HttpRequest.Cookiesコレクションにも追加されます。これは、たとえばASP.NETセッションが失われないようにするために必要です。
  • Cookieを復元するときに、重複するCookieヘッダーが作成される可能性を回避します。これは RFC 6265 に反し、アプリケーションで問題を引き起こす可能性があります。

展開のいくつかのオプション:

  • ハンドラーのコードを既存のアプリケーションに追加する
  • DLLにコンパイルして、アプリケーションのbinフォルダーにデプロイします。
  • DLLにコンパイルして、GACに追加します

構成(web.configなど):

<system.webServer>
  ...
  <modules>
    <add name="SameSiteModule" type="SameSiteHttpModule.SameSiteModule, CustomSameSiteModule" />

pSチャールズ、私はvarのファンです、ごめんなさい:)

0
Phil Dennis