WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

中間テーブルへのナビゲーションプロパティ

by WebSurfer 3. May 2023 15:32

多対多のリレーションシップを関連付ける中間テーブルへのナビゲーションプロパティ定義の問題で ASP.NET Core MVC での Create, Edit に失敗するのにハマって悩みました。またハマることがないよう備忘録として書いておきます。

エンティティ クラス

上の画像は Microsoft のドキュメント「チュートリアル: ASP.NET MVC Web アプリでの EF Core の概要」のもので Enrollment が中間テーブルです。チュートリアルは Student テーブルの CRUD 操作を行う ASP.NET Core MVC アプリを作成するもので、これをこの記事の例に使います。

Visual Studio 2022 のテンプレートを使ってターゲットフレームワーク .NET 6.0 以降でプロジェクトを作ると、デフォルトでプロジェクト全体で「Null 許容」オプションが有効にされます。

「Null 許容」オプションが有効にされていると、チュートリアルの Student クラスの定義では LastName, FirstMiddleName, Enrollments プロパティ に対しては "null 非許容のプロパティ 'xxxxx' には、コンストラクタの終了時に null 以外の値が入っていなければなりません" という警告が出ます。

その警告を回避するのに、以下のように = null!; を追加したのですが、ナビゲーションプロパティ Enrollments に対してそれはダメでした。ASP.NET Core MVC アプリによる CRUD 操作のうち Create, Edit に失敗します。

public class Student
{
    public int ID { get; set; }
    public string LastName { get; set; } = null!;
    public string FirstMidName { get; set; } = null!;
    public DateTime EnrollmentDate { get; set; }

    // これはダメ。ASP.NET Core MVC での Create, Edit に失敗する
    public ICollection<Enrollment> Enrollments { get; set; } = null!;
}

なぜ失敗するかと言うと、上の定義の場合 Enrollments が ModelStateDictionary に含まれるようになり、値が null になるので ValidationState が Invalid となってしまうからのようです。下の画像を見てください。結果、ModelSate.IsValid が false になって SaveChangesAsync メソッドがスキップされてしまいます。

デバッグ結果

チュートリアルの次のステップ「チュートリアル: CRUD 機能を実装する - ASP.NET MVC と EF Core」で、Edit アクションメソッドに TryUpdateModelAsync<T> メソッドを使う方法が紹介されていますが、それも失敗します。

解決策は、リバースエンジニアリングで既存の同等なデータベースから生成されるコードをまねて、Enrollments プロパティを以下のようにすることだと思います。

public ICollection<Enrollment> Enrollments { get; } = 
                                          new List<Enrollment>();

そうすれば、下の画像の通り ModelStateDictionary には Enrollments は含まれなくなります。Edit アクションメソッドに TryUpdateModelAsync<T> メソッドを使った場合も期待通りの結果となります。

デバッグ結果

Tags: , , ,

CORE

ASP.NET の CSRF 対策

by WebSurfer 2. May 2023 16:45

Visual Studio のテンプレートを使って作成する ASP.NET MVC と Web Forms プロジェクトに組み込まれている Cross-Site Request Forgery (CSRF) 対策について書きます。

Cross-Site Request Forgery (CSRF)

上の図は、Microsoft のドキュメント「ASP.NET Core でクロスサイト リクエスト フォージェリ (XSRF/CSRF) 攻撃を防止する」に述べられている CSRF 攻撃の仕組みを図示したものです。

どういうことかと言うと、(1) ユーザー認証が必要な正規サイトにログインしたクライアントが、ログアウトせず(即ち、認証クッキーを持ったまま)作業を続ける。(2) その後、悪意のあるサイトにアクセスして攻撃用の HTML フォームが仕組まれたページを閲覧し、その攻撃用の HTML フォームのボタンをクリックする。(3) それにより正規サイトに POST 要求がかかるが、認証クッキーも同時に送信されるので、認証ユーザーが実行を許可されているアクションを実行できる・・・ということです。

その攻撃を防止するための組み込みの機能について以下に書きます。

(1) MVC の場合

