pax_global_header 0000666 0000000 0000000 00000000064 13102427470 0014512 g ustar 00root root 0000000 0000000 52 comment=93f500d4f0e94007ac2bf73a1d59e9c98e980f84
ring-anti-forgery-1.1.0/ 0000775 0000000 0000000 00000000000 13102427470 0015054 5 ustar 00root root 0000000 0000000 ring-anti-forgery-1.1.0/.gitignore 0000664 0000000 0000000 00000000131 13102427470 0017037 0 ustar 00root root 0000000 0000000 /target
/classes
/checkouts
/doc
pom.xml
pom.xml.asc
*.jar
*.class
/.lein-*
/.nrepl-port
ring-anti-forgery-1.1.0/.travis.yml 0000664 0000000 0000000 00000000050 13102427470 0017160 0 ustar 00root root 0000000 0000000 language: clojure
script: lein test-all
ring-anti-forgery-1.1.0/README.md 0000664 0000000 0000000 00000005724 13102427470 0016343 0 ustar 00root root 0000000 0000000 # Ring-Anti-Forgery
[](https://travis-ci.org/ring-clojure/ring-anti-forgery)
Ring middleware that prevents [CSRF][1] attacks by via a
randomly-generated anti-forgery token.
[1]: http://en.wikipedia.org/wiki/Cross-site_request_forgery
## Install
Add the following dependency to your `project.clj`:
[ring/ring-anti-forgery "1.1.0"]
## Usage
The `wrap-anti-forgery` middleware should be applied to your Ring
handler, inside of the standard `wrap-session` middleware in Ring:
```clojure
(use 'ring.middleware.anti-forgery
'ring.middleware.session)
(def app
(-> handler
wrap-anti-forgery
wrap-session))
```
Any request that isn't a `HEAD` or `GET` request will now require an
anti-forgery token, or an "access denied" response will be returned.
The token is bound to the session, and accessible via the
`*anti-forgery-token*` var.
By default the middleware looks for the anti-forgery token in the
`__anti-forgery-token` form parameter, which can be added to your
forms as a hidden field. For convenience, this library provides a
function to generate the HTML of that hidden field:
```clojure
(use 'ring.util.anti-forgery)
(anti-forgery-field) ;; returns the HTML for the anti-forgery field
```
The middleware also looks for the token in the `X-CSRF-Token` and
`X-XSRF-Token` header fields, which are commonly used in AJAX
requests.
This behavior can be customized further by supplying a function to the
`:read-token` option. This function is passed the request map, and
should return the anti-forgery token found in the request.
```clojure
(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)))
```
It's also possible to customize the error response returned when the
token is invalid or missing:
```clojure
(def custom-error-response
{:status 403
:headers {"Content-Type" "text/html"}
:body "
Missing anti-forgery token "})
(def app
(-> handler
(wrap-anti-forgery {:error-response custom-error-response})
(wrap-session)))
```
Or, for more control, an error handler can be supplied:
```clojure
(defn custom-error-handler [request]
{:status 403
:headers {"Content-Type" "text/html"}
:body "Missing anti-forgery token "})
(def app
(-> handler
(wrap-anti-forgery {:error-handler custom-error-handler})
(wrap-session)))
```
## Caveats
This middleware will prevent all HTTP methods except for GET and HEAD
from accessing your handler without an anti-forgery token that matches
the one in the current session.
You should therefore only apply this middleware to the parts of your
application designed to be accessed through a web browser. This
middleware should not be applied to handlers that define web services.
## License
Copyright © 2017 James Reeves
Distributed under the MIT License, the same as Ring.
ring-anti-forgery-1.1.0/project.clj 0000664 0000000 0000000 00000001427 13102427470 0017220 0 ustar 00root root 0000000 0000000 (defproject ring/ring-anti-forgery "1.1.0"
:description "Ring middleware to prevent CSRF attacks"
:url "https://github.com/ring-clojure/ring-anti-forgery"
:license {:name "The MIT License"
:url "http://opensource.org/licenses/MIT"}
:dependencies [[org.clojure/clojure "1.5.1"]
[crypto-random "1.2.0"]
[crypto-equality "1.0.0"]
[hiccup "1.0.5"]]
:aliases {"test-all" ["with-profile" "default:+1.6:+1.7:+1.8:+1.9" "test"]}
:profiles
{:dev {:dependencies [[ring/ring-mock "0.3.0"]]}
:1.6 {:dependencies [[org.clojure/clojure "1.6.0"]]}
:1.7 {:dependencies [[org.clojure/clojure "1.7.0"]]}
:1.8 {:dependencies [[org.clojure/clojure "1.8.0"]]}
:1.9 {:dependencies [[org.clojure/clojure "1.9.0-alpha11"]]}})
ring-anti-forgery-1.1.0/src/ 0000775 0000000 0000000 00000000000 13102427470 0015643 5 ustar 00root root 0000000 0000000 ring-anti-forgery-1.1.0/src/ring/ 0000775 0000000 0000000 00000000000 13102427470 0016602 5 ustar 00root root 0000000 0000000 ring-anti-forgery-1.1.0/src/ring/middleware/ 0000775 0000000 0000000 00000000000 13102427470 0020717 5 ustar 00root root 0000000 0000000 ring-anti-forgery-1.1.0/src/ring/middleware/anti_forgery.clj 0000664 0000000 0000000 00000007553 13102427470 0024113 0 ustar 00root root 0000000 0000000 (ns ring.middleware.anti-forgery
"Ring middleware to prevent CSRF attacks with an anti-forgery token."
(:require [crypto.random :as random]
[crypto.equality :as crypto]))
(def ^{:doc "Binding that stores an anti-forgery token that must be included
in POST forms if the handler is wrapped in wrap-anti-forgery."
:dynamic true}
*anti-forgery-token*)
(defn- new-token []
(random/base64 60))
(defn- session-token [request]
(get-in request [:session ::anti-forgery-token]))
(defn- find-or-create-token [request]
(or (session-token request) (new-token)))
(defn- add-session-token [response request token]
(if response
(let [old-token (session-token request)]
(if (= old-token token)
response
(-> response
(assoc :session (:session response (:session request)))
(assoc-in [:session ::anti-forgery-token] token))))))
(defn- form-params [request]
(merge (:form-params request)
(:multipart-params request)))
(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"))))
(defn- valid-token? [request read-token]
(let [user-token (read-token request)
stored-token (session-token request)]
(and user-token
stored-token
(crypto/eq? user-token stored-token))))
(defn- get-request? [{method :request-method}]
(or (= method :head)
(= method :get)
(= method :options)))
(defn- valid-request? [request read-token]
(and (not (get-request? request))
(not (valid-token? request read-token))))
(def ^:private default-error-response
{:status 403
:headers {"Content-Type" "text/html"}
:body "Invalid anti-forgery token "})
(defn- constant-handler [response]
(fn
([_] response)
([_ respond _] (respond response))))
(defn- make-error-handler [options]
(or (:error-handler options)
(constant-handler (:error-response options default-error-response))))
(defn wrap-anti-forgery
"Middleware that prevents CSRF attacks. Any POST request to the handler
returned by this function must contain a valid anti-forgery token, or else an
access-denied response is returned.
The anti-forgery token can be placed into a HTML page via the
*anti-forgery-token* var, which is bound to a random key unique to the
current session. By default, the token is expected to be in a form field
named '__anti-forgery-token', or in the 'X-CSRF-Token' or 'X-XSRF-Token'
headers.
Accepts the following options:
:read-token - a function that takes a request and returns an anti-forgery
token, or nil if the token does not exist
:error-response - the response to return if the anti-forgery token is
incorrect or missing
:error-handler - a handler function to call if the anti-forgery token is
incorrect or missing.
Only one of :error-response, :error-handler may be specified."
([handler]
(wrap-anti-forgery handler {}))
([handler options]
{:pre [(not (and (:error-response options) (:error-handler options)))]}
(let [read-token (:read-token options default-request-token)
error-handler (make-error-handler options)]
(fn
([request]
(let [token (find-or-create-token request)]
(binding [*anti-forgery-token* token]
(if (valid-request? request read-token)
(error-handler request)
(add-session-token (handler request) request token)))))
([request respond raise]
(let [token (find-or-create-token request)]
(binding [*anti-forgery-token* token]
(if (valid-request? request read-token)
(error-handler request respond raise)
(handler request #(respond (add-session-token % request token)) raise)))))))))
ring-anti-forgery-1.1.0/src/ring/util/ 0000775 0000000 0000000 00000000000 13102427470 0017557 5 ustar 00root root 0000000 0000000 ring-anti-forgery-1.1.0/src/ring/util/anti_forgery.clj 0000664 0000000 0000000 00000000662 13102427470 0022745 0 ustar 00root root 0000000 0000000 (ns ring.util.anti-forgery
"Utility functions for inserting anti-forgery tokens into HTML forms."
(:use [hiccup core form]
ring.middleware.anti-forgery))
(defn anti-forgery-field
"Create a hidden field with the session anti-forgery token as its value.
This ensures that the form it's inside won't be stopped by the anti-forgery
middleware."
[]
(html (hidden-field "__anti-forgery-token" *anti-forgery-token*)))
ring-anti-forgery-1.1.0/test/ 0000775 0000000 0000000 00000000000 13102427470 0016033 5 ustar 00root root 0000000 0000000 ring-anti-forgery-1.1.0/test/ring/ 0000775 0000000 0000000 00000000000 13102427470 0016772 5 ustar 00root root 0000000 0000000 ring-anti-forgery-1.1.0/test/ring/middleware/ 0000775 0000000 0000000 00000000000 13102427470 0021107 5 ustar 00root root 0000000 0000000 ring-anti-forgery-1.1.0/test/ring/middleware/test/ 0000775 0000000 0000000 00000000000 13102427470 0022066 5 ustar 00root root 0000000 0000000 ring-anti-forgery-1.1.0/test/ring/middleware/test/anti_forgery.clj 0000664 0000000 0000000 00000016322 13102427470 0025254 0 ustar 00root root 0000000 0000000 (ns ring.middleware.test.anti-forgery
(:require [ring.middleware.anti-forgery :as af])
(:use clojure.test
ring.middleware.anti-forgery
ring.mock.request))
(deftest forgery-protection-test
(let [response {:status 200, :headers {}, :body "Foo"}
handler (wrap-anti-forgery (constantly response))]
(are [status req] (= (:status (handler req)) status)
403 (-> (request :post "/")
(assoc :form-params {"__anti-forgery-token" "foo"}))
403 (-> (request :post "/")
(assoc :session {::af/anti-forgery-token "foo"})
(assoc :form-params {"__anti-forgery-token" "bar"}))
200 (-> (request :post "/")
(assoc :session {::af/anti-forgery-token "foo"})
(assoc :form-params {"__anti-forgery-token" "foo"})))))
(deftest request-method-test
(let [response {:status 200, :headers {}, :body "Foo"}
handler (wrap-anti-forgery (constantly response))]
(are [status req] (= (:status (handler req)) status)
200 (request :head "/")
200 (request :get "/")
200 (request :options "/")
403 (request :post "/")
403 (request :put "/")
403 (request :patch "/")
403 (request :delete "/"))))
(deftest csrf-header-test
(let [response {:status 200, :headers {}, :body "Foo"}
handler (wrap-anti-forgery (constantly response))
sess-req (-> (request :post "/")
(assoc :session {::af/anti-forgery-token "foo"}))]
(are [status req] (= (:status (handler req)) status)
200 (assoc sess-req :headers {"x-csrf-token" "foo"})
200 (assoc sess-req :headers {"x-xsrf-token" "foo"}))))
(deftest multipart-form-test
(let [response {:status 200, :headers {}, :body "Foo"}
handler (wrap-anti-forgery (constantly response))]
(is (= (-> (request :post "/")
(assoc :session {::af/anti-forgery-token "foo"})
(assoc :multipart-params {"__anti-forgery-token" "foo"})
handler
:status)
200))))
(deftest token-in-session-test
(let [response {:status 200, :headers {}, :body "Foo"}
handler (wrap-anti-forgery (constantly response))]
(is (contains? (:session (handler (request :get "/")))
::af/anti-forgery-token))
(is (not= (get-in (handler (request :get "/"))
[:session ::af/anti-forgery-token])
(get-in (handler (request :get "/"))
[:session ::af/anti-forgery-token])))))
(deftest token-binding-test
(letfn [(handler [request]
{:status 200
:headers {}
:body *anti-forgery-token*})]
(let [response ((wrap-anti-forgery handler) (request :get "/"))]
(is (= (get-in response [:session ::af/anti-forgery-token])
(:body response))))))
(deftest nil-response-test
(letfn [(handler [request] nil)]
(let [response ((wrap-anti-forgery handler) (request :get "/"))]
(is (nil? response)))))
(deftest no-lf-in-token-test
(letfn [(handler [request]
{:status 200
:headers {}
:body *anti-forgery-token*})]
(let [response ((wrap-anti-forgery handler) (request :get "/"))
token (get-in response [:session ::af/anti-forgery-token])]
(is (not (.contains token "\n"))))))
(deftest single-token-per-session-test
(let [expected {:status 200, :headers {}, :body "Foo"}
handler (wrap-anti-forgery (constantly expected))
actual (handler
(-> (request :get "/")
(assoc-in [:session ::af/anti-forgery-token] "foo")))]
(is (= actual expected))))
(deftest not-overwrite-session-test
(let [response {:status 200 :headers {} :body nil}
handler (wrap-anti-forgery (constantly response))
session (:session (handler (-> (request :get "/")
(assoc-in [:session "foo"] "bar"))))]
(is (contains? session ::af/anti-forgery-token))
(is (= (session "foo") "bar"))))
(deftest session-response-test
(let [response {:status 200 :headers {} :session {"foo" "bar"} :body nil}
handler (wrap-anti-forgery (constantly response))
session (:session (handler (request :get "/")))]
(is (contains? session ::af/anti-forgery-token))
(is (= (session "foo") "bar"))))
(deftest custom-error-response-test
(let [response {:status 200, :headers {}, :body "Foo"}
error-resp {:status 500, :headers {}, :body "Bar"}
handler (wrap-anti-forgery (constantly response)
{:error-response error-resp})]
(is (= (dissoc (handler (request :get "/")) :session)
response))
(is (= (dissoc (handler (request :post "/")) :session)
error-resp))))
(deftest custom-error-handler-test
(let [response {:status 200, :headers {}, :body "Foo"}
error-resp {:status 500, :headers {}, :body "Bar"}
handler (wrap-anti-forgery (constantly response)
{:error-handler (fn [request] error-resp)})]
(is (= (dissoc (handler (request :get "/")) :session)
response))
(is (= (dissoc (handler (request :post "/")) :session)
error-resp))))
(deftest disallow-both-error-response-and-error-handler
(is (thrown?
AssertionError
(wrap-anti-forgery (constantly {:status 200})
{:error-handler (fn [request] {:status 500 :body "Handler"})
:error-response {:status 500 :body "Response"}}))))
(deftest custom-read-token-test
(let [response {:status 200, :headers {}, :body "Foo"}
handler (wrap-anti-forgery
(constantly response)
{:read-token #(get-in % [:headers "x-forgery-token"])})
req (-> (request :post "/")
(assoc :session {::af/anti-forgery-token "foo"})
(assoc :headers {"x-forgery-token" "foo"}))]
(is (= (:status (handler req))
200))
(is (= (:status (handler (assoc req :headers {"x-csrf-token" "foo"})))
403))))
(deftest random-tokens-test
(let [handler (fn [_] {:status 200, :headers {}, :body *anti-forgery-token*})
get-response (fn [] ((wrap-anti-forgery handler) (request :get "/")))
tokens (map :body (repeatedly 1000 get-response))]
(is (every? #(re-matches #"[A-Za-z0-9+/]{80}" %) tokens))
(is (= (count tokens) (count (set tokens))))))
(deftest forgery-protection-cps-test
(let [response {:status 200, :headers {}, :body "Foo"}
handler (wrap-anti-forgery (fn [_ respond _] (respond response)))]
(testing "missing token"
(let [req (-> (request :post "/")
(assoc :form-params {"__anti-forgery-token" "foo"}))
resp (promise)
ex (promise)]
(handler req resp ex)
(is (not (realized? ex)))
(is (= (:status @resp) 403))))
(testing "valid token"
(let [req (-> (request :post "/")
(assoc :session {::af/anti-forgery-token "foo"})
(assoc :form-params {"__anti-forgery-token" "foo"}))
resp (promise)
ex (promise)]
(handler req resp ex)
(is (not (realized? ex)))
(is (= (:status @resp) 200))))))
ring-anti-forgery-1.1.0/test/ring/util/ 0000775 0000000 0000000 00000000000 13102427470 0017747 5 ustar 00root root 0000000 0000000 ring-anti-forgery-1.1.0/test/ring/util/test/ 0000775 0000000 0000000 00000000000 13102427470 0020726 5 ustar 00root root 0000000 0000000 ring-anti-forgery-1.1.0/test/ring/util/test/anti_forgery.clj 0000664 0000000 0000000 00000000560 13102427470 0024111 0 ustar 00root root 0000000 0000000 (ns ring.util.test.anti-forgery
(:use clojure.test
ring.util.anti-forgery
ring.middleware.anti-forgery))
(deftest anti-forgery-field-test
(binding [*anti-forgery-token* "abc"]
(is (= (anti-forgery-field)
(str " ")))))