Skip to content

Commit

Permalink
Add :safe-header option
Browse files Browse the repository at this point in the history
  • Loading branch information
weavejester committed Jan 24, 2025
1 parent 3e217cb commit 45ff3f4
Show file tree
Hide file tree
Showing 3 changed files with 64 additions and 5 deletions.
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,29 @@ The token will be used to validate the request is accessible via the
`*anti-forgery-token*` var. The token is also placed in the request
under the `:anti-forgery-token` key.

### Safe headers

When making HTTP requests via `XMLHttpRequest` in JavaScript, a custom
header can be added to the request. If it is, the request is
'preflighted'; that is, a CORS request will be sent to the server by the
browser to check to see if the domain is valid. This ensures that the
request cannot be used in a CSRF attack (see the [OWASP CSRF
Cheatsheet][owasp_custom_headers]).

The `wrap-anti-forgery` middleware has a `:safe-header` option to check
for a custom header. If this header exists and is not blank, then the
request is deemed safe and the anti-forgery token is unnecessary. By
default this option is `nil` and not checked for.

```clojure
(def app
(-> handler
(wrap-anti-forgery {:safe-header "X-CSRF-Protection"})
(wrap-session)))
```

[owasp_custom_headers]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#employing-custom-request-headers-for-ajaxapi

### Custom token reader

By default the middleware looks for the anti-forgery token in the
Expand Down
18 changes: 13 additions & 5 deletions src/ring/middleware/anti_forgery.clj
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
(ns ring.middleware.anti-forgery
"Ring middleware to prevent CSRF attacks."
(:require [ring.middleware.anti-forgery.strategy :as strategy]
(:require [clojure.string :as str]
[ring.middleware.anti-forgery.strategy :as strategy]
[ring.middleware.anti-forgery.session :as session]))

(def ^{:doc "Binding that stores an anti-forgery token that must be included
Expand Down Expand Up @@ -28,8 +29,10 @@
(= method :get)
(= method :options)))

(defn- valid-request? [strategy request read-token]
(defn- valid-request? [strategy request read-token safe-header]
(or (get-request? request)
(when safe-header
(not (str/blank? (get-in request [:headers safe-header]))))
(when-let [token (read-token request)]
(strategy/valid-token? strategy request token))))

Expand Down Expand Up @@ -72,6 +75,10 @@
:error-handler - a handler function to call if the anti-forgery token is
incorrect or missing
:safe-header - a header that, if found on the request, will make this
middleware treat the request as safe without the need for
a valid anti-forgery token
:strategy - a strategy for creating and validating anti-forgety tokens,
which must satisfy the
ring.middleware.anti-forgery.strategy/Strategy protocol
Expand All @@ -85,17 +92,18 @@
{:pre [(not (and (:error-response options) (:error-handler options)))]}
(let [read-token (:read-token options default-request-token)
strategy (:strategy options (session/session-strategy))
error-handler (make-error-handler options)]
error-handler (make-error-handler options)
safe-header (some-> (:safe-header options) str/lower-case)]
(fn
([request]
(if (valid-request? strategy request read-token)
(if (valid-request? strategy request read-token safe-header)
(let [token (strategy/get-token strategy request)]
(binding [*anti-forgery-token* token]
(when-let [response (handler (assoc request :anti-forgery-token token))]
(strategy/write-token strategy request response token))))
(error-handler request)))
([request respond raise]
(if (valid-request? strategy request read-token)
(if (valid-request? strategy request read-token safe-header)
(let [token (strategy/get-token strategy request)]
(binding [*anti-forgery-token* token]
(handler (assoc request :anti-forgery-token token)
Expand Down
28 changes: 28 additions & 0 deletions test/ring/middleware/test/anti_forgery.clj
Original file line number Diff line number Diff line change
Expand Up @@ -189,3 +189,31 @@
(handler req resp ex)
(is (not (realized? ex)))
(is (= (:status @resp) 200))))))

(deftest safe-header-test
(testing "sync handlers"
(let [response {:status 200, :headers {}, :body "Foo"}
handler (wrap-anti-forgery (constantly response)
{:safe-header "X-CSRF-Protection"})]
(are [status req] (= (:status (handler req)) status)
403 (mock/request :post "/")
200 (-> (mock/request :post "/")
(mock/header "X-CSRF-Protection" "1")))))

(testing "async handlers"
(let [response {:status 200, :headers {}, :body "Foo"}
handler (wrap-anti-forgery (fn [_ respond _] (respond response))
{:safe-header "X-CSRF-Protection"})]
(let [req (mock/request :post "/")
resp (promise)
ex (promise)]
(handler req resp ex)
(is (not (realized? ex)))
(is (= (:status @resp) 403)))
(let [req (-> (mock/request :post "/")
(mock/header "X-CSRF-Protection" "1"))
resp (promise)
ex (promise)]
(handler req resp ex)
(is (not (realized? ex)))
(is (= (:status @resp) 200))))))

0 comments on commit 45ff3f4

Please sign in to comment.