LoginSignup
8
11

More than 5 years have passed since last update.

Gatlingによるcsrf対策の突破法

Last updated at Posted at 2016-11-04

背景

以前アプリのチューニングを行った際、Gatlingを用いて負荷テストを実施しました。
※Gatlingの詳細や細かい使い方は公式ドキュメントを参照ください。

Gatlingのレコード機能を使えば、GUIで行った操作を記録し、自動でコードに落とし込んでくれます。

しかし、csrfトークンの値など、動的に値が変化していくものについては、その操作を記録した時点での値で固定されてしまいます。アプリ側のcsrf対策を切れれば話は早いのですが、そういうわけにもいきません。なんとかこのcstfトークンをシナリオ実行中に自動で取得し、動的にその値を変更させながらシナリオを実行させる必要があります。

今回実際にいろいろとやってみて、かなりハマってしまったので備忘録的に残しておきます。

やりたいこと

csrfトークンの値を動的に取得し、アプリケーションのcsrf対策をONにしたままgatlingのシナリオを実施したい

今回実行した操作の流れ

  • アプリケーションにログインする(ログインのエンドポイントに対しPOSTリクエストを投げる)
    • レスポンスヘッダにcstfトークンが含まれるので、コイツを取得する
  • ログイン後、トップページからコメントを投稿
    • 上記で取得したcsrfトークンを用いて投稿処理を行う

最終的に出来上がったソースコードの雰囲気はこんな感じ

val httpProtocol = http
    .baseURL("https://dummy-domain")
    #その他の設定
    .disableFollowRedirect

val $token = "${csrf_token2(0)}"

#headersの設定など

.exec(http("ログイン")
  .post("/dummy/login")
  .check(headerRegex("Set-Cookie", "dummy_csrf_token=(.*); path").findAll.saveAs("csrf_token2"))
  .check(status.is(302))
  .formParam("email", "dummy_address")
  .formParam("password", "dummy_password")
  .formParam("dummy_token", "${csrf_token}"))
.exec(http("トップ画面")
  .get("/dummy/top"))
.exec(http("投稿")
  .post("/dummt/post")
  .body(StringBody(s"""{"body":"テスト, "dummy_csrf_token":"$token"}""" )).asJSON)

やりたいことをもう少し具体的にすると、

  • /dummy/loginに対してPOSTを投げた時に返ってくるレスポンスヘッダのSet−Cookieの値を取得し、保存すること

となります。

やり方

公式ドキュメントを見れば、以下のように書いてあります。

.headerRegex(headerName, pattern)

よってこのheaderRegexを使えば、レスポンスヘッダの中身を取得できる様子。なので今回は

.check(headerRegex("Set-Cookie", "dummy_csrf_token=(.*);").saveAs("csrf_token"))

みたいな感じで指定してやればcsrfトークンの値を取れると思っていました。

ハマッた内容

Set-Cookie(同じheaderName)が複数ある場合、.findAllを指定する必要がある

今回、ログインのPOSTに対するレスポンスヘッダにはSet-Cookieが複数存在し、上記のやり方だとそのうちの一つしか取得できないことがわかりました。
なのでSet−Cookieの値全てを取得し、そのうちdummy_csrf_tokenの値を正規表現で抽出する必要があります。

.check(headerRegex("Set-Cookie", "dummy_csrf_token=(.*);").findAll.saveAs("csrf_token"))

このように.findAllを使えば該当する全てのheaderNameの値を取得できるようになります。
この時、取得した値は配列で返ってくるので、呼び出しの形を配列に変える必要があります。

val $token = "${csrf_token2(0)}"

ログイン後に自動でリダイレクトしないようにする必要がある

これでいけるようになったかと思いきや、なかなかうまくいきません。

.post("/dummy/login")
.check(headerRegex("Set-Cookie", "dummy_csrf_token=(.*); path").findAll.saveAs("csrf_token2"))

一見これでPOSTリクエストに対するレスポンスヘッダのSet-Cookieの値をとってきてくれそうですが、どうやらログイン直後のリダイレクト先へのGETリクエストに対するレスポンスヘッダの値を見ているようでした。

gatlingではリダイレクトのステータスコードを受け取ると、自動でリダイレクト先のURLにGETでアクセスしてくれます。
なので、httpProtocolに.disableFollowRedirectを付与し、自動でリダイレクトしないように設定する必要があります。

val httpProtocol = http
    .baseURL("https://dummy-domain")
    ...
    .disableFollowRedirect

こうするとリダイレクトのステータスコードを受け取っても、自動でリダイレクトしないようになります。
これでPOSTリクエストに対するレスポンスヘッダの値にアクセスできるようになりました。

もう一つ注意点として、自動リダイレクトを切ると、操作としてはきちんとログインできているのですが、gatlingの見かけ上はfailとなってしまいます。なので受け取ったステータスコードは300系であってますよ、ということをgatlingに教えてやる必要があります。これは.checkのstatus.isを使えば問題ありません。

.post("/dummy/login")
.check(headerRegex("Set-Cookie", "dummy_csrf_token=(.*); path").findAll.saveAs("csrf_token2"))
.check(status.is(302))

最後に

JMeterかGatlingか結構迷ったのですが、慣れてしまえばGatlingが使いやすいのかなあという感じがしました(ほとんどJMeter使ってないのであれですが)。

8
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
11