Giraffe で CSRF 対策したい


この記事は「F# Advent Calendar 2017」に参加しています。

Giraffe はかつて ASP.NET Core Lambda という名前で、名前が示す通り ASP.NET Core 向けの関数型プログラミングによるウェブアプリケーションフレームワークです。ここでいう関数型プログラミングは当然 F# のことで、 F# の特徴的な機能であるコンピュテーション式を利用するといい感じに書くことができます。

type MessageModel = { Message : string }

let webApp =
   choose [
      GET >=> route "/" >=> razorHtmlView "Index" { Message = "Hello, World!" }
      setStatusCode 404 >=> text "Not Found"
   ]

上記のように魚演算子 >=> を利用して関数(ハンドラー)を連鎖させます。

さて、ここからが本題になるのですが、 ASP.NET Core MVC では、 CSRF 対策として、ビューで Html.AntiforgeryToken() を呼び、コントローラーのメソッドに ValidateAntiForgeryToken 属性を付与します。

<form>
   @Html.AntiforgeryToken
   <input type="submit">
</form>
[Controller]
public class MyController : Controller
{
   [HttpPost]
   [ValidateAntiForgeryToken]
   public IAsyncResult DoSomething()
   {
      // ...
   }
}

Giraffe は関数型プログラミングの作法で前述の魚演算子によるハンドラーの連鎖によりリクエストの処理を行います。この関数がコントローラーに相当するものであるので、 ASP.NET Core MVC のコントローラーそのものは利用しません。そのため、 ValidateAntiForgeryToken に相当するハンドラーを作成する必要があります。

ASP.NET Core MVC では、 IAntiforgery.ValidateRequestAsync を呼んで AntiforgeryToken の検証を行います。検証に失敗した場合には、例外が投げられます。したがって IAntiforgery.ValidateRequestAsync を呼ぶようなハンドラーを作ってあげるだけで、 ValidateAntiForgeryToken に相当することが実現できます。

let validateAntiforgeryToken : HttpHandler = fun next ctx -> task {
   let antiforgery = ctx.GetService<IAntiforgery>()
   do! antiforgery.ValidateRequestAsync(ctx)
   return! next ctx
}

以下のように、他のハンドラーの間に挟んでやれば良いでしょう。

POST >=> validateAntiforgeryToken >=> ...

検証に失敗した場合は例外が投げられると GiraffeErrorHandlerMiddleware が例外をキャッチしてエラーハンドラーに処理をつなぎます。普通は例外が投げられたときに対応するハンドラーは、 HTTP ステータス 500 で返すものを用意するでしょうから、上記のコードで AntiforgeryToken の検証に失敗した場合は 500 が返ってきます。 400 を返すようにするには、以下のように try-withAntiforgeryValidationException を捕捉してレスポンスを返すようにすれば良いでしょう。

try
   do! antiforgery.ValidateRequestAsync(ctx)
   return! next ctx
with
| :? AntiforgeryValidationException as ex ->
   let handler = clearResponse >=> setStatusCode 400 >=> text ex.Message
   let interrupt : HttpFunc = fun _ -> Task.FromResult(None)
   return! handler interrupt ctx

おまけ

setStatusCodeint をとるので、誤って変なステータスコードを返してしまう可能性があります。これを防ぐために HttpStatusCode 列挙型を使いたい場合は、以下のようにシャドーイングしてしまうのが良いと思います。

let setStatusCode : HttpStatusCode -> HttpHandler =
   LanguagePrimitives.EnumToValue >> setStatusCode

... >=> setStatusCode HttpStatusCode.BadRequest >=> ...