¿Cómo utilizo correctamente la prevención de falsificación de solicitudes entre sitios en un cliente HTTP de Clojure que llama a una ruta Ring REST?

Dec 30 2020

Todavía estoy aprendiendo Clojure (y todas esas bibliotecas que lo acompañan ...), así que si hago algo estúpido en mi ignorancia, no dude en señalarlo :-)

Tengo problemas para llamar a los puntos finales REST a través del POSTmétodo desde el código del cliente. Mis rutas se envuelven usando (ring.middleware.defaults/wrap-defaults <my-routes> site-defaults)(creo que esta es una idea bastante buena si alguna vez dicho código se ejecuta en producción). Este contenedor aplica varios otros contenedores, incluido el ring.middleware.anti-forgery/wrap-anti-forgeryque implementa (entre otros) un esquema de prevención de falsificación de solicitudes entre sitios (CSRF o XSRF): el predeterminado (que estoy usando) es una estrategia de token (o sesión) sincronizador .

Llamar al mismo punto final REST a través de GETfunciona bien (porque la protección CSRF no se aplica a GET, HEADy las OPTIONSllamadas, consulte ring.middleware.anti-forgery/get-request?), pero el uso POST(o uno de los otros métodos) da como resultado una respuesta de token antifalsificación 403: no válida .

(Como se ve en el código de muestra a continuación) Sé cómo agregar un encabezado "X-CSRF-Token" o "X-XSRF-Token" a la solicitud HTTP. (Dado que esta es una llamada REST, no agrego un campo oculto "__anti-forgery-token" como lo sugiere esta pregunta y respuesta , aunque uno de los encabezados o el campo de formulario sería suficiente para el contenedor, consulte ring.middleware.anti-forgery/default-request-token). , si entiendo el código correctamente, mi problema se debe al hecho de que la estrategia predeterminada compara el token anterior con un valor de token de sesión, que se recupera mediante ring.middleware.anti-forgery.session/session-token:

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

No tengo idea de cómo configurar la información de la sesión correctamente en la llamada del cliente HTTP. (Cualquier cliente HTTP es suficiente, ya que el resultado 403 mencionado anteriormente es generado por el contenedor de middleware. Para la siguiente demostración, uso el simple ring.mock.request).

Aquí hay un código mínimo que muestra lo que tengo hasta ahora. Define las rutas y los controladores, luego intenta llamarlos desde una prueba unitaria.

(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

Las (println)llamadas muestran lo siguiente (se aplica algún formato):

Pedido:

{  :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>" }  }

Respuesta:

{  :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>"  }

Los tutoriales que pude encontrar se concentran principalmente en el GETmétodo y parecen suponer que los puntos finales / rutas se llamarían desde HTML, que se sirve desde el servidor (que incluye información de sesión). Así que me siento un poco estancado en este momento.

Respuestas

2 SeanCorfield Dec 30 2020 at 06:38

Para una API REST, debe usar api-defaultso secure-api-defaults NOT site-defaults .

La maquinaria anti-falsificación está diseñada para sitios web donde la aplicación está generando un formy puede incluir el token generado para ser devuelto como parte del envío del formulario POST; no está diseñada para usarse con una API REST.