Search code examples
restclojurecsrfmiddlewarering

How do I use Cross-Site Request Forgery prevention correctly in a Clojure HTTP client calling a Ring REST route?


I'm still learning Clojure (and all those accompanying libraries...), so if I do anything stupid in my ignorance, feel free to point it out :-)

I have trouble calling REST endpoints via the POST method from client code. My routes are wrapped using (ring.middleware.defaults/wrap-defaults <my-routes> site-defaults) (I believe this is a fairly good idea if ever such code is to run in production). This wrapper applies various other wrappers, including ring.middleware.anti-forgery/wrap-anti-forgery, which implements (among others) a Cross-Site Request Forgery (CSRF or XSRF) prevention scheme - the default (which I'm using) is a synchronizer token (or session) strategy.

Calling the same REST endpoint via GET works just fine (because CSRF protection is not applied to GET, HEAD and OPTIONS calls - see ring.middleware.anti-forgery/get-request?), but using POST (or one of the other methods) results in a 403 - Invalid anti-forgery token response.

(As seen in the sample code below) I do know how to add a "X-CSRF-Token" or "X-XSRF-Token" header to the HTTP request. (Since this is a REST call, I do not add a hidden "__anti-forgery-token" field as suggested by this question and answer, although either one of the headers or the form field would suffice for the wrapper - see ring.middleware.anti-forgery/default-request-token.) Rather, if I understand the code correctly, my problem stems from the fact that the default strategy compares the above token to a a session token value, that is retrieved by ring.middleware.anti-forgery.session/session-token:

(defn- session-token [request]
  (get-in request [:session :ring.middleware.anti-forgery/anti-forgery-token]))

I have no idea how to set up the session information correctly in the HTTP client call. (Any HTTP client is sufficient, as the above-mentioned 403 result is generated by the middleware wrapper. For the below demo I thus use the simple ring.mock.request.)

Here is some minimal code that shows what I have up to now. It defines the routes and handlers, then tries to call them from a unit test.

(ns question.rest
  (:require [compojure.core :refer :all]
            [ring.middleware.defaults :refer [wrap-defaults site-defaults secure-site-defaults]]
            [ring.middleware.anti-forgery :refer [*anti-forgery-token*]]
            [clojure.test :refer :all]
            [ring.mock.request :as mock]))

(defroutes
  exmpl-routes
  (ANY "/" [] "Site up OK.")
  (GET "/aft" [] (force *anti-forgery-token*)))

(def exmpl (wrap-defaults exmpl-routes site-defaults))

(deftest test-mock-fail
  (testing "POST to root route"
    (let [
          ; In a normal web app, the view/page would be GET'ed from the server, which would
          ; include the Anti-Forgery Token in it, and have the POST as an action on it. Hence
          ; the way atf is done here...
          aft (:body (exmpl (mock/request :get "https://localhost:8443/aft")))
          request (-> (mock/request :post "https://localhost:8443/")
                      (mock/header "X-CSRF-Token" aft))
          _ (println request)
          response (exmpl request)
          _ (println response)
          ]
      (is (= 200 (:status response)))                       ;;403
      (is (= "Site up OK." (:body response))))))            ;;Invalid anti-forgery token

The (println) calls show the following (some formatting applied):

Request:

{  :protocol "HTTP/1.1", 
   :server-port 8443, 
   :server-name "localhost", 
   :remote-addr "localhost", 
   :uri "/post", 
   :scheme :https, 
   :request-method :post, 
   :headers {  "host" "localhost:8443",
               "x-csrf-token" "<long token value here>" }  }

Response:

{  :status 403, 
   :headers {  "Content-Type" "text/html; charset=utf-8",
               "X-XSS-Protection" "1; mode=block",
               "X-Frame-Options" "SAMEORIGIN",
               "X-Content-Type-Options" "nosniff" }, 
   :body "<h1>Invalid anti-forgery token</h1>"  }

Tutorials I could find mostly concentrate on the GET method and seem to assume that the endpoints/routes would be called from HTML, which is served from the server (which includes session info). So I feel a bit stuck at the moment.


Solution

  • For a REST API you should use api-defaults or secure-api-defaults NOT site-defaults.

    The anti-forgery machinery is designed for web sites where the app is generating a form and can include the generated token to be sent back as part of the form submission POST -- it is not intended for use with a REST API.