mirror of
https://github.com/penpot/penpot.git
synced 2026-05-29 19:58:09 +00:00
Merge pull request #7402 from penpot/niwinz-develop-enhancements-3
✨ Add additional http middlewares
This commit is contained in:
commit
73ed5f8bc5
@ -146,7 +146,6 @@
|
|||||||
[:quotes-team-access-requests-per-team {:optional true} ::sm/int]
|
[:quotes-team-access-requests-per-team {:optional true} ::sm/int]
|
||||||
[:quotes-team-access-requests-per-requester {:optional true} ::sm/int]
|
[:quotes-team-access-requests-per-requester {:optional true} ::sm/int]
|
||||||
|
|
||||||
[:auth-data-cookie-domain {:optional true} :string]
|
|
||||||
[:auth-token-cookie-name {:optional true} :string]
|
[:auth-token-cookie-name {:optional true} :string]
|
||||||
[:auth-token-cookie-max-age {:optional true} ::ct/duration]
|
[:auth-token-cookie-max-age {:optional true} ::ct/duration]
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
[app.http.errors :as errors]
|
[app.http.errors :as errors]
|
||||||
[app.http.management :as mgmt]
|
[app.http.management :as mgmt]
|
||||||
[app.http.middleware :as mw]
|
[app.http.middleware :as mw]
|
||||||
|
[app.http.security :as sec]
|
||||||
[app.http.session :as session]
|
[app.http.session :as session]
|
||||||
[app.http.websocket :as-alias ws]
|
[app.http.websocket :as-alias ws]
|
||||||
[app.main :as-alias main]
|
[app.main :as-alias main]
|
||||||
@ -167,6 +168,7 @@
|
|||||||
[_ cfg]
|
[_ cfg]
|
||||||
(rr/router
|
(rr/router
|
||||||
[["" {:middleware [[mw/server-timing]
|
[["" {:middleware [[mw/server-timing]
|
||||||
|
[sec/sec-fetch-metadata]
|
||||||
[mw/params]
|
[mw/params]
|
||||||
[mw/format-response]
|
[mw/format-response]
|
||||||
[session/soft-auth cfg]
|
[session/soft-auth cfg]
|
||||||
@ -187,7 +189,8 @@
|
|||||||
|
|
||||||
(::ws/routes cfg)
|
(::ws/routes cfg)
|
||||||
|
|
||||||
["/api" {:middleware [[mw/cors]]}
|
["/api" {:middleware [[mw/cors]
|
||||||
|
[sec/client-header-check]]}
|
||||||
(::oidc/routes cfg)
|
(::oidc/routes cfg)
|
||||||
(::rpc.doc/routes cfg)
|
(::rpc.doc/routes cfg)
|
||||||
(::rpc/routes cfg)]]]))
|
(::rpc/routes cfg)]]]))
|
||||||
|
|||||||
55
backend/src/app/http/security.clj
Normal file
55
backend/src/app/http/security.clj
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
(ns app.http.security
|
||||||
|
"Additional security layer middlewares"
|
||||||
|
(:require
|
||||||
|
[app.config :as cf]
|
||||||
|
[yetti.request :as yreq]
|
||||||
|
[yetti.response :as yres]))
|
||||||
|
|
||||||
|
(def ^:private safe-methods
|
||||||
|
#{:get :head :options})
|
||||||
|
|
||||||
|
(defn- wrap-sec-fetch-metadata
|
||||||
|
"Sec-Fetch metadata security layer middleware"
|
||||||
|
[handler]
|
||||||
|
(fn [request]
|
||||||
|
(let [site (yreq/get-header request "sec-fetch-site")]
|
||||||
|
(cond
|
||||||
|
(= site "same-origin")
|
||||||
|
(handler request)
|
||||||
|
|
||||||
|
(or (= site "same-site")
|
||||||
|
(= site "cross-site"))
|
||||||
|
(if (contains? safe-methods (yreq/method request))
|
||||||
|
(handler request)
|
||||||
|
{::yres/status 403})
|
||||||
|
|
||||||
|
:else
|
||||||
|
(handler request)))))
|
||||||
|
|
||||||
|
(def sec-fetch-metadata
|
||||||
|
{:name ::sec-fetch-metadata
|
||||||
|
:compile (fn [_ _]
|
||||||
|
(when (contains? cf/flags :sec-fetch-metadata-middleware)
|
||||||
|
wrap-sec-fetch-metadata))})
|
||||||
|
|
||||||
|
(defn- wrap-client-header-check
|
||||||
|
"Check for a penpot custom header to be present as additional CSRF
|
||||||
|
protection"
|
||||||
|
[handler]
|
||||||
|
(fn [request]
|
||||||
|
(let [client (yreq/get-header request "x-client")]
|
||||||
|
(if (some? client)
|
||||||
|
(handler request)
|
||||||
|
{::yres/status 403}))))
|
||||||
|
|
||||||
|
(def client-header-check
|
||||||
|
{:name ::client-header-check
|
||||||
|
:compile (fn [_ _]
|
||||||
|
(when (contains? cf/flags :client-header-check-middleware)
|
||||||
|
wrap-client-header-check))})
|
||||||
@ -11,7 +11,6 @@
|
|||||||
[app.common.logging :as l]
|
[app.common.logging :as l]
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.time :as ct]
|
[app.common.time :as ct]
|
||||||
[app.common.uri :as u]
|
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.db.sql :as sql]
|
[app.db.sql :as sql]
|
||||||
@ -148,9 +147,7 @@
|
|||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(declare ^:private assign-auth-token-cookie)
|
(declare ^:private assign-auth-token-cookie)
|
||||||
(declare ^:private assign-auth-data-cookie)
|
|
||||||
(declare ^:private clear-auth-token-cookie)
|
(declare ^:private clear-auth-token-cookie)
|
||||||
(declare ^:private clear-auth-data-cookie)
|
|
||||||
(declare ^:private gen-token)
|
(declare ^:private gen-token)
|
||||||
|
|
||||||
(defn create-fn
|
(defn create-fn
|
||||||
@ -164,10 +161,9 @@
|
|||||||
:user-agent uagent}
|
:user-agent uagent}
|
||||||
token (gen-token cfg params)
|
token (gen-token cfg params)
|
||||||
session (write! manager token params)]
|
session (write! manager token params)]
|
||||||
(l/trace :hint "create" :profile-id (str profile-id))
|
(l/trc :hint "create" :profile-id (str profile-id))
|
||||||
(-> response
|
(-> response
|
||||||
(assign-auth-token-cookie session)
|
(assign-auth-token-cookie session)))))
|
||||||
(assign-auth-data-cookie session)))))
|
|
||||||
|
|
||||||
(defn delete-fn
|
(defn delete-fn
|
||||||
[{:keys [::manager]}]
|
[{:keys [::manager]}]
|
||||||
@ -175,13 +171,12 @@
|
|||||||
(fn [request response]
|
(fn [request response]
|
||||||
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
|
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)
|
||||||
cookie (yreq/get-cookie request cname)]
|
cookie (yreq/get-cookie request cname)]
|
||||||
(l/trace :hint "delete" :profile-id (:profile-id request))
|
(l/trc :hint "delete" :profile-id (:profile-id request))
|
||||||
(some->> (:value cookie) (delete! manager))
|
(some->> (:value cookie) (delete! manager))
|
||||||
(-> response
|
(-> response
|
||||||
(assoc :status 204)
|
(assoc :status 204)
|
||||||
(assoc :body nil)
|
(assoc :body nil)
|
||||||
(clear-auth-token-cookie)
|
(clear-auth-token-cookie)))))
|
||||||
(clear-auth-data-cookie)))))
|
|
||||||
|
|
||||||
(defn- gen-token
|
(defn- gen-token
|
||||||
[cfg {:keys [profile-id created-at]}]
|
[cfg {:keys [profile-id created-at]}]
|
||||||
@ -222,7 +217,7 @@
|
|||||||
(-> (assoc ::token-claims claims)
|
(-> (assoc ::token-claims claims)
|
||||||
(assoc ::token token))))
|
(assoc ::token token))))
|
||||||
(catch Throwable cause
|
(catch Throwable cause
|
||||||
(l/trace :hint "exception on decoding malformed token" :cause cause)
|
(l/trc :hint "exception on decoding malformed token" :cause cause)
|
||||||
request)))]
|
request)))]
|
||||||
|
|
||||||
(fn [request]
|
(fn [request]
|
||||||
@ -242,8 +237,7 @@
|
|||||||
(if (renew-session? session)
|
(if (renew-session? session)
|
||||||
(let [session (update! manager session)]
|
(let [session (update! manager session)]
|
||||||
(-> response
|
(-> response
|
||||||
(assign-auth-token-cookie session)
|
(assign-auth-token-cookie session)))
|
||||||
(assign-auth-data-cookie session)))
|
|
||||||
response))))
|
response))))
|
||||||
|
|
||||||
(def soft-auth
|
(def soft-auth
|
||||||
@ -276,46 +270,11 @@
|
|||||||
:secure secure?}]
|
:secure secure?}]
|
||||||
(update response :cookies assoc name cookie)))
|
(update response :cookies assoc name cookie)))
|
||||||
|
|
||||||
(defn- assign-auth-data-cookie
|
|
||||||
[response {profile-id :profile-id updated-at :updated-at}]
|
|
||||||
(let [max-age (cf/get :auth-token-cookie-max-age default-cookie-max-age)
|
|
||||||
domain (cf/get :auth-data-cookie-domain)
|
|
||||||
cname default-auth-data-cookie-name
|
|
||||||
|
|
||||||
created-at updated-at
|
|
||||||
renewal (ct/plus created-at default-renewal-max-age)
|
|
||||||
expires (ct/plus created-at max-age)
|
|
||||||
|
|
||||||
comment (str "Renewal at: " (ct/format-inst renewal :rfc1123))
|
|
||||||
secure? (contains? cf/flags :secure-session-cookies)
|
|
||||||
strict? (contains? cf/flags :strict-session-cookies)
|
|
||||||
cors? (contains? cf/flags :cors)
|
|
||||||
|
|
||||||
cookie {:domain domain
|
|
||||||
:expires expires
|
|
||||||
:path "/"
|
|
||||||
:comment comment
|
|
||||||
:value (u/map->query-string {:profile-id profile-id})
|
|
||||||
:same-site (if cors? :none (if strict? :strict :lax))
|
|
||||||
:secure secure?}]
|
|
||||||
|
|
||||||
(cond-> response
|
|
||||||
(string? domain)
|
|
||||||
(update :cookies assoc cname cookie))))
|
|
||||||
|
|
||||||
(defn- clear-auth-token-cookie
|
(defn- clear-auth-token-cookie
|
||||||
[response]
|
[response]
|
||||||
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)]
|
(let [cname (cf/get :auth-token-cookie-name default-auth-token-cookie-name)]
|
||||||
(update response :cookies assoc cname {:path "/" :value "" :max-age 0})))
|
(update response :cookies assoc cname {:path "/" :value "" :max-age 0})))
|
||||||
|
|
||||||
(defn- clear-auth-data-cookie
|
|
||||||
[response]
|
|
||||||
(let [cname default-auth-data-cookie-name
|
|
||||||
domain (cf/get :auth-data-cookie-domain)]
|
|
||||||
(cond-> response
|
|
||||||
(string? domain)
|
|
||||||
(update :cookies assoc cname {:domain domain :path "/" :value "" :max-age 0}))))
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; TASK: SESSION GC
|
;; TASK: SESSION GC
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
@ -342,13 +301,13 @@
|
|||||||
(let [threshold (ct/minus (ct/now) max-age)
|
(let [threshold (ct/minus (ct/now) max-age)
|
||||||
result (-> (db/exec-one! conn [sql:delete-expired threshold threshold])
|
result (-> (db/exec-one! conn [sql:delete-expired threshold threshold])
|
||||||
(db/get-update-count))]
|
(db/get-update-count))]
|
||||||
(l/debug :task "gc"
|
(l/dbg :task "gc"
|
||||||
:hint "clean http sessions"
|
:hint "clean http sessions"
|
||||||
:deleted result)
|
:deleted result)
|
||||||
result))
|
result))
|
||||||
|
|
||||||
(defmethod ig/init-key ::tasks/gc
|
(defmethod ig/init-key ::tasks/gc
|
||||||
[_ {:keys [::tasks/max-age] :as cfg}]
|
[_ {:keys [::tasks/max-age] :as cfg}]
|
||||||
(l/debug :hint "initializing session gc task" :max-age max-age)
|
(l/dbg :hint "initializing session gc task" :max-age max-age)
|
||||||
(fn [_]
|
(fn [_]
|
||||||
(db/tx-run! cfg collect-expired-tasks)))
|
(db/tx-run! cfg collect-expired-tasks)))
|
||||||
|
|||||||
59
backend/test/backend_tests/http_middleware_security.clj
Normal file
59
backend/test/backend_tests/http_middleware_security.clj
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
;;
|
||||||
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
|
(ns backend-tests.http-middleware-security
|
||||||
|
(:require
|
||||||
|
[app.http.security :as sec]
|
||||||
|
[clojure.test :as t]
|
||||||
|
[yetti.request :as yreq]
|
||||||
|
[yetti.response :as yres]))
|
||||||
|
|
||||||
|
(defn- mock-request
|
||||||
|
[method value]
|
||||||
|
(reify yreq/IRequest
|
||||||
|
(method [_]
|
||||||
|
method)
|
||||||
|
(get-header [_ _]
|
||||||
|
value)))
|
||||||
|
|
||||||
|
(t/deftest sec-fetch-metadata
|
||||||
|
(let [request1 (mock-request :get "same-origin")
|
||||||
|
request2 (mock-request :post "same-origin")
|
||||||
|
request3 (mock-request :get "same-site")
|
||||||
|
request4 (mock-request :post "same-site")
|
||||||
|
request5 (mock-request :get "cross-site")
|
||||||
|
request6 (mock-request :post "cross-site")
|
||||||
|
|
||||||
|
handler (fn [request]
|
||||||
|
{::yres/status 200})
|
||||||
|
handler (#'sec/wrap-sec-fetch-metadata handler)
|
||||||
|
resp1 (handler request1)
|
||||||
|
resp2 (handler request2)
|
||||||
|
resp3 (handler request3)
|
||||||
|
resp4 (handler request4)
|
||||||
|
resp5 (handler request5)
|
||||||
|
resp6 (handler request6)]
|
||||||
|
|
||||||
|
(t/is (= 200 (::yres/status resp1)))
|
||||||
|
(t/is (= 200 (::yres/status resp2)))
|
||||||
|
(t/is (= 200 (::yres/status resp3)))
|
||||||
|
(t/is (= 403 (::yres/status resp4)))
|
||||||
|
(t/is (= 200 (::yres/status resp5)))
|
||||||
|
(t/is (= 403 (::yres/status resp6)))))
|
||||||
|
|
||||||
|
(t/deftest client-header-check
|
||||||
|
(let [request1 (mock-request :get "some")
|
||||||
|
request2 (mock-request :post nil)
|
||||||
|
|
||||||
|
handler (fn [request]
|
||||||
|
{::yres/status 200})
|
||||||
|
handler (#'sec/wrap-client-header-check handler)
|
||||||
|
resp1 (handler request1)
|
||||||
|
resp2 (handler request2)]
|
||||||
|
|
||||||
|
(t/is (= 200 (::yres/status resp1)))
|
||||||
|
(t/is (= 403 (::yres/status resp2)))))
|
||||||
|
|
||||||
@ -135,7 +135,15 @@
|
|||||||
:subscriptions
|
:subscriptions
|
||||||
:subscriptions-old
|
:subscriptions-old
|
||||||
:frontend-binary-fills
|
:frontend-binary-fills
|
||||||
:inspect-styles})
|
:inspect-styles
|
||||||
|
|
||||||
|
;; Security layer middleware that filters request by fetch
|
||||||
|
;; metadata headers
|
||||||
|
:sec-fetch-metadata-middleware
|
||||||
|
|
||||||
|
;; Security layer middleware that check the precense of x-client
|
||||||
|
;; http headers and enables an addtional csrf protection
|
||||||
|
:client-header-check-middleware})
|
||||||
|
|
||||||
(def all-flags
|
(def all-flags
|
||||||
(set/union email login varia))
|
(set/union email login varia))
|
||||||
|
|||||||
@ -51,7 +51,8 @@
|
|||||||
|
|
||||||
(defn default-headers
|
(defn default-headers
|
||||||
[]
|
[]
|
||||||
{"x-frontend-version" (:full cfg/version)})
|
{"x-frontend-version" (:full cfg/version)
|
||||||
|
"x-client" (str "penpot-frontend/" (:full cfg/version))})
|
||||||
|
|
||||||
(defn fetch
|
(defn fetch
|
||||||
[{:keys [method uri query headers body mode omit-default-headers credentials]
|
[{:keys [method uri query headers body mode omit-default-headers credentials]
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user