Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support :same-site attribute #56

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
2 changes: 1 addition & 1 deletion project.clj
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
[org.clojure/clojure
ring/ring-core]]
[clj-time "0.12.0"]
[ring/ring-core "1.5.0"]
[ring/ring-core "1.9.5" :exclusions [ring/ring-codec commons-codec]]
[javax.servlet/servlet-api "2.5"]
;; use 1.8 for development
^:replace [org.clojure/clojure "1.8.0"]]
Expand Down
23 changes: 16 additions & 7 deletions src/peridot/cookie_jar.clj
Original file line number Diff line number Diff line change
Expand Up @@ -31,24 +31,30 @@
(string/replace #"([a-z\d])([A-Z])" dash-match)
(string/lower-case)))

(defn ^:private str->dashed-keyword [k]
(keyword (dasherize k)))

(defn ^:private parse-map
([map-string] (parse-map map-string #"; *"))
([map-string regex]
(when map-string
(apply merge (map #(let [[k v] (string/split % #"=" 2)]
{(keyword (dasherize k)) (or v true)})
{(str->dashed-keyword k) (or v true)})
(string/split map-string regex))))))

(defn ^:private build-cookie [cookie-string uri host]
(let [[assign options] (or (string/split cookie-string #"; *" 2)
[cookie-string nil])
[k v] (string/split assign #"=" 2)]
[(string/lower-case k)
(merge {:value v}
{:path (re-find #".*\/" uri)}
{:domain host}
{:raw assign}
(parse-map options))]))
(-> (merge {:value v}
{:path (re-find #".*\/" uri)}
{:domain host}
{:raw assign}
(parse-map options))
(update-in [:same-site] (fn [s]
(or (and s (str->dashed-keyword s))
:lax))))]))

(defn ^:private set-cookie [cookie-jar [k v]]
(assoc cookie-jar k v))
Expand All @@ -65,7 +71,7 @@
(update-in cookie-jar [host] merge
(into {} (map #(build-cookie % uri host) cookie-string))))))

(defn cookies-for [cookie-jar scheme uri host]
(defn cookies-for [cookie-jar scheme uri host same-site?]
(let [cookie-string
(->> cookie-jar
(remove (fn [[domain _]]
Expand All @@ -82,6 +88,9 @@
(:path %)))
uri)))
(remove #(and (:secure %) (= scheme :http)))
(remove #(and (not (:secure %)) (= (:same-site %) :none)))
(remove #(when-not same-site?
(= (:same-site %) :strict)))
(map :raw)
(interpose ";")
(apply str))]
Expand Down
18 changes: 14 additions & 4 deletions src/peridot/core.clj
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,21 @@
location
(pr/url (assoc request :uri location :query-string nil))))

;; Backward compatibility for clojure < 1.8 -> clojure.string/start-with?
(def ^:private
string-starts-with?
(or (resolve 'clojure.string/starts-with?)
(fn [^CharSequence s ^String substr]
(.startsWith (.toString s) substr))))

(defn follow-redirect
"Follow the redirect from the previous response."
[state]
[{request-map :request :as state}]
(if-let [location (rur/get-header (:response state) "Location")]
(request state
(expand-location location (:request state))
:headers {"referer" (pr/url (:request state))})
(let [prev-location (str (name (:scheme request-map)) "://" (:server-name request-map))
new-location (expand-location location request-map)
same-site? (string-starts-with? new-location prev-location)]
(request state new-location
:same-site? same-site?
:headers {"referer" (pr/url request-map)}))
(throw (IllegalArgumentException. "Previous response was not a redirect"))))
3 changes: 2 additions & 1 deletion src/peridot/request.clj
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@
(merge (cj/cookies-for cookie-jar
(:scheme request)
(:uri request)
(get-host request)))
(get-host request)
(not (false? (:same-site? env)))))
(merge (:headers env))))
(set-content-type content-type)
(add-env (dissoc (dissoc env :params) :headers))
Expand Down
77 changes: 76 additions & 1 deletion test/peridot/test/cookie_jar.clj
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
(ns peridot.test.cookie-jar
(:require [clj-time.core :as t]
[clj-time.format :as tf]
[clojure.edn]
[clojure.test :refer :all]
[clojure.string :as str]
[net.cgrand.moustache :as moustache]
[peridot.core :refer [session request]]
[peridot.core :refer [session request follow-redirect]]
[peridot.cookie-jar :as cj]
[ring.util.response :as response]
[ring.util.codec :as codec]
Expand Down Expand Up @@ -70,6 +71,13 @@
:cookies
(cookies-from-map (:params req)
(constantly {:http-only true}))))}
["echo"]
{:post (fn [{:keys [params] :as req}]
(let [cookies (some-> params (get "cookies") clojure.edn/read-string)
headers (some-> params (get "headers") clojure.edn/read-string)]
(cond-> (response/response "ok")
cookies (assoc :cookies cookies)
headers (assoc :headers headers))))}
["default-path"]
{:get (fn [req]
(let [resp (response/response "ok")]
Expand Down Expand Up @@ -278,3 +286,70 @@
(#(is (= (get (:headers (:request %)) "cookie")
"value=1")
"http-only cookies are sent")))))

(defn set-cookie [app host cookie]
(request app
(str host "/echo")
:request-method :post
:params {:cookies (pr-str {"a" (merge {:value "b",
:path "/",
;; :same-site :lax is default
}
cookie)})}))

(defn cross-site-request [a b & [cookie]]
(-> (session app)
(set-cookie a cookie)
;; Redirect back from B to A
(request (str b "/echo")
:request-method :post
:params {:headers (pr-str {"Location" (str a "/cookies/get")})})
(follow-redirect)))

(defn cookie-has-been-sent [app]
(doto app
(#(is (= (get (:headers (:request %)) "cookie")
"a=b")
"cookie has been sent"))))

(defn cookie-has-not-been-sent [app]
(doto app
(#(is (not= (get (:headers (:request %)) "cookie")
"a=b")
"cookie has not been sent"))))

(deftest cookie-security-same-site
;; - cookies are sent cross domain (and cross protocol) for :same-site :lax (default)
(-> (cross-site-request
"http://host-a.com"
"http://host-b.com")
(cookie-has-been-sent))

;; - cookies should not be sent cross-domain for :same-site :strict
(-> (cross-site-request
"http://host-a.com"
"http://host-b.com"
{:same-site :strict})

(cookie-has-not-been-sent)

;; On a subsequent normal request cookies are sent again
(request "http://host-a.com/cookies/get")
(cookie-has-been-sent))

;; Not sure about this one (also not supported in all browsers, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite)
;; - cookies should not be send for :same-site :none without :secure true
(-> (session app)
(set-cookie "https://host-a.com" {:same-site :none})

(request "https://host-a.com/cookies/get")
(cookie-has-not-been-sent))

;; - cookies are sent cross domain for :same-site :none and :secure true
(-> (cross-site-request
"https://host-a.com"
"https://host-b.com"
{:same-site :none
:secure true})

(cookie-has-been-sent)))