pax_global_header00006660000000000000000000000064131024274700014512gustar00rootroot0000000000000052 comment=93f500d4f0e94007ac2bf73a1d59e9c98e980f84 ring-anti-forgery-1.1.0/000077500000000000000000000000001310242747000150545ustar00rootroot00000000000000ring-anti-forgery-1.1.0/.gitignore000066400000000000000000000001311310242747000170370ustar00rootroot00000000000000/target /classes /checkouts /doc pom.xml pom.xml.asc *.jar *.class /.lein-* /.nrepl-port ring-anti-forgery-1.1.0/.travis.yml000066400000000000000000000000501310242747000171600ustar00rootroot00000000000000language: clojure script: lein test-all ring-anti-forgery-1.1.0/README.md000066400000000000000000000057241310242747000163430ustar00rootroot00000000000000# Ring-Anti-Forgery [![Build Status](https://travis-ci.org/ring-clojure/ring-anti-forgery.svg?branch=master)](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.clj000066400000000000000000000014271310242747000172200ustar00rootroot00000000000000(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/000077500000000000000000000000001310242747000156435ustar00rootroot00000000000000ring-anti-forgery-1.1.0/src/ring/000077500000000000000000000000001310242747000166025ustar00rootroot00000000000000ring-anti-forgery-1.1.0/src/ring/middleware/000077500000000000000000000000001310242747000207175ustar00rootroot00000000000000ring-anti-forgery-1.1.0/src/ring/middleware/anti_forgery.clj000066400000000000000000000075531310242747000241130ustar00rootroot00000000000000(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/000077500000000000000000000000001310242747000175575ustar00rootroot00000000000000ring-anti-forgery-1.1.0/src/ring/util/anti_forgery.clj000066400000000000000000000006621310242747000227450ustar00rootroot00000000000000(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/000077500000000000000000000000001310242747000160335ustar00rootroot00000000000000ring-anti-forgery-1.1.0/test/ring/000077500000000000000000000000001310242747000167725ustar00rootroot00000000000000ring-anti-forgery-1.1.0/test/ring/middleware/000077500000000000000000000000001310242747000211075ustar00rootroot00000000000000ring-anti-forgery-1.1.0/test/ring/middleware/test/000077500000000000000000000000001310242747000220665ustar00rootroot00000000000000ring-anti-forgery-1.1.0/test/ring/middleware/test/anti_forgery.clj000066400000000000000000000163221310242747000252540ustar00rootroot00000000000000(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/000077500000000000000000000000001310242747000177475ustar00rootroot00000000000000ring-anti-forgery-1.1.0/test/ring/util/test/000077500000000000000000000000001310242747000207265ustar00rootroot00000000000000ring-anti-forgery-1.1.0/test/ring/util/test/anti_forgery.clj000066400000000000000000000005601310242747000241110ustar00rootroot00000000000000(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 "")))))