適当に俳句投稿サービス作ったらXSRF脆弱性孕んでた件。

ハッカソン的なイベントでよく適当にwebサービスを作ると思います。作りますね。そんなときのあるあるですが、割とWebのセキュリティを考えずにデプロイして成果発表みたいなノリです。良くないですね。そんな問題でした。

回答方法が他の問題とは異なり、GitLabのMerge Requestを作成することでした。これは一般的なWebの開発スタイルに則り、ステージング環境のソースコードを直接イジって「はいできました」ではなく、hot-fix branchを切って修正し、Merge Request→Code Review→Mergeという流れを再現したかったからです。

やること自体は簡単だったのではないかと思いますが、Goでの開発に慣れてない場合は少し苦労したかもしれません。

問題文

問題文は以下の通りでした。

俳句投稿サービスHikerを作成した。Hikerでは、ユーザー作成後ログインして俳句を詠むことができる。詠んだ俳句は公開される。また、他のユーザーが詠んだ俳句に対してmogamigawaする機能があり、mogamigawaした俳句は後でまとめて閲覧することができる。所謂お気に入り機能のようなものである。

ユーザーからのフィードバックで、意図しない俳句がmogamigawaされており困っているという情報が複数あった。それらのユーザーは共通して特定のWebサイトを閲覧したようである。
以上のことからHikerはXSRF脆弱性を孕んでいることが予想される。これらのユーザーはこの脆弱性を利用して、意図しない俳句をmogamigawaさせられたと考えられる。

任意の方法でこの脆弱性に対して対策を施してMerge Requestを建ててほしい。

尚、この俳句投稿サービスは次のようなシステム構成である。

  • サーバーサイド: Golang, Gin
  • フロントエンド: multitemplate + Bootstrap4
  • データベース: MySQL + GORM
  • docker-compose

初期状態とゴール

初期状態

  • 各チームのVMはHikeのステージング環境である。remoteRepositoryは削除されているので、各自で各自のfork先urlを設定すること。
    • url: https://gitlab.com/ictsc2019-teamチーム番号/ictsc2019-f21-xsrf
    • urlのチーム番号部分には1,2,3,…,15のような自分のチーム番号を代入すること
  • 既にictscというユーザーが俳句を投稿している
  • Hikeで各自アカウントを新規作成後、ログインし https://hackmd.io/tRks_vT-QjasH9hUsEJ-BA?view にアクセスするとictscというユーザーの俳句をmogamigawaしてしまう
  • ソースコードはGitLabで管理されており、問題解答開始時にチームリーダーにOWNER権限のinviteのメールが送信される
  • Hikerが動いているときは、http://192.168.15.1:8080 でサービスにアクセスできる

終了状態

  • 適切なXSRF対策がされている
  • 初期状態に示されているURL上の検証コードはあくまで一例であることに注意すること
  • 修正されたソースコードのMerge RequestをGitLab上で作成する
  • スコアサーバに、Merge RequestのURLを提出する

解説

XSRF脆弱性の対策をします。様々な手法が考えられますが、今回はWeb Application Framework(WAF)にGinを採用しているので、utrack/gin-csrfを使って対策するケースで解説します。
utrack/gin-csrfで実装する理由は、問題環境はステージング環境でありdocker-composeで管理されていることから、様々な場所で実行されることを考慮して、utrack/gin-csrfのような環境に依存しにくい実装をしたいためです。

.envを作成し.gitignoreで無視してgodotenvのようなもので使用して新たな環境変数との一致によって防ぐこともできますが、そういった解答が今回は無かったので省略します。

リモートリポジトリを追加する

ステージング環境にデプロイされているソースコードのディレクトリに移動してリモートリポジトリを追加します。

$ git remote add origin https://gitlab.com/ictsc2019-teamチーム番号/ictsc2019-f21-xsrf

この問題では解答にMerge Requestを建てなければいけないので、cloneしたときかこのタイミングでbranchを切ります。

$ git checkout -b xsrf-fix
$ git push --set-upstream origin xsrf-fix

あとはcommitと徳を積んでMerge Requestをします。

server.go の修正

utrack/gin-csrfのREADME.mdを参考に頑張ります。
変更点は次の通りです。

import部分の追記

  • "net/http"を追加しました。csrfのErrorFuncでhttp.StatusBadRequestを使用するためです。
  • "github.com/utrack/gin-csrf"を追加しました。gin-csrfを使います。
  • "ictsc2019-f21-xsrf/util"を追加しました。csrfのSecretをランダムな文字列にする関数をapp/util/util.goに追記して、それを使用するためです。
import (
    "log"
    "net/http"
    "path/filepath"

    "github.com/gin-contrib/multitemplate"
    "github.com/gin-contrib/sessions"
    "github.com/gin-contrib/sessions/cookie"
    "github.com/gin-gonic/gin"
    csrf "github.com/utrack/gin-csrf"

    "ictsc2019-f21-xsrf/domain"
    "ictsc2019-f21-xsrf/handler"
    "ictsc2019-f21-xsrf/util"

)

func main部分の変更

以下の記述を追加します。utrack/gin-csrfのREADME.mdの通りです。

