RingRESTルートを呼び出すClojureHTTPクライアントでクロスサイトリクエストフォージェリ防止を正しく使用するにはどうすればよいですか?

Dec 30 2020

私はまだClojure(およびそれに付随するすべてのライブラリ...)を学んでいるので、無知で愚かなことをした場合は、遠慮なく指摘してください:-)

POSTクライアントコードからメソッドを介してRESTエンドポイントを呼び出すのに問題があります。私のルートはを使用してラップされています(ring.middleware.defaults/wrap-defaults <my-routes> site-defaults)(このようなコードを本番環境で実行する場合は、これはかなり良い考えだと思います)。このラッパーはring.middleware.anti-forgery/wrap-anti-forgery、クロスサイトリクエストフォージェリ(CSRFまたはXSRF)防止スキームを実装するなど、他のさまざまなラッパーを適用します。デフォルト(私が使用している)はシンクロナイザートークン(またはセッション)戦略です。

を介して同じRESTエンドポイントを呼び出すGETことは問題なく機能しますが(CSRF保護が適用されていないためGETHEADOPTIONS呼び出します-を参照ring.middleware.anti-forgery/get-request?)、POST(または他の方法の1つを)使用すると403-無効な偽造防止トークン応答が発生します。

(以下のサンプルコードに見られるように)「X-CSRF-Token」または「X-XSRF-Token」ヘッダーをHTTPリクエストに追加する方法を知っています。(これはREST呼び出しであるため、この質問と回答で提案されているように、非表示の「__anti-forgery-token」フィールドは追加しませんが、ラッパーにはヘッダーまたはフォームフィールドのいずれかで十分ですring.middleware.anti-forgery/default-request-token。参照してください。) 、コードを正しく理解している場合、私の問題は、デフォルトの戦略が上記のトークンをセッショントークン値と比較するという事実に起因します。これは次の方法で取得されring.middleware.anti-forgery.session/session-tokenます。

(defn- session-token [request]
  (get-in request [:session :ring.middleware.anti-forgery/anti-forgery-token]))

HTTPクライアント呼び出しでセッション情報を正しく設定する方法がわかりません。(上記の403の結果はミドルウェアラッパーによって生成されるため、任意のHTTPクライアントで十分です。以下のデモでは、単純なものを使用しring.mock.requestます。)

これが私が今まで持っているものを示すいくつかの最小限のコードです。ルートとハンドラーを定義し、単体テストからそれらを呼び出そうとします。

(ns question.rest
  (:require [compojure.core :refer :all]
            [ring.middleware.defaults :refer [wrap-defaults site-defaults secure-site-defaults]]
            [ring.middleware.anti-forgery :refer [*anti-forgery-token*]]
            [clojure.test :refer :all]
            [ring.mock.request :as mock]))

(defroutes
  exmpl-routes
  (ANY "/" [] "Site up OK.")
  (GET "/aft" [] (force *anti-forgery-token*)))

(def exmpl (wrap-defaults exmpl-routes site-defaults))

(deftest test-mock-fail
  (testing "POST to root route"
    (let [
          ; In a normal web app, the view/page would be GET'ed from the server, which would
          ; include the Anti-Forgery Token in it, and have the POST as an action on it. Hence
          ; the way atf is done here...
          aft (:body (exmpl (mock/request :get "https://localhost:8443/aft")))
          request (-> (mock/request :post "https://localhost:8443/")
                      (mock/header "X-CSRF-Token" aft))
          _ (println request)
          response (exmpl request)
          _ (println response)
          ]
      (is (= 200 (:status response)))                       ;;403
      (is (= "Site up OK." (:body response))))))            ;;Invalid anti-forgery token

(println)呼び出しは次のことを示しています(いくつかのフォーマットが適用されています)。

リクエスト:

{  :protocol "HTTP/1.1", 
   :server-port 8443, 
   :server-name "localhost", 
   :remote-addr "localhost", 
   :uri "/post", 
   :scheme :https, 
   :request-method :post, 
   :headers {  "host" "localhost:8443",
               "x-csrf-token" "<long token value here>" }  }

応答:

{  :status 403, 
   :headers {  "Content-Type" "text/html; charset=utf-8",
               "X-XSS-Protection" "1; mode=block",
               "X-Frame-Options" "SAMEORIGIN",
               "X-Content-Type-Options" "nosniff" }, 
   :body "<h1>Invalid anti-forgery token</h1>"  }

私が見つけたチュートリアルは主にGETメソッドに集中しており、エンドポイント/ルートはサーバーから提供されるHTML(セッション情報を含む)から呼び出されると想定しているようです。ですから、今は少し行き詰まっているように感じます。

回答

2 SeanCorfield Dec 30 2020 at 06:38

RESTのAPIのためには使用すべきapi-defaultsか、secure-api-defaults しません site-defaults

偽造防止機構は、アプリがを生成しているWebサイト向けに設計さformれており、フォーム送信の一部として返送される生成されたトークンを含めるPOSTことができます。これは、RESTAPIでの使用を目的としたものではありません。