WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

ASP.NET MVC の View での CS8602 対応

by WebSurfer 2023年8月1日 18:24

ASP.NET Core MVC アプリで、以下の画像のように View でナビゲーションプロパティ経由でデータを取得するコードを書くと、プロパティの型が null 許容参照型の場合は "CS8602: null 参照の可能性があるものの逆参照です" という警告が出ます。これにどう対応するかという話を書きます。

null 許容の警告

(ターゲットフレームワークが .NET 6.0 以降のプロジェクトでは全体で「Null 許容」オプションが有効になっています。リバースエンジニアリングを使って生成する Entity Framework 用のコンテキストクラス、エンティティクラスも、基になるデータベースの構造に合わせて、プロパティに null 許容参照型が使われるようになります)

どう対応するかですが、結論から書きますと、何しなくても問題は出なかったです。上の画像のコードで Category, Supplier が null でも NullReferenceException がスローされることはなくアプリは完了し、ブラウザ上ではそれらの項目は空白となります。

以下に、そのあたりのことをもう少し詳しく書きます。

上の画像の View には Controller から IEnumerable<Product> 型の Model が渡されています。そのコードは以下の通りです。Include を使って Category, Supplier も取り込んでいるところに注意してください。

var northwindContext = _context.Products
                       .Include(p => p.Category)
                       .Include(p => p.Supplier);

return View(await northwindContext.ToListAsync());

上の画像で警告が出ているコードの item というのは Product クラスのオブジェクトになります。

この記事の例では、既存の SQL Server サンプルデータベース Northwind の Products, Suppliers, Categories テーブルからリバースエンジニアリングでコンテキストクラスとエンティティクラスを作成して使っており、そのエンティティクラスの一つが Product クラスです。

Product クラスの基になる SQL Server の Products テーブルは下の画像の構成となっています。赤枠で示した SupplierID, CategoryID フィールドは NULL 可で、Suppliers, Categories テーブルに FK 制約が張られています。

SQL Server の Products テーブル

このテーブルからリバースエンジニアリングで生成される Product エンティティクラスの Category, Supplier ナビゲーションプロパティは、以下の通り null 許容参照型となります。

[ForeignKey("CategoryId")]
[InverseProperty("Products")]
public virtual Category? Category { get; set; }

[ForeignKey("SupplierId")]
[InverseProperty("Products")]
public virtual Supplier? Supplier { get; set; }

それゆえ、この記事の一番上の画像のように、View でそれらのナビゲーションプロパティ経由で値を取得しようとすると、"CS8602: null 参照の可能性があるものの逆参照です" という警告が出ます。

Products テーブル の SupplierID, CategoryID フィールドは NULL 可なので、Product エンティティクラスの Category, Supplier ナビゲーションプロパティから取得できる値は null になることがあります。

実際に Products テーブル の SupplierID, CategoryID フィールドを NULL にすると、View のコードで item.Category, item.Supplier は null になります。下の画像がその例です。item の中の Category と Supplier を見てください。

デバッグ画面

ということで、NullReferenceException がスローされないよう対処する必要がある・・・と思っていましたが、実際は、上にも書きましたように、NullReferenceException がスローされることはなくアプリは正常に終了し、ブラウザ上での表示はそれらの項目は空白となります。

ただし、そのあたりのことを書いた Microsoft のドキュメントは見つけることができませんでした。なので、試した結果からそう言っているだけで、どういう状況でも 100% 問題ないかまでは自信がありません。

(Microsoft のドキュメントの「必須のナビゲーション プロパティ」のセクションに "必要な依存が適切に読み込まれている限り (例: 経由 Include)、ナビゲーション プロパティにアクセスすると、常に null 以外が返されます" と書いてありますが、違う話のような気がします)

どうしても対応したいという場合はどうしたらいいでしょうか? Microsoft のドキュメント「null 許容の警告を解決する」に書いてある "変数を逆参照する前に変数が null でないこと確認する" ということになりますが、@Html.DisplayFor の引数のラムダ式上ではそれはできないようです。

ちなみに、三項演算子を使うと "InvalidOperationException: Templates can be used only with field access, property access, single-dimension array index, or single-parameter custom indexer expressions." というエラーになります。null 合体演算子を使って item.Supplier?.CompanyName ?? "" のようにすると "CS8072 式ツリーのラムダに null 伝搬演算子を含めることはできません" というエラーになります。

というわけで、"変数を逆参照する前に変数が null でないこと確認する" には以下のようにすることになりそうです。

<td>
    @{
        var categoryName = (item.Category != null) ?
            item.Category.CategoryName : "";
    }
    @Html.DisplayFor(modelItem => categoryName)
</td>
<td>
    @{
        var supplierName = (item.Supplier != null) ?
            item.Supplier.CompanyName : "";
    }
    @Html.DisplayFor(modelItem => supplierName)
</td>

そこまでやらなくても、! (null 免除) 演算子を使って警告を消すだけでもよさそうな気はしますが。

Tags: , , , ,

CORE

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

by WebSurfer 2023年5月3日 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 2023年5月2日 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 を使わない限り Session cookie が発行されないので、そうはできないケースがあります。

なので、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

About this blog

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

Calendar

<<  2024年4月  >>
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar