Skip to content

Commit 45ff3f4

Browse files
committed
Add :safe-header option
1 parent 3e217cb commit 45ff3f4

File tree

3 files changed

+64
-5
lines changed

3 files changed

+64
-5
lines changed

README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,29 @@ The token will be used to validate the request is accessible via the
4343
`*anti-forgery-token*` var. The token is also placed in the request
4444
under the `:anti-forgery-token` key.
4545

46+
### Safe headers
47+
48+
When making HTTP requests via `XMLHttpRequest` in JavaScript, a custom
49+
header can be added to the request. If it is, the request is
50+
'preflighted'; that is, a CORS request will be sent to the server by the
51+
browser to check to see if the domain is valid. This ensures that the
52+
request cannot be used in a CSRF attack (see the [OWASP CSRF
53+
Cheatsheet][owasp_custom_headers]).
54+
55+
The `wrap-anti-forgery` middleware has a `:safe-header` option to check
56+
for a custom header. If this header exists and is not blank, then the
57+
request is deemed safe and the anti-forgery token is unnecessary. By
58+
default this option is `nil` and not checked for.
59+
60+
```clojure
61+
(def app
62+
(-> handler
63+
(wrap-anti-forgery {:safe-header "X-CSRF-Protection"})
64+
(wrap-session)))
65+
```
66+
67+
[owasp_custom_headers]: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#employing-custom-request-headers-for-ajaxapi
68+
4669
### Custom token reader
4770

4871
By default the middleware looks for the anti-forgery token in the

src/ring/middleware/anti_forgery.clj

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
(ns ring.middleware.anti-forgery
22
"Ring middleware to prevent CSRF attacks."
3-
(:require [ring.middleware.anti-forgery.strategy :as strategy]
3+
(:require [clojure.string :as str]
4+
[ring.middleware.anti-forgery.strategy :as strategy]
45
[ring.middleware.anti-forgery.session :as session]))
56

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

31-
(defn- valid-request? [strategy request read-token]
32+
(defn- valid-request? [strategy request read-token safe-header]
3233
(or (get-request? request)
34+
(when safe-header
35+
(not (str/blank? (get-in request [:headers safe-header]))))
3336
(when-let [token (read-token request)]
3437
(strategy/valid-token? strategy request token))))
3538

@@ -72,6 +75,10 @@
7275
:error-handler - a handler function to call if the anti-forgery token is
7376
incorrect or missing
7477
78+
:safe-header - a header that, if found on the request, will make this
79+
middleware treat the request as safe without the need for
80+
a valid anti-forgery token
81+
7582
:strategy - a strategy for creating and validating anti-forgety tokens,
7683
which must satisfy the
7784
ring.middleware.anti-forgery.strategy/Strategy protocol
@@ -85,17 +92,18 @@
8592
{:pre [(not (and (:error-response options) (:error-handler options)))]}
8693
(let [read-token (:read-token options default-request-token)
8794
strategy (:strategy options (session/session-strategy))
88-
error-handler (make-error-handler options)]
95+
error-handler (make-error-handler options)
96+
safe-header (some-> (:safe-header options) str/lower-case)]
8997
(fn
9098
([request]
91-
(if (valid-request? strategy request read-token)
99+
(if (valid-request? strategy request read-token safe-header)
92100
(let [token (strategy/get-token strategy request)]
93101
(binding [*anti-forgery-token* token]
94102
(when-let [response (handler (assoc request :anti-forgery-token token))]
95103
(strategy/write-token strategy request response token))))
96104
(error-handler request)))
97105
([request respond raise]
98-
(if (valid-request? strategy request read-token)
106+
(if (valid-request? strategy request read-token safe-header)
99107
(let [token (strategy/get-token strategy request)]
100108
(binding [*anti-forgery-token* token]
101109
(handler (assoc request :anti-forgery-token token)

test/ring/middleware/test/anti_forgery.clj

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,3 +189,31 @@
189189
(handler req resp ex)
190190
(is (not (realized? ex)))
191191
(is (= (:status @resp) 200))))))
192+
193+
(deftest safe-header-test
194+
(testing "sync handlers"
195+
(let [response {:status 200, :headers {}, :body "Foo"}
196+
handler (wrap-anti-forgery (constantly response)
197+
{:safe-header "X-CSRF-Protection"})]
198+
(are [status req] (= (:status (handler req)) status)
199+
403 (mock/request :post "/")
200+
200 (-> (mock/request :post "/")
201+
(mock/header "X-CSRF-Protection" "1")))))
202+
203+
(testing "async handlers"
204+
(let [response {:status 200, :headers {}, :body "Foo"}
205+
handler (wrap-anti-forgery (fn [_ respond _] (respond response))
206+
{:safe-header "X-CSRF-Protection"})]
207+
(let [req (mock/request :post "/")
208+
resp (promise)
209+
ex (promise)]
210+
(handler req resp ex)
211+
(is (not (realized? ex)))
212+
(is (= (:status @resp) 403)))
213+
(let [req (-> (mock/request :post "/")
214+
(mock/header "X-CSRF-Protection" "1"))
215+
resp (promise)
216+
ex (promise)]
217+
(handler req resp ex)
218+
(is (not (realized? ex)))
219+
(is (= (:status @resp) 200))))))

0 commit comments

Comments
 (0)