Microsoft のドキュメント「Web Stack Runtime XSRF の軽減策」に書いてあるように、2 つの CSRF 対策用トークンを用います。

まず、サーバが入力フォームをクライアントに送信する際、クッキーと隠しフィールドに CSRF 対策用トークンを設定してクライアントに渡します。

クライアントが入力フォーム上で入力を済ませて、サーバーにフォームを POST 送信する際、同時にクッキーと隠しフィールドの CSRF 対策用トークンもサーバーに送信されます。

サーバーは、両方のトークンが比較チェックに合格した場合にのみ要求の続行を許可します。不合格の場合はサーバーエラーとなります。

上の図で、「罠を仕込んだページ」の「攻撃用の HTML フォーム」には CSRF 対策用トークンを持った隠しフィールドは存在しないので比較チェックは不合格となり、CSRF 攻撃を防ぐことができるということになります。

(2) Web Forms の場合

Web Froms には状態管理の手段の一つに ViewState というものがあって、隠しフィールドに状態を保存してクライアントに送信し、ポストバックされたときにサーバー側で隠しフィールドから状態情報を取得するという仕組みになっています。

ViewState には EnableViewStateMac という改ざんを検証する機能があって、デフォルトで有効になっているので、ある程度それで CSRF を防ぐことができます。

ただ、それだけでは不十分だそうで、Page.ViewStateUserKey プロパティを利用して、ViewState に個々のユーザーの 識別子を割り当てることが推奨されています。

ViewStateUserKey プロパティを設定した場合、ASP.NET は、ポストバックによってクライアントから送信されてきた隠しフィールドの ViewState から識別子を抽出し、実行中のページの ViewStateUserKey と比較します。 2 つが一致する場合要求は正当と見なされ、それ以外の場合は例外がスローされる仕組みになっているそうです。(そのあたりの詳しい説明は ViewStateUserKey を見てください)

Visual Studio 2022 のテンプレートで作る Web アプリケーションプロジェクトの場合、マスターページ (Site.Master.cs) に、Page.ViewStateUserKey プロパティを利用して CSRF 対策を強化するためのコードが実装されています。

それがどうなっているかを説明します。

上の Microsoft のドキュメントでは Session.SessionID を Page.ViewStateUserKey プロパティに設定する例が紹介されています。ただ、Session を使わない限り SessionID は発行されないので、そうはできないケースがあります。

なので、Site.Master.cs では、SessionID に代えて Guid の文字列を Page.ViewStateUserKey プロパティに設定しています。そのコードは以下の通りです。

Page_Init で、要求ヘッダに __AntiXsrfToken という名前のクッキーが含まれ、かつ、その値を Guid にパースできる場合、そのクッキーの値を Page.ViewStateUserKey に設定しています。

要求ヘッダに __AntiXsrfToken という名前のクッキーが含まれない場合、含まれていてもその値を Guid にパースできない場合は、Guid を生成してその文字列を Page.ViewStateUserKey に設定します。さらに、__AntiXsrfToken という名前のクッキーを作成し、その値に生成した Guid の文字列を設定してクライアントに送信しています。

using System;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using Microsoft.AspNet.Identity;

namespace WebForms1
{
    public partial class SiteMaster : MasterPage
    {
        private const string AntiXsrfTokenKey = "__AntiXsrfToken";
        private const string AntiXsrfUserNameKey = "__AntiXsrfUserName";
        private string _antiXsrfTokenValue;

        protected void Page_Init(object sender, EventArgs e)
        {
            // 以下のコードは、XSRF 攻撃からの保護に役立ちます
            var requestCookie = Request.Cookies[AntiXsrfTokenKey];
            Guid requestCookieGuidValue;
            if (requestCookie != null && 
                Guid.TryParse(requestCookie.Value, 
                              out requestCookieGuidValue))
            {
                // Cookie の Anti-XSRF トークンを使用します
                _antiXsrfTokenValue = requestCookie.Value;
                Page.ViewStateUserKey = _antiXsrfTokenValue;
            }
            else
            {
                // 新しい Anti-XSRF トークンを生成し、Cookie に保存
                _antiXsrfTokenValue = Guid.NewGuid().ToString("N");
                Page.ViewStateUserKey = _antiXsrfTokenValue;

                var responseCookie = new HttpCookie(AntiXsrfTokenKey)
                {
                    HttpOnly = true,
                    Value = _antiXsrfTokenValue
                };
                if (FormsAuthentication.RequireSSL && 
                    Request.IsSecureConnection)
                {
                    responseCookie.Secure = true;
                }
                Response.Cookies.Set(responseCookie);
            }

            Page.PreLoad += master_Page_PreLoad;
        }

