taoensso / sente

Realtime web comms library for Clojure/Script
https://www.taoensso.com/sente
Eclipse Public License 1.0
1.73k stars 193 forks source link

Bad CSRF token #376

Closed eneroth closed 3 years ago

eneroth commented 3 years ago

I'm bashing my head against the CSRF token.

This is what happens:

  1. Request sent to /
    {…
    :character-encoding "utf8",
    :uri "/",
    :server-name "localhost",
    :anti-forgery-token
    "Mo17F+bR6A/zGCkRNwf9Msk7VDuOemefmDZQxcUN005+eOW2iAiT3URCgJcMakt0uz9O5JNf9G/sLWCc",
    :query-string nil
    …}

anti-forgery generates a token. I grab that and set it in a cookie in the response:

{…
 :cookies {"csrf-token" *anti-forgery-token*}
 …}

Then retrieve it from there in CLJS:

…

(:import goog.net.Cookies)

(def cookies (Cookies. js/document))
(def csrf-token (cookies.get "csrf-token"))

(let [packer (sente-transit/get-transit-packer)
      {:keys [chsk ch-recv send-fn state]}
      (sente/make-channel-socket!
       "/chsk"
       csrf-token
       {:type :auto ;; :auto, :ajax, :ws
        :packer packer})])

…
  1. Sente hits up /chsk to upgrade the connection (token was successfully retrieved from cookie, as seen in query params)
{…
 :uri "/chsk",
 :websocket? true,
 :server-name "localhost",
 :anti-forgery-token
 "Mo17F+bR6A/zGCkRNwf9Msk7VDuOemefmDZQxcUN005+eOW2iAiT3URCgJcMakt0uz9O5JNf9G/sLWCc",
 :query-string
 "client-id=1a4faf71-8811-4011-9b56-f14a82a828fb&csrf-token=Mo17F%252BbR6A%252FzGCkRNwf9Msk7VDuOemefmDZQxcUN005%252BeOW2iAiT3URCgJcMakt0uz9O5JNf9G%252FsLWCc",
 :path-params {},
 :body nil,
 :scheme :http,
 :request-method :get,
 :session
 {:uid #uuid "b52b6e10-8d8b-42cc-bee3-d5ab0cad1f60",
  :ring.middleware.anti-forgery/anti-forgery-token
  "Mo17F+bR6A/zGCkRNwf9Msk7VDuOemefmDZQxcUN005+eOW2iAiT3URCgJcMakt0uz9O5JNf9G/sLWCc"}}
  1. Sente sends a handshake request to /chsk, this time without the CSRF-token as query param (is this as intended? No idea)
    {…
    :cookies
    {"ring-session" {:value "2d29b801-9f26-41cd-8b23-3b3bc5310187"},
    "csrf-token"
    {:value
    "Mo17F+bR6A/zGCkRNwf9Msk7VDuOemefmDZQxcUN005+eOW2iAiT3URCgJcMakt0uz9O5JNf9G/sLWCc"}},
    :remote-addr "0:0:0:0:0:0:0:1",
    :headers
    {"sec-fetch-site" "same-origin",
    "x-requested-with" "XMLHTTPRequest",
    "host" "localhost:8080",
    "user-agent"
    "Mozilla/5.0 (Windows NT 10.0; rv:78.0) Gecko/20100101 Firefox/78.0",
    "cookie"
    "ring-session=2d29b801-9f26-41cd-8b23-3b3bc5310187; csrf-token=Mo17F%2BbR6A%2FzGCkRNwf9Msk7VDuOemefmDZQxcUN005%2BeOW2iAiT3URCgJcMakt0uz9O5JNf9G%2FsLWCc",
    "referer" "http://localhost:8080/",
    "connection" "keep-alive",
    "x-csrf-token"
    "Mo17F%2BbR6A%2FzGCkRNwf9Msk7VDuOemefmDZQxcUN005%2BeOW2iAiT3URCgJcMakt0uz9O5JNf9G%2FsLWCc",
    "accept" "*/*",
    "accept-language" "en-US,en;q=0.5",
    "sec-fetch-dest" "empty",
    "accept-encoding" "gzip, deflate",
    "sec-fetch-mode" "cors",
    "dnt" "1"},
    :async-channel
    #object[org.httpkit.server.AsyncChannel 0x1b5311c "0.0.0.0/0.0.0.0:8080<->null"],
    :server-port 8080,
    :content-length 0,
    :websocket? false,
    :session/key "2d29b801-9f26-41cd-8b23-3b3bc5310187",
    :content-type nil,
    :character-encoding "utf8",
    :uri "/chsk",
    :server-name "localhost",
    :anti-forgery-token
    "Mo17F+bR6A/zGCkRNwf9Msk7VDuOemefmDZQxcUN005+eOW2iAiT3URCgJcMakt0uz9O5JNf9G/sLWCc",
    :query-string
    "udt=1597418224000&client-id=1a4faf71-8811-4011-9b56-f14a82a828fb&handshake%3F=true",
    :path-params {},
    :body nil,
    :scheme :http,
    :request-method :get,
    :session
    {:uid #uuid "b52b6e10-8d8b-42cc-bee3-d5ab0cad1f60",
    :ring.middleware.anti-forgery/anti-forgery-token
    "Mo17F+bR6A/zGCkRNwf9Msk7VDuOemefmDZQxcUN005+eOW2iAiT3URCgJcMakt0uz9O5JNf9G/sLWCc"}}

