13
votes

I'm attempting to implement the Ring-Anti-Forgery library via setting the X-CSRF-Token in the header.

Since I am using static html files I found the built-in hiccup helper, which sets the token in the form, to be useless.

This is my first stab at using Clojure for web development so I'm guessing that I am completely missing what should be obvious to someone with experience.

The instructions from the README state:

The middleware also looks for the token in the X-CSRF-Token and X-XSRF-Token header fields. This behavior can be customized further using the :read-token option:

(defn get-custom-token [request]
  (get-in request [:headers "x-forgery-token"]))

(def app
  (-> handler
      (wrap-anti-forgery {:read-token get-custom-token})
      (wrap-session)))

I have added the above to handler.clj without any success.

project.clj

(defproject hooktale "0.0.1"
  :description "Hooktale iOS App Website"
  :url "http://www.hooktale.com"
  :repositories {"sonartype releases" "https://oss.sonatype.org/content/repositories/releases/"}
  :source-paths ["src/clj" "src/cljs"]
  :dependencies [[org.clojure/clojure "1.5.1"]
                 [org.clojure/clojurescript "0.0-2080"]
                 [org.clojure/java.jdbc "0.3.0-beta2"]
                 [compojure "1.1.6"]
                 [com.mchange/c3p0 "0.9.5-pre5"]
                 [org.postgresql/postgresql "9.3-1100-jdbc4"]
                 [ring-anti-forgery "0.3.0"]]
  :plugins [[lein-ring "0.8.8"]
            [lein-cljsbuild "1.0.1-SNAPSHOT"]]
  :ring {:handler hooktale.handler/app}
  :profiles {:dev {:plugins [[javax.servlet/servlet-api "2.5"]
                             [ring-mock "0.1.5"]]
                   :cljsbuild {:builds [{:source-paths ["src/cljs"]
                                         :compiler {:optimizations :advanced
                                                    :pretty-print false
                                                    :output-to "resources/public/js/trout.js"}}]}}})

handler.clj

(ns hooktale.handler
  (:require [compojure.core :refer [defroutes GET POST]]
            [compojure.handler :refer [site]]
            [compojure.route :refer [resources not-found]]
            [clojure.java.io :refer [resource]]
            [ring.middleware.anti-forgery :refer :all]
            [ring.middleware.session :refer [wrap-session]]
            [hooktale.controllers.prospect :refer [create-prospect]]))

(defn get-custom-token [request]
  (get-in request [:headers "x-forgery-token"]))

(defroutes app-routes
  (GET "/" [] (resource "public/index.html"))
  (POST "/" [email] (create-prospect email))
  (resources "/")
  (not-found "Not Found"))

(def app
  (->
   (site app-routes)
   (wrap-anti-forgery {:read-token get-custom-token})
   (wrap-session)))

Sending a request to the page returns the following info:

curl -I localhost:3000

HTTP/1.1 200 OK
Date: Fri, 06 Dec 2013 16:30:45 GMT
Set-Cookie: ring-session=0b2a477f-9352-4fd8-a3c3-a6b6f8d9e063;Path=/
Content-Length: 0
Server: Jetty(7.6.8.v20121106)

curl -X POST -d '{:email "[email protected]"}' localhost:3000

<h1>Invalid anti-forgery token</h1>

The function in ring.middleware.anti-forgery that I thought would allow me to set the token in the header without having to set the hidden token value inside the form field.

(defn- default-request-token [request]
  (or (-> request form-params (get "__anti-forgery-token"))
      (-> request :headers (get "x-csrf-token"))
      (-> request :headers (get "x-xsrf-token"))))

If I am reading it correctly, it will check for the token in the form, if not there it will check for the x-csrf-token then the x-xsrf-token in the header.

I seem to be having difficulty in actually setting the value of x-csrf-token or x-xsrf-token in the header.

Curl responses

View the Cookie set by ring-session:

curl -I localhost:3000

HTTP/1.1 200 OK
Date: Fri, 06 Dec 2013 19:52:22 GMT
Set-Cookie: ring-session=b02dd6f8-74b8-4ce0-a1d6-07251dadb9aa;Path=/
Content-Length: 0
Server: Jetty(7.6.8.v20121106)

Setting the X-CSRF-Token:

curl -v --header "X-CSRF-Token: b02dd6f8-74b8-4ce0-a1d6-07251dadb9aa;Path=/" -X POST -d '{:email "[email protected]"}' localhost:3000

* Adding handle: conn: 0x7fd3ab004000
* Adding handle: send: 0
* Adding handle: recv: 0
* Curl_addHandleToPipeline: length: 1
* - Conn 0 (0x7fd3ab004000) send_pipe: 1, recv_pipe: 0
* About to connect() to localhost port 3000 (#0)
*   Trying ::1...
* Connected to localhost (::1) port 3000 (#0)
> POST / HTTP/1.1
> User-Agent: curl/7.30.0
> Host: localhost:3000
> Accept: */*
> X-CSRF-Token: b02dd6f8-74b8-4ce0-a1d6-07251dadb9aa;Path=/
> Content-Length: 27
> Content-Type: application/x-www-form-urlencoded
>
* upload completely sent off: 27 out of 27 bytes
< HTTP/1.1 403 Forbidden
< Date: Fri, 06 Dec 2013 19:54:52 GMT
< Content-Type: text/html;charset=ISO-8859-1
< Content-Length: 35
* Server Jetty(7.6.8.v20121106) is not blacklisted
< Server: Jetty(7.6.8.v20121106)
<
* Connection #0 to host localhost left intact
<h1>Invalid anti-forgery token</h1>
1
The whole point of these techniques is that the server actively prepares the HTML for the page (and the form in the page) with a special token crafted for the current session. If you're serving static HTML, then you have to figure out a way for on-page JavaScript to learn about the proper token value somehow.Pointy
You'll need to add (anti-forgery-field) to your forms.edbond
@Pointy Thanks. Reading the Ring-Anti-Forgery docs, I noticed that the library gives the option of looking for the token in the X-CSRF-Token and X-XSRF-Token header fields. Which would allow me to avoid passing the token in a hidden form field via some sort of templating.jasonshell
Use --header curl option to set headersedbond
@edbond I edited the post with new information at the bottom. The gist of it is I was under the impression the the library will check the form, if the token is not there, it will then check the header. This way, the middleware will look for the token in the X-CSRF-Token and X-XSRF-Token header fields. Which leaves me to my problem of setting the X-CSRF-Token / X-XSRF-Token value.jasonshell

1 Answers

11
votes

I created a repository https://github.com/edbond/CSRF with example. Readme describes process needed to POST requests with CSRF token.

In short (for API calls, curl):

  1. Get CSRF Token and session cookie from server (server will store CSRF token inside your session which identified by cookie)

  2. Send X-CSRF-Token and cookie along with POST request (server will compare CSRF token with that stored inside your session identified by cookie)

cookie -> session -> CSRF-Token

For HTML, form POSTing it should be enough to include (anti-forgery-field) to forms. Note, you can also send form field instead of header using curl.

HTH