13

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 "piglet@aol.com"}' 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 "starbuck@bsg.com"}' 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>
jasonshell
  • 352
  • 6
  • 12
  • 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 Dec 06 '13 at 18:41
  • You'll need to add (anti-forgery-field) to your forms. – edbond Dec 06 '13 at 19:10
  • @Pointy Thanks. Reading the [Ring-Anti-Forgery](https://github.com/weavejester/ring-anti-forgery#usage) 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 Dec 06 '13 at 19:29
  • Use --header curl option to set headers – edbond Dec 06 '13 at 19:40
  • @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 Dec 06 '13 at 19:42
  • You need to get CSRF Token somehow first (either template or a special request with JSON response). CSRF token will be generated stored in session so you need use cookies. Insert CSRF using any method: form field or header. – edbond Dec 06 '13 at 19:50
  • CSRF is generated on server and stored inside your session. You need a route to get it from session first. Session and token is not the same. – edbond Dec 06 '13 at 20:51

1 Answers1

11

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

edbond
  • 3,921
  • 19
  • 26
  • 1
    Thank you for the example and details. I'll be able to work on an implementation this afternoon. – jasonshell Dec 07 '13 at 14:41
  • Nice, let me know if you need help. – edbond Dec 07 '13 at 19:46
  • 2
    Updating your code repo to the latest libraries, it does not seem to work anymore. I've filed an issue but not sure it isn't a mistake on my part: https://github.com/sventech/CSRF – sventechie May 11 '15 at 16:43
  • 2
    OK, `wrap-anti-forgery` doesn't work with `ring-defaults` (which replaces compojure.handler) because it is redundant. Taking that out, the updated version of your example works beautifully (pull request sent). – sventechie May 11 '15 at 20:49