3 repeats forever, with Bad CSRF token as the only response.

What am I doing wrong here?

ptaoussanis commented 3 years ago

Hi Henrik,

Only had the opportunity to skim this - but just checking that you saw the example project? Was that no help?

eneroth commented 3 years ago

Hi!

I've largely followed the example project, with three differences:

  1. The token storage strategy (cookie)
  2. I'm using Reitit for routing
  3. I'm using the Transit packer.

Other than that, I'm following the basic structure of the example project pretty closely.

It works as intended with :csrf-token-fn nil.

ptaoussanis commented 3 years ago

Assuming the example project is working as intended - have you tried maybe introducing your differences to the example 1-at-a-time to see which specific change is causing trouble?

eneroth commented 3 years ago

After some vigorous logging, it seems that the token is escaped at some point when transferred to the client. Maybe Ring escapes it as it is set as cookie?

Either way it doesn't seem to be a problem with Sente itself: inspecting the cookie in the browser confirms that it's escaped before reaching storage, not upon retrieval.

{:server-token "OWFYMPMAaJyr1np0F/rJ89CdS4t6YhE3+PamQQHbvoS3lcSNYhVydH16Bww0A96BVWhlsxcDJB159BE0"
 :client-token "OWFYMPMAaJyr1np0F%2FrJ89CdS4t6YhE3%2BPamQQHbvoS3lcSNYhVydH16Bww0A96BVWhlsxcDJB159BE0"
 :ok? false}
eneroth commented 3 years ago

Yep, my fault for making assumptions and not reading the docs, as it turns out:

wrap-cookies
(wrap-cookies handler)
(wrap-cookies handler options)

Parses the cookies in the request map, then assocs the resulting map
to the :cookies key on the request.

Accepts the following options:

:decoder - a function to decode the cookie value. Expects a function that
           takes a string and returns a string. Defaults to URL-decoding.

:encoder - a function to encode the cookie name and value. Expects a
           function that takes a name/value map and returns a string.
           Defaults to URL-encoding.
eneroth commented 3 years ago

Solution:

Just make sure to URL decode token before handing to Sente:

(def cookies (Cookies. js/document))
(def csrf-token (js/decodeURIComponent (cookies.get "csrf-token")))

(Thanks for the suggestions along the way!)

ptaoussanis commented 3 years ago

Great, happy you found a solution Henrik. Thanks a lot for sharing the details- they could be helpful for others! Cheers :-)

green-coder commented 3 years ago

Another common reason why a Reitit users could have a Bad CSRF token issue: https://github.com/metosin/reitit/issues/205