ASP.NET Core: ファイルアップロードのバリデーション

ASP.Net Core(3.1)を使ったファイルアップロードに関する考察です。
元ネタはマイクロソフトのサイトですが、記載内容が私には難しかったり、業務で使用するために悩む部分があったので独自に纏めてみました。

バッファリング vs. ストリーミング

ASP.Net Coreではサーバ側の実装方式として、バッファリングとストリーミングがある。
動画、高精細画像、大量ファイルのような数GB単位のアップロードが想定されるのであればストリーミング型をお薦めしますが、このような明確な想定がない場合は実装が容易なバッファリングから始めることをお薦めします。(基本はバッファリングで実装し、必要に応じてストリーミング型を使用するイメージ。)
なお、上記は一般論であり、性能要件やサーバスペックによって判断が変わる場合もあります。

方式メリットデメリット
バッファリング
(アップロードファイルを一時的にメモリやハードディスク上に全て保存してから、サーバ側処理を行う)
実装が容易
アップロードファイルの最低限の検証はランタイムが行うので、プログラマは以降の処理のみ実装すれば良い。
(アップロードしたファイルは、モデルバインディングという仕組みでオブジェクト(IFormFile)としてメソッドに引き渡されるので実装が楽です。)
大型のファイルアップロードには不向き
一時的ではあるが、ファイルサイズに応じてサーバリソースを消費するため。
ストリーミング
(アップロードファイルを受信しながらサーバ側処理を行う)
大型のファイルアップロードが可能
サーバ上にファイルは保存されないため、バッファリングに比べ消費されるサーバリソースは少ない。
実装量が多く、難易度が高い
プログラマはマルチパート仕様を理解し、そのデータの解析や検証、ファイル抽出やデータの読み取りを実装する必要がある。実装の仕方によってサーバリソースを消費してしまう可能性があるので、神経質な実装が必要となる。

(参考)上記の選定に関わらず、クライアントから送信されるデータや送信方法(マルチパート)は変わらず、クライアント側の実装方法に影響はありません。(上記はクライアントから受信したマルチパートデータをどう扱うかの話であるため。)

アップロードするファイルサイズの上限チェック

前述のバッファリングの実装の場合、アップロードファイルサイズ=サーバ上で使用するメモリやハードディスクサイズになります。そのため、サーバリソースを超えるような数十~数百GB等のファイルをアップロードすることで、サーバリソースを枯渇させることができます。IIS等の今日のアプリサーバでは、アップロードファイルの上限チェックが必ず実装されているので安全です。
アプリサーバでの上限チェックは、特定のエラーページを表示させる等の単純なことしかできない制限があります。ユーザが間違えて大きなファイルをアップロードした際に、このようなエラーページを表示させるのは、あまりユーザフレンドリではないので、次のような段階的な上限チェックをお薦めします。

上限チェック/設定箇所用途備考
業務ロジックユーザの誤操作の検知ファイル内容を読み取ってサイズをチェック
コントローラ/アクション特定機能でアップロード上限を引き上げるRequestSizeLimit/RequestFormLimits属性やオプションで指定
アプリサーバ攻撃からの保護アプリサーバの設定ファイルで定義する。
IISの場合、applicationhost.configのrequestLimits要素で指定する。既定では3,000,000バイト(約28.6MB)である。

参考ですが、アプリ開発者がサーバによる上限があることを知らず、結合試験等で問題になることが多いので、設計レベルでアプリとサーバサーバ側でのチェック仕様を明確にした方が良いと思います。

マルウェアスキャナの使用是非

