0%

CompojureのCSRF対策トークンのanti-forgery-token作成

前回Docker Composureを使って作成したCompojureプロジェクトに、curlからPOSTするとInvalid anti-forgery tokenが発生してしまいました。デフォルトでCSRF対策用トークンを追加しないとPOSTはできない仕様になっています。開発中はanti-forgeryを無効にした方が簡単です。いろいろ調べるとStackOverflowにあった方法でうまくいきました。

Invalid anti-forgery token

以下のようにPOST用ハンドラをhandler.cljに作成してcurlでPOSTするとInvalid anti-forgery tokenが発生します。

$ curl -X POST -d "name=masato" localhost:3000/greeting
<h1>Invalid anti-forgery token</h1>

StackOverflowでも議論されていました。以下のサイトを参考に作業していきます。

プロジェクト

前回と同じようにDocker Composeでプロジェクトを管理します。

$ cd ~/clojure_apps
$ tree .
.
├── Dockerfile
├── cookies
├── docker-compose.yml
└── m2

Dockerホストの作業ユーザーと同じuidのユーザーをDockerコンテナにも作成します。MavenのjarファイルはVOLUMEに指定してDockerホストにマップしてコンテナ間で共有します。

~/clojure_apps/Dockerfile
FROM clojure
MAINTAINER Masato Shimizu <ma6ato@gmail.com>

WORKDIR /usr/src/app

RUN apt-get update && apt-get install sudo net-tools && \
rm -rf /var/lib/apt/lists/*

RUN adduser --disabled-password --gecos '' --uid 1000 docker && \
adduser docker sudo && \
echo 'docker ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers && \
mkdir /home/docker/.m2 && \
chown -R docker:docker /usr/src/app /home/docker/.m2

VOLUME /home/docker/.m2
USER docker
RUN lein

ENTRYPOINT ["lein"]

ローカルにClojureのイメージをビルドします。

$ docker build -t clojure .

docker-compose.ymlのworking_dirディレクティブはCompojureプロジェクトを作成する前はコメントアウトしておきます。

~/clojure_apps/docker-compose.yml
lein: &defaults
image: clojure
volumes:
- .:/usr/src/app
- ./m2:/home/docker/.m2
# working_dir: /usr/src/app/hello-world
ports:
- "3000:3000"
- "3449:3449"
cljslein:
<<: *defaults
ports:
- "9000:9000"

lein newでcompojureのプロジェクトを作成します。

$ cd ~/clojure_apps
$ docker-compose run --rm --service-ports lein new compojure hello-world

作成したCompojureプロジェクトをworking_dirディレクティブに指定します。

~/clojure_apps/docker-compose.yml
lein: &defaults
image: clojure
volumes:
- .:/usr/src/app
- ./m2:/home/docker/.m2
working_dir: /usr/src/app/hello-world
ports:
- "3000:3000"
- "3449:3449"
cljslein:
<<: *defaults
ports:
- "9000:9000"

project.cljファイルにはJSON作成のcheshireを依存関係に追加します。

~/clojure_apps/hello-world/project.clj
(defproject hello-world "0.1.0-SNAPSHOT"
:description "FIXME: write description"
:url "http://example.com/FIXME"
:min-lein-version "2.0.0"
:dependencies [[org.clojure/clojure "1.6.0"]
[compojure "1.3.1"]
[ring/ring-defaults "0.1.2"]
[cheshire "5.4.0"]]
:plugins [[lein-ring "0.8.13"]]
:ring {:handler hello-world.handler/app}
:profiles
{:dev {:dependencies [[javax.servlet/servlet-api "2.5"]
[ring-mock "0.1.5"]]}})

GET “/“ハンドラでCSRFトークンを出力するように変更します。トークンはRing-Anti-Forgery*anti-forgery-token*を評価します。動的なvarのスペシャル変数です。セッションに保存しているcookie-storeのキーはランダムで適当に作成しました。

またJSONでレスポンスを返すためにcheshireのgenerate-stringを使います。

~/clojure_apps/hello-world/src/hello_world/handler.clj
(ns hello-world.handler
(:require [compojure.core :refer :all]
[compojure.route :as route]
[ring.middleware.defaults :refer [wrap-defaults site-defaults]]
[ring.middleware.session :refer [wrap-session]]
[ring.middleware.anti-forgery :refer :all]
[ring.middleware.session.cookie :refer (cookie-store)]
[cheshire.core :refer :all]))

(defn greeting-handler [request]
(let [name (get-in request [:params :name])]
(str "Hi, " name)))

(defroutes app-routes
(GET "/" [] (generate-string {:csrf-token
*anti-forgery-token*}))
(POST "/greeting" [] greeting-handler)
(route/not-found "Not Found"))

(def app
(-> app-routes
(wrap-defaults site-defaults)
(wrap-session {:cookie-attrs {:max-age 3600}
:store (cookie-store {:key "ahY9poQuaghahc7I"})})))

Ringサーバーをheadlessで起動します。

$ cd ~/clojure_apps
$ docker-compose run --rm --service-ports lein ring server-headless
...
SelectChannelConnector@0.0.0.0:3000
Started server on port 3000

最初に用意したハンドラを実行してCSRFトークンを出力します。

$ curl -X GET --cookie-jar cookies "http://localhost:3000/"
{"csrf-token":"FxyHMRjb5I9wwxskEo2h2uXhtU/CNUo38xLDTa/2fJp7QhZ/Wo7hi4zRbey9yUZgRKfe3y1uS66K8+kA"}

JSONで取得したcsfr-tokenをHTTPヘッダのX-CSRF-Tokenに指定してcurlからPOSTすると成功します。

$ curl -X POST  \
--cookie cookies \
-F "name=masato" \
-H "X-CSRF-Token: FxyHMRjb5I9wwxskEo2h2uXhtU/CNUo38xLDTa/2fJp7QhZ/Wo7hi4zRbey9yUZgRKfe3y1uS66K8+kA" \
"http://localhost:3000/greeting"
Hi, masato

anti-forgeryを無効にする場合

CSRF対策を無効にする場合は、site-defaultsのanti-forgeryキーの値をfalseにして起動します。

~/clojure_apps/hello-world/src/hello_world/handler.clj
...
(def app (wrap-defaults app-routes (assoc-in site-defaults [:security :anti-forgery] false)))