ASP.NET Coreでファイルアップロードをマルチパートから読み取る場合、formタグで自動生成された偽造防止トークンをValidateAntiForgeryTokenフィルターで処理するとエラーになります。このエラーの解決策の1つとしてCookieのヘッダーから偽造防止トークンを読み取る方法を紹介します。
WSL v1 | Ubuntu18.04 LTS |
App | ASP.NET Core 3.1.302 |
Ubuntu18.04 LTSでのASP.NET Coreの開発環境は、こちらをご参照ください。
ファイルアップロード
Javascriptを使用してファイルをコントローラーのアクションにストリーミングします。アクションでは、マルチパートセクションを読み取りアップロードされたファイルを処理します。ここでは、formタグ内にはinput type=”file”要素のみとします。
Uploadアクション
Uploadアクションは、公式のストリーミングを使用して大きいファイルをアップロードするを参考にしています。モデルバインドを使用せず、Requestを参照して処理します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
[HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Upload() { // Content-Typeヘッダーからmultipart/が含まれるか調べます(リクエストの本文のタイプ) if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType)) { _logger.LogError("failure is not multipart contentTyp request"); return BadRequest(); } var formAccumulator = new KeyValueAccumulator(); // デリミターを取得します var boundary = MultipartRequestHelper.GetBoundary(MediaTypeHeaderValue.Parse(Request.ContentType), _defaultFormOptions.MultipartBoundaryLengthLimit); var reader = new MultipartReader(boundary, HttpContext.Request.Body); // セクションを読み込みます var section = await reader.ReadNextSectionAsync(); ・・・ } |
例外発生
UploadアクションにJavascriptからアクセスするとawait reader.ReadNextSectionAsync()で次の例外が発生します。
1 2 3 |
ERROR|Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware |An unhandled exception has occurred while executing the request. System.IO.IOException: Unexpected end of Stream, the content may have already been read by another component. |
このエラーを解決するために、まずは偽造防止オプションを見直します。
偽造防止オプション
通常、formタグのmethodがGETでない場合、既定で偽造防止トークンを生成します。そして、POSTの場合、その偽造防止トークンはBodyに含まれます。偽造防止トークンをヘッダーに格納するために、Startup.ConfigureServicesの偽造防止オプションをカスタマイズします。
Startup.ConfigureServices
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
services.AddAntiforgery(options => { // Cookie名 options.Cookie.Name = "[CookieName]"; // スクリプトからのアクセス制御 options.Cookie.HttpOnly = true; // セキュリティポリシー options.Cookie.SecurePolicy = (flag) ? CookieSecurePolicy.Always : CookieSecurePolicy.None; // 偽造防止トークンをレンダリングする非表示フォームフィールドの名前 options.FormFieldName = "[AntiforgeryFieldname]"; // ★add★ 偽造防止トークンを埋め込むヘッダーの名前 options.HeaderName = "X-CSRF-TOKEN"; options.SuppressXFrameOptionsHeader = false; } |
Cookieに偽造防止トークンを設定
偽造防止トークンを生成してCookieに設定するフィルター属性をストリーミングを使用して大きいファイルをアップロードするを参考に作成します。
フィルター属性を作成
ActionResultオブジェクトが実行される前にCookieに偽造防止トークンを設定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class GenerateAntiforgeryTokenCookieAttribute : ResultFilterAttribute { public override void OnResultExecuting(ResultExecutingContext context) { var antiforgery = context.HttpContext.RequestServices.GetService<IAntiforgery>(); var tokens = antiforgery.GetAndStoreTokens(context.HttpContext); context.HttpContext.Response.Cookies.Append( // Javacriptで指定する名前 "CSRF-TOKEN", tokens.RequestToken, // 偽造防止オプション構成では、Javascriptでのアクセスを許可しないように設定したが、 // 今回は、Javascriptでヘッダーに偽造防止トークンを設定する必要があるため許可。 new CookieOptions() { HttpOnly = false }); } public override void OnResultExecuted(ResultExecutedContext context){} } |
フィルター属性を指定
GenerateAntiforgeryTokenCookieAttributeをビューアクションに指定します。
1 2 3 4 5 6 |
[HttpGet] [GenerateAntiforgeryTokenCookie] public IActionResult Index() { return View(); } |
Javascriptで偽造防止トークンを送信
次にUploadアクションにアクセスするクライアントの実装です。
偽造防止トークンの自動生成を無効
asp-antiforgery=”false”をformタグに追加して偽造防止トークンを明示的に無効にします。
1 2 3 |
<form id="form-upload" method="POST" enctype="multipart/form-data" asp-antiforgery="false"> ・・・ </form> |
偽造防止トークンの送信
クライアントの実装(TypeScript)は、分かり易くfetch部分のみを抜粋します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// ヘッダーを作成 const getCookie = (cname: string): string => { const name = cname + "="; const decodedCookie = decodeURIComponent(document.cookie); const ca = decodedCookie.split(";"); for(let i = 0; i <ca.length; i++) { let c = ca[i]; while (c.charAt(0) == " ") { c = c.substring(1); } if (c.indexOf(name) == 0) { return c.substring(name.length, c.length); } } return ""; } // Uploadアクションにアクセス const response = await fetch(url, { method: "post", headers: { "X-CSRF-TOKEN": getCookie("CSRF-TOKEN") }, body: new FormData([HTMLFormElement]), credentials: "same-origin" }); |
これで、Javascriptからアクセスすると偽造防止トークンの検証、Updateアクションが処理されます。