        // ・・・中略・・・

    }
}

さらに、初期画面の要求の処理を行う際 Page_PreLoad で ViewState に Page.ViewStateUserKey の文字列とユーザー名(ユーザーが認証を受けている場合)を設定し、ポストバックの際はそれらが一致しているか否かの検証を行い一致しない場合は例外をスローするという操作を、上のコードで Page.PreLoad にアタッチしたイベントハンドラ master_Page_PreLoad で行っています。

そのコードは以下の通りです。

protected void master_Page_PreLoad(object sender, EventArgs e)
{
    if (!IsPostBack)
    {
        // Anti-XSRF トークンを設定します
        ViewState[AntiXsrfTokenKey] = Page.ViewStateUserKey;
        ViewState[AntiXsrfUserNameKey] = 
                    Context.User.Identity.Name ?? String.Empty;
    }
    else
    {
        // Anti-XSRF トークンを検証します
        if ((string)ViewState[AntiXsrfTokenKey] != _antiXsrfTokenValue ||
            (string)ViewState[AntiXsrfUserNameKey] != 
                         (Context.User.Identity.Name ?? String.Empty))
        {
            throw new InvalidOperationException(
                "Anti-XSRF トークンの検証が失敗しました。");
        }
    }
}

ただし、上に書いた「攻撃用の HTML フォーム」で POST 要求するような場合は ViewState が含まれないので ViewState Mac の検証機能は動きません。また、ポストバックと判定されないので master_Page_PreLoad によるトークンの検証もされません。

それで CSRF 対策になるのかというのが疑問でしたが、普通に ASP.NET Web Forms アプリでやるように、ユーザー入力に TextBox コントロール使い、Button コントロールをクリックしてポストバックし、Button の Click イベントで処置を行うケースでは問題なさそうです。

「攻撃用の HTML フォーム」から送信された値は TextBox.Text に代入されることはなく(TextBox.LoadPostData が呼ばれないので)、Click イベントも発生しないのでサーバー側では何も起こりません。

ちょっと問題なのは「攻撃用の HTML フォーム」は POST 要求を出すのでそれを受けた正規 Website は応答を返すという点です。予期せぬ応答をもらったユーザーはびっくりすると思いますが、その対策までは上のコードには実装されていません。

Tags: , , ,

ASP.NET

User-Agent Client Hints

by WebSurfer 21. April 2023 15:27

Windows OS のブラウザから、要求ヘッダに含まれて Web サーバーに送信されてくる従来の User-Agent では、OS のバージョンが Windows 10 なのか Windows 11 なのかの識別ができません。

しかし、Microsoft のドキュメント「User-Agent クライアント ヒントを使用してWindows 11と CPU アーキテクチャを検出する」によると、User-Agent Client Hints を利用すればサーバー側で OS のバージョンが Windows 10 なのか Windows 11 なのかの識別ができるとのことなので検証してみました。