// csrf
r.Use(csrf.Middleware(csrf.Options{
    Secret: util.RandString(10),
    ErrorFunc: func(c *gin.Context) {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": "CSRF token mismatch",
        })
        c.Abort()
    },
}))

そして、mogamigawaするAPIをPOSTに変更します。

apiRouter.POST("/mogamigawa", handler.NewMogamigawa)

以上がserver.goの更新作業になります。

util.go に追記

server.goで呼び出されているutil.RandString(n int)を、util.goに作成します。
"math/rand"を追加でimportしてください。

const rs2Letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"

func RandString(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = rs2Letters[rand.Intn(len(rs2Letters))]
}
return string(b)
}

RandString(n int)を叩くことでn文字のランダムな文字列を返すことができます。これをcsrfのSecretに使います。

このSecretですが、文字列が固定されている解答がありました。これは第三者から推測が困難ではないかもしれないので適切ではありません。(減点はしてません)

XSRF対策をする

今回は全てのFORM要素にhiddenなinputを用意して、そこにtokenを持たせ、送信させることにします。

hikeline.html の修正

17行目にhiddenなinputを追加します。

<form method="POST" action="api/newhike">
    <input type="hidden" name="_csrf" value="{{ .csrfToken }}">

また、hikeline.htmlのmogamigawaのbutton部分は以下のように入れ替えます。この変更でmogamigawaのAPIをPOSTにした変更に対応し、XSRF対策ができます。

<!-- mogamigawa button -->
<div>
  <form action="/api/mogamigawa?hike_id={{ .Hike_id }}" method="POST">
    <input type="hidden" name="_csrf" value="{{ $.csrfToken }}">
    <button type="submit">
      <a href=""><i class="fas fa-water"></i></a>
    </button>
    <style>
      button {
        padding: 0, 0;
        border-style: none;
      }

    </style>

</form>
</div>

signin.htmlsignup.htmlにもformがありますが、ここはXSRFから保護しなければならない場所ではありませんよね?

55%の採点を受けたチームは、この部分でapi/mogamigawaの対策はできてるんだけど、api/newhikeの対策がなされてない解答になっていました。

以上がhtmlの修正作業になります。

res.goの修正

htmlに{{ .csrfToken }}という新しいプレースホルダーを追加しました。これに対応して/app/handler/res.goを更新します。

"github.com/utrack/gin-csrf"を追加でimportしてください。gin-csrfを使います。
hikeline.htmlを返す部分のc.HTML(http.StatusOK, hikeline.html, gin.H{})に、以下のように記述を追加します。冗長なので、全箇所の記述は省略します。

c.HTML(http.StatusOK, hikeline.html, gin.H{
    "csrfToken": csrf.GetToken(c),
})

Merge Request

以上の変更をcommitしたらpushして、GitLabでMerge Requestを建てます。

解説は以上です。

採点基準

  1. 適切なMerge Requestがなされている: +10%
    • あまりにも杜撰な解答は許されません。:angry:
  2. 任意の手法でXSRF対策をしている: +90%
    • mogamigawaのAPIとフロントエンドにのみ修正を加えた場合は半分の +45% にしています。
    • mogamigawaのAPIに加えてXSRF対策の必要なnewhikeのAPIにも対策をした場合に +90% としました。
    • 適切なXSRF対策がなされている場合にのみ点数を取れるようにしました。

講評

お詫び

まず、採点方法に不備があったことにお詫び申し上げます。失礼しました。

解答について

10チームが解答を提出してくれました。様々な解答、MRがあり楽しかったです。ありがとうございました。

実装としては、

  • utrack/gin-csrf での対策(この解説と同様/同等な解答)
  • ホストを縛る環境変数/変数を追加して一致判定による対策
  • jwt-go での対策
  • net/httpのhttp.SameSiteStrictModeによる対策

の4種類でした。

ホストを縛る手法ですが、冒頭で

.envを作成し.gitignoreで無視してgodotenvのようなもので使用して新たな環境変数との一致によって防ぐこともできますが、そういった解答が今回は無かったので省略します。

と述べたとおり、dotenvを使用した手法ではなかったので個人的にあまり好きではない解答ですが、当然対策できているので満点を出しました。

JWT(私はじょっとと読んでいます)の実装は、良いところがたくさんありますが(ググってね:heart:)、署名アルゴリズムに脆弱性があったケースもあるのでパッケージの中身を確認してから使用したり、その脆弱性に対する適切な対策が必要です。(あらゆるパッケージにも言えます) このJWT使った解答を解説しようかと迷うくらいには好きな対策方法です。

また、http.SameSiteStrictModeの解答は非常に秀逸で個人的に気に入っています。

終わりに

俳句を投稿する謎サービスのmogamigawaとかいう謎機能がXSRF対策してなくてimgタグで雑に攻撃されるという問題でした。
GitLabを使ってMRを出すという他の問題とは毛色の違う問題でもありました。
一般的なチームで作るWebアプリケーションの開発スタイルに合わせて解答方法を工夫したつもりです。
楽しんで頂けたら幸いです。

来年も挑戦してください。待ってますよ。