Skip to content

Commit

Permalink
Merge branch 'csurf-replace-csrf-csrf'
Browse files Browse the repository at this point in the history
  • Loading branch information
chr15m committed Mar 4, 2024
2 parents 3647339 + 64c3473 commit c831b16
Show file tree
Hide file tree
Showing 9 changed files with 83 additions and 27 deletions.
28 changes: 20 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,25 +415,37 @@ Also see the [send-email example](https://github.com/chr15m/sitefox/tree/main/ex

See the [form validation example](https://github.com/chr15m/sitefox/tree/main/examples/form-validation) which uses [node-input-validator](https://www.npmjs.com/package/node-input-validator) and checks for CSRF problems.

#### CSRF protection

To ensure you can `POST` without CSRF warnings you should create a hidden element like this (Reagent syntax):

```clojure
[:input {:name "_csrf" :type "hidden" :default-value (.csrfToken req)}]
```

If you're making an ajax `POST` request from the client side, you should pass the CSRF token as a header. A valid token is available in the document's cookie and you can add it to the headers of a fetch request as follows:
If you're making an ajax `POST` request from the client side, you should pass the CSRF token as a header.
A valid token is available as a string at the JSON endpoint `/_csrf-token` and you can fetch it using `fetch-csrf-token`
and add it to the headers of a fetch request as follows:

```clojure
(ns n (:require [sitefox.ui :refer [csrf-token]]))
(ns n (:require [sitefox.ui :refer [fetch-csrf-token]]))

(js/fetch "/api/endpoint"
#js {:method "POST"
:headers #js {:Content-Type "application/json"
:XSRF-Token (csrf-token)}
:body (js/JSON.stringify (clj->js data))})
(-> (fetch-csrf-token)
(.then (fn [token]
(js/fetch "/api/endpoint"
#js {:method "POST"
:headers #js {:Content-Type "application/json"
:X-XSRF-TOKEN token} ; <- use token here
:body (js/JSON.stringify (clj->js some-data))}))))
```

In some rare circumstances you may wish to turn off CSRF checking (for example posting to an API from a non-browser device).
**Note**: you can fetch the CSRF token from a client side cookie instead if you set the environment variable `SEND-CSRF-TOKEN`.
This was the default in previous Sitefox versions.
When set, Sitefox will send the token on every GET request in the client side cookie
`XSRF-TOKEN` and this can be retrieved with the `ui/csrf-token` function.
This is a valid, but less secure form of CSRF protection.

In some rare circumstances you may wish to turn off CSRF checks (for example posting to an API from a non-browser device).
If you know what you are doing you can use the `pre-csrf-router` to add routes which bypass the CSRF checking:

```clojure
Expand Down
15 changes: 9 additions & 6 deletions examples/form-validation/webserver.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,15 @@

(def button-script
(str
"token=document.cookie.split('; ')
.find((row)=>row.startsWith('XSRF-TOKEN='))?.split('=')[1];
ajax.onclick=()=>{fetch('/ajax',
{'method':'POST','body':'received!',
'headers':{'Content-Type':'text/plain','XSRF-Token':token}})
.then(r=>r.text()).then(d=>{ajaxresult.innerHTML=d})}"))
"ajax.onclick=()=>{
fetch('/_csrf-token').then(r=>r.json()).then((token)=>{
console.log('token: ', token);
fetch('/ajax',
{'method':'POST','body':'received!',
'headers':{'Content-Type':'text/plain','X-XSRF-TOKEN':token}}
).then(r=>r.text()).then(d=>{ajaxresult.innerHTML=d})
});
}"))

(defn view:form [csrf-token data validation-errors]
(let [ve (or validation-errors #js {})
Expand Down
1 change: 0 additions & 1 deletion examples/nbb/webserver.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
(p/catch
(p/let [self *file*
[app host port] (web/start)]
(print "here")
(setup-routes app)
(nbb-reloader self #(setup-routes app))
(println "Serving on" (str "http://" host ":" port)))
Expand Down
2 changes: 1 addition & 1 deletion examples/shadow-cljs/src/shadowtest/server.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

(defonce server (atom nil))

(defn home-page [req res]
(defn home-page [_req res]
(.send res (render [:h1 "Hello world! yes"])))

(defn setup-routes [app]
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"dependencies": {
"@keyv/sqlite": "3.6.5",
"cookie-parser": "1.4.5",
"csurf": "1.11.0",
"csrf-csrf": "^3.0.3",
"express": "4.18.2",
"express-session": "1.17.2",
"html-to-text": "8.1.0",
Expand Down
6 changes: 3 additions & 3 deletions src/sitefox/deps.cljc
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@
:cljs
["node-html-parser" :refer [parse]])
#?(:org.babashka/nbb
["csurf$default" :as r-csrf]
["csrf-csrf" :refer [doubleCsrf]]
:cljs
["csurf" :as r-csrf])
["csrf-csrf" :refer [doubleCsrf]])
#?(:org.babashka/nbb
["keyv$default" :as r-Keyv]
:cljs
Expand All @@ -61,7 +61,7 @@
(def cookies r-cookies)
(def body-parser r-body-parser)
(def session r-session)
(def csrf r-csrf)
(def csrf doubleCsrf)
(def serve-static r-serve-static)
(def morgan r-morgan)
(def parse-html parse)
Expand Down
27 changes: 26 additions & 1 deletion src/sitefox/ui.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@
(second (re-find (js/RegExp. (str cookie-name "=([^;]+)")) (or cookies (aget js/document "cookie")))))

(defn csrf-token
"Returns the CSRF token passed by Sitefox in the `XSRF-TOKEN` header.
"Returns the CSRF token passed by Sitefox in the `XSRF-TOKEN` cookie.
Pass this value when doing POST requests via ajax:
```clojure
Expand All @@ -75,6 +75,31 @@
:XSRF-Token (csrf-token)}
:body (js/JSON.stringify (clj->js data))})
```
**Note**: passing the token via client side cookie is now deprecated.
If you want to re-enable it set the client side environment variable `SEND-CSRF-COOKIE`,
and then this function will work again.
See the README for more details: <https://github.com/chr15m/sitefox/#csrf-protection>
"
[]
(get-cookie "XSRF-TOKEN"))

(defn fetch-csrf-token
"Returns a promise which resolves to a string containing the CSRF token fetched from the Sitefox backend server.
Pass this value as the X-XSRF-TOKEN when doing POST requests via ajax fetch:
```clojure
(-> (fetch-csrf-token)
(.then (fn [token]
(js/fetch \"/api/endpoint\"
#js {:method \"POST\"
:headers #js {:Content-Type \"application/json\"
:X-XSRF-TOKEN token} ; <- use token here
:body (js/JSON.stringify (clj->js some-data))}))))
```
"
[]
(->
(js/fetch "/_csrf-token")
(.then #(.json %))))
19 changes: 17 additions & 2 deletions src/sitefox/web.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,23 @@
(let [pre-csrf-router (Router.)]
(.use app pre-csrf-router)
(j/assoc! app :pre-csrf-router pre-csrf-router))
(.use app (csrf #js {:cookie #js {:httpOnly true :sameSite "Strict" :secure true}}))
(.use app (fn [req res done] (j/call res :cookie "XSRF-TOKEN" (j/call req :csrfToken) #js {:secure true :sameSite "Strict"}) (done)))
(.use app (j/get (csrf #js {:getSecret (fn [] (env "SECRET" "DEVMODE"))
:cookieOptions #js {:httpOnly true :sameSite "Strict" :secure true}
:size 32
:cookieName "XSRF-TOKEN"
:getTokenFromRequest (fn [req]
(or (j/get-in req [:body :_csrf])
(j/call req :get "xsrf-token")
(j/call req :get "x-xsrf-token")))})
:doubleCsrfProtection))
(.get app "/_csrf-token"
(fn [req res]
(.json res (j/call req :csrfToken))))
(when (env "SEND-CSRF-COOKIE")
(.get app (fn [req res done]
(j/call res :cookie "XSRF-TOKEN" (j/call req :csrfToken)
#js {:secure true :sameSite "Strict"})
(done))))
app)

(defn static-folder
Expand Down
10 changes: 6 additions & 4 deletions src/sitefoxtest/e2etests.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,10 @@
(j/call-in server [:stdout :on] "data" #(console-listener js/console.log (aget env "DEBUG") %))
(j/call-in server [:stderr :on] "data" #(console-listener js/console.error true %))
(j/call-in server [:on] "exit" (fn [code]
(js/console.log "Server exited with code" code)
(js/console.log "Set DEBUG=1 to see stderr.")
(j/call js/process :exit code)))
(when (> code 0)
(js/console.log "Server exited with code: " code)
(js/console.log "Set DEBUG=1 to see stderr.")
(j/call js/process :exit code))))
(p/let [port-info (wait-for-port #js {:host host :port port})
pid (j/get server :pid)]
(log "Port found, server running with PID" pid)
Expand Down Expand Up @@ -106,6 +107,7 @@
(log "Test done. Killing server.")
(j/call server :kill)
(log "After server.")
(print)
(done))
#(catch-fail % done server))))))

Expand Down Expand Up @@ -357,5 +359,5 @@
(done))
#(catch-fail % done server browser))))))

; (t/run-test sitefoxtest.e2etests/nbb-forms)
;(t/run-test sitefoxtest.e2etests/nbb-forms)
(t/run-tests *ns*)

0 comments on commit c831b16

Please sign in to comment.