Set Ring-Anti-Forgery CSRF header token
Asked Answered
M

1

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 "[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>
Maracaibo answered 6/12, 2013 at 17:46 Comment(7)
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.Perishable
You'll need to add (anti-forgery-field) to your forms.Parol
@Perishable 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.Maracaibo
Use --header curl option to set headersParol
@Parol 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.Maracaibo
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.Parol
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.Parol
P
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

Parol answered 7/12, 2013 at 7:32 Comment(4)
Thank you for the example and details. I'll be able to work on an implementation this afternoon.Maracaibo
Nice, let me know if you need help.Parol
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: github.com/sventech/CSRFSheepdip
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).Sheepdip

© 2022 - 2024 — McMap. All rights reserved.