なお、ドキュメントには書いてありませんが、HTTPS 通信に限ると言うところに注意してください。また、現時点では実験的な機能であり、対応ブラウザも Edge, Chrome, Opera に限られている点にも注意してください。(参考 : ユーザーエージェントクライアントヒント API

ブラウザはデフォルトで要求ヘッダに User-Agent Client Hints 関係の情報 sec-ch-ua, sec-ch-ua-mobile, sec-ch-ua-platform を含めます (HTTPS 通信の場合に限ります)。Windows 10 22H2 の Microsoft Edge 112.0.1722.48 の場合以下の通りとなります。

sec-ch-ua: "Chromium";v="112", "Microsoft Edge";v="112", "Not:A-Brand";v="99"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"

上の情報からは OS が Windows だというのは分かりますがバージョンは分かりません。バージョン情報もブラウザが送信するようにするには、Web サーバーからの応答ヘッダに、

Accept-CH: Sec-CH-UA-Platform-Version

を含めます。そうすると、次の要求からブラウザが要求ヘッダに、

sec-ch-ua-platform-version: "10.0.0"

という OS のバージョン情報を含めて送信してくれるようになります。 (上の例で OS は Windows 10 22H2)

ただし、ブラウザからの最初の要求にはバージョン情報は含まれず、次の要求からになります。そこが使いにくい点かもしれません。

そこが問題であれば、上に紹介した Microsoft のドキュメントの「検出パフォーマンスの最適化 Critical-CH」セクションに書いてあるように、Accept-CH と一緒に Critical-CH ヘッダーを応答ヘッダに含めてクライアント(ブラウザ)に送るのが良さそうです。

具体的には、例えば、Web サーバーからの応答ヘッダに、

Accept-CH: Sec-CH-UA-Platform-Version
Critical-CH: Sec-CH-UA-Platform-Version 

を含めると、ブラウザは最初の応答を受けた後、直ちに再度要求を出し、その要求ヘッダには Critical-CH ヘッダーに指定された情報(この例では sec-ch-ua-platform-version: "10.0.0")を含めて送信してくれます。

以降は、ユーザーがブラウザの「Cookie およびその他のサイト データ」を消去しない限り、ブラウザは指定された情報を送信し続けます。

また、再度 Accept-CH と一緒に Critical-CH ヘッダーを応答ヘッダに含めて送信しても 2 回要求が出るということはありません。

以下に、Fiddler で要求・応答をキャプチャした画像を貼って説明を加えておきます。

(1) 最初の要求

(1) 最初の要求

Default.aspx.cs の Page_Load に以下の C# コードを実装して要求をかけたものです。

Response.AppendHeader("Accept-CH", "Sec-CH-UA-Platform-Version");
Response.AppendHeader("Critical-CH", "Sec-CH-UA-Platform-Version");

画像の青枠部分がブラウザからの要求ヘッダに含まれる User-Agent Client Hints 情報です。この時点では、バージョン情報 Sec-CH-UA-Platform-Version は含まれていません。

画像の赤枠部分に示した通り、Web サーバーからの応答ヘッダには上の C# コードで指定した Accept-CH と Critical-CH が設定されています。

(2) 自動的に再度要求が出る

(2) 自動的に再度要求が出る

ブラウザは Web サーバーからの応答ヘッダの Accept-CH と Critical-CH を見て自動的に再度 Default.aspx に要求を出します。

画像の青枠の通り要求ヘッダにバージョン情報 sec-ch-ua-platform-version が含まれています。

(3) 他のページを要求

(3) 他のページを要求

Web サーバーからの応答ヘッダに Accept-CH, Critical-CH を含まない他のページ(この例では Contact.aspx)を要求してみます。

画像の青枠の通り要求ヘッダに sec-ch-ua-platform-version が含まれています。

(4) 再度 Default.aspx を要求

(4) 再度 Default.aspx を要求

ブラウザから再度 Default.aspx を要求してみます。画像の赤枠の通り Web サーバーからの応答ヘッダには Accept-CH と Critical-CH が含まれていますが、、上の (2) のように再度要求が出ることはありません。


ASP.NET の C# のコードでブラウザからの要求ヘッダに含まれる Sec-CH-UA-Platform および Sec-CH-UA-Platform-Version の情報を取得するには以下のようにします。

string platform = Request.ServerVariables["HTTP_SEC_CH_UA_PLATFORM"];
string version = Request.ServerVariables["HTTP_SEC_CH_UA_PLATFORM_VERSION"];

上のコードで、platform, version に取得された文字列はダブルクォーテーション " で囲まれるので注意してください。

Tags: ,

ASP.NET

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  June 2023  >>
MoTuWeThFrSaSu
2930311234
567891011
12131415161718
19202122232425
262728293012
3456789

View posts in large calendar