アップロードした画像、Excel、PDF等のファイルを処理するサーバ側ライブラリの脆弱性が攻撃される場合や、後でそのファイルをダウンロードするユーザが攻撃される可能性があります。
マルウェアをチェックする環境(マルウェアスキャナ)を準備するのは大変なので、そこまでチェックするのか悩みどころです。有識者に相談しても「業務要件による。」と言われ、マイクロソフトはサードパーティー製品によるマルウェアスキャンを推奨するだけで、具体的なガイドを示してくれません…
当然ですが、コストや期間に余裕があるならマルウェアスキャンの環境を準備するべきだと思います。難しい場合、サーバ側の脆弱性を可能かなぎり低減するために信頼性のあるライブラリを選定しつつ、アップロードしたファイルに関してはスキャンできない旨の免責事項と、マルウェアスキャンを諦める方法もあるかと思います。このような選択をしたとしても、後でマルウェアスキャンが行えるような設計(チェック処理の内部を隠蔽したり、マルウェアスキャンを呼び出す実装にするが内部は空実装等)にすることをお薦めします。
特定組織だけでなく不特定多数のユーザと共有する場合は、問題発生時のコントロールが難しく、影響が広くなるので、マルウェアスキャンが必須だと思います。(フリーソフトのダウンロードサイトにマルウェアが仕込まれた時のように、免責事項になっていたとしても実質的な運営側の問題を問われると思うので。)

なお、マルウェアスキャナを導入する際の考察です。

  • サーバ上のアンチウイルスチェックを有効にするとアプリが誤動作する場合があり、これまでの経験では無効にされていることが多いです。仮に有効だった場合、前述のバッファリングのようにアップロードしたファイルを一時的にファイル保存した際に、アンチウイルスのリアルタイムスキャンに横取り(隔離)され、アプリ側が期待通りに動作しない可能性があります。
  • サーバのアンチウイルスチェックではなく、サードパーティーのアンチウイルス機能(マルウェアスキャナ)をインストールし、アプリからマルウェアスキャナにチェックを依頼するような構成で実現できます。
    マルウェアスキャナはサーバリソースを比較的多く消費します。ファイルアップロードが頻繁に行われる場合、応答性が劣化する可能性があるので、アプリとは別のサーバに構築する必要があります。
  • Azure環境でお手軽に対応する場合、オープンソースのClamAVを使ったマルウェアスキャナの事例が多くあり、NuGetでAPI(nClam)をインストールすることも可能です。その他、ポピュラーなアンチウイルスのベンダが提供するSDKで実現できるのではないかと思うのですが、公開されている情報は乏しく、ベンダへの問い合わせや相談が必要となる。導入や運用に追加のコストがかかることもまり、明確な方針がないと着手しづらい。

クライアント側でのアップロード処理の実装

クライアント側でのアップロードの実装は、XMLHttpRequest(XHR)やFetch APIを使った例が記載されています。各ブラウザでのサポート情報はこちらを参考とのこと。
Fetch APIは比較的新しいAPIでより簡単な実装が可能ですが、従来からのXMLHttpRequestと完全に同じことができるわけではないようです。例えば、XMLHttpRequestで行えたアップロード状態の把握は、Fetch APIでは難しいようです。
後々のトラブルに備え、XMLHttpRequestや、これをラップしてより容易に実装できるjQueryのAjax()メソッド、jQueryのアップロードライブラリの使用をお薦めします。将来的にドラッグ&ドロップ等のより高度なUIを想定するなら、jQueryライブラリの使用をお薦めします。。

ストリーミングによる実装と他機能との干渉

マイクロソフトのページでストリーミングのサンプルが記載されています。なぜこのような実装になっているのか理解に苦しんだので、その補足です。

ストリーミングによる実装の場合、HTTP要求のボディ内容をMultipartReaderで読み取る必要がありますが、偽造防止機能(ValidateAntiForgeryToken属性の指定時)やモデルバインディングが有効になると、読み取りができません。この問題を回避するためにサンプルでは、各機能を次のように制御しています。

  • 偽造防止機能:XSRF/CSRF攻撃を防止するために既定ではフォームに偽造防止トークンをhidden項目として埋め込んでいます。偽造防止のチェックを行うためにボディ部を読み込んでいると思われます。
    サンプルではCookieに偽造防止トークンを設定し、ボディ部を読み取らないようにしています。偽造防止機能やHTTPヘッダやCookieに対するトークンの埋め込み方法等はこちらに説明があります。
  • モデルバインディング:フォームの項目を引数の自動的に設定するためにボディ部を読み込んでいると思われます。(アップロード先となるアクションメソッドにおいて、引数がない場合は当該事象は発生しませんが、引数を指定すると当該事象が発生します。)
    サンプルでは、明示的にモデルバインディングを無効にするためのDisableFormValueModelBinding属性を実装しています。