Skip to content

Commit

Permalink
Merge pull request #329 from yetanalytics/sql-210
Browse files Browse the repository at this point in the history
[SQL-210] Security Header Interceptors
  • Loading branch information
cliffcaseyyet committed Aug 30, 2023
2 parents 5779c2e + 1750111 commit 2b60690
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 24 deletions.
70 changes: 56 additions & 14 deletions doc/env_vars.md

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions resources/lrsql/config/prod/default/webserver.edn
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@
:jwt-no-val-issuer #or [#env LRSQL_JWT_NO_VAL_ISSUER nil]
:jwt-no-val-role-key #or [#env LRSQL_JWT_NO_VAL_ROLE_KEY nil]
:jwt-no-val-role #or [#env LRSQL_JWT_NO_VAL_ROLE nil]
:sec-head-hsts #or [#env LRSQL_SEC_HEAD_HSTS nil]
:sec-head-frame #or [#env LRSQL_SEC_HEAD_FRAME nil]
:sec-head-content-type #or [#env LRSQL_SEC_HEAD_CONTENT_TYPE nil]
:sec-head-xss #or [#env LRSQL_SEC_HEAD_XSS nil]
:sec-head-download #or [#env LRSQL_SEC_HEAD_DOWNLOAD nil]
:sec-head-cross-domain #or [#env LRSQL_SEC_HEAD_CROSS_DOMAIN nil]
:sec-head-content #or [#env LRSQL_SEC_HEAD_CONTENT nil]
:enable-http #boolean #or [#env LRSQL_ENABLE_HTTP true]
:enable-http2 #boolean #or [#env LRSQL_ENABLE_HTTP2 true]
:http-host #or [#env LRSQL_HTTP_HOST "0.0.0.0"]
Expand Down
11 changes: 7 additions & 4 deletions src/main/lrsql/admin/routes.clj
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@
[lrsql.admin.interceptors.ui :as ui]
[lrsql.admin.interceptors.jwt :as ji]
[lrsql.admin.interceptors.status :as si]
[lrsql.util.interceptor :as util-i]))
[lrsql.util.interceptor :as util-i]
[lrsql.util.headers :as h]))

(defn- make-common-interceptors
[lrs]
[lrs sec-head-opts]
[i/error-interceptor
(util-i/handle-json-parse-exn true)
i/x-forwarded-for-interceptor
(h/secure-headers sec-head-opts)
json-body
(body-params)
(i/lrs-interceptor lrs)])
Expand Down Expand Up @@ -144,12 +146,13 @@
enable-admin-status
enable-account-routes
oidc-interceptors
oidc-ui-interceptors]
oidc-ui-interceptors
head-opts]
:or {oidc-interceptors []
oidc-ui-interceptors []
enable-account-routes true}}
routes]
(let [common-interceptors (make-common-interceptors lrs)
(let [common-interceptors (make-common-interceptors lrs head-opts)
common-interceptors-oidc (into common-interceptors oidc-interceptors)
no-val-opts {:no-val? no-val?
:no-val-uname no-val-uname
Expand Down
15 changes: 15 additions & 0 deletions src/main/lrsql/spec/config.clj
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,14 @@
(s/def ::oidc-verify-remote-issuer boolean?)
(s/def ::oidc-enable-local-admin boolean?)

(s/def ::sec-head-hsts (s/nilable string?))
(s/def ::sec-head-frame (s/nilable string?))
(s/def ::sec-head-content-type (s/nilable string?))
(s/def ::sec-head-xss (s/nilable string?))
(s/def ::sec-head-download (s/nilable string?))
(s/def ::sec-head-cross-domain (s/nilable string?))
(s/def ::sec-head-content (s/nilable string?))

(s/def ::webserver
(s/and
(s/keys :req-un [::http-host
Expand Down Expand Up @@ -202,6 +210,13 @@
::jwt-no-val-issuer
::jwt-no-val-role
::jwt-no-val-role-key
::sec-head-hsts
::sec-head-frame
::sec-head-content-type
::sec-head-xss
::sec-head-download
::sec-head-cross-domain
::sec-head-content
::oidc-issuer
::oidc-audience
::oidc-client-id])
Expand Down
17 changes: 16 additions & 1 deletion src/main/lrsql/system/webserver.clj
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@
enable-admin-ui
enable-admin-status
enable-stmt-html
sec-head-hsts
sec-head-frame
sec-head-content-type
sec-head-xss
sec-head-download
sec-head-cross-domain
sec-head-content
allow-all-origins
allowed-origins
jwt-no-val
Expand Down Expand Up @@ -72,7 +79,15 @@
:enable-admin-status enable-admin-status
:enable-account-routes enable-local-admin
:oidc-interceptors oidc-admin-interceptors
:oidc-ui-interceptors oidc-admin-ui-interceptors}))
:oidc-ui-interceptors oidc-admin-ui-interceptors
:head-opts
{:sec-head-hsts sec-head-hsts
:sec-head-frame sec-head-frame
:sec-head-content-type sec-head-content-type
:sec-head-xss sec-head-xss
:sec-head-download sec-head-download
:sec-head-cross-domain sec-head-cross-domain
:sec-head-content sec-head-content}}))
;; Build allowed-origins list. Add without ports as well for
;; default ports
allowed-list
Expand Down
58 changes: 58 additions & 0 deletions src/main/lrsql/util/headers.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
(ns lrsql.util.headers
(:require [io.pedestal.http.secure-headers :as hsh]
[io.pedestal.interceptor :refer [interceptor]]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; General
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(defn headers-interceptor
"Takes a map of header names to values and creates an interceptor to inject
them in response."
[headers]
(interceptor
{:leave (fn [{response :response :as context}]
(assoc-in context [:response :headers]
(merge headers (:headers response))))}))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Security Headers
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

(def default-value "[default]")

(def sec-head-defaults
{:sec-head-hsts (hsh/hsts-header)
:sec-head-frame (hsh/frame-options-header)
:sec-head-content-type (hsh/content-type-header)
:sec-head-xss (hsh/xss-protection-header)
:sec-head-download (hsh/download-options-header)
:sec-head-cross-domain (hsh/cross-domain-policies-header)
:sec-head-content (hsh/content-security-policy-header)})

(def sec-head-names
{:sec-head-hsts "Strict-Transport-Security"
:sec-head-frame "X-Frame-Options"
:sec-head-content-type "X-Content-Type-Options"
:sec-head-xss "X-XSS-Protection"
:sec-head-download "X-Download-Options"
:sec-head-cross-domain "X-Permitted-Cross-Domain-Policies"
:sec-head-content "Content-Security-Policy"})

(defn build-sec-headers
[sec-header-opts]
(reduce-kv
(fn [agg h-key h-val]
(if (string? h-val)
(assoc agg (get sec-head-names h-key)
(if (= default-value h-val)
(get sec-head-defaults h-key)
h-val))
agg)) {} sec-header-opts))

(defn secure-headers
"Iterate header-opts, generating values for each header and returning an
interceptor"
[sec-header-opts]
(let [sec-headers (build-sec-headers sec-header-opts)]
(headers-interceptor sec-headers)))
50 changes: 45 additions & 5 deletions src/test/lrsql/admin/route_test.clj
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
"Test for admin-related interceptors + routes
(as opposed to just the protocol)."
(:require [clojure.test :refer [deftest testing is use-fixtures]]
[clojure.string :refer [lower-case]]
[babashka.curl :as curl]
[com.stuartsierra.component :as component]
[xapi-schema.spec.regex :refer [Base64RegEx]]
[lrsql.test-support :as support]
[lrsql.util.headers :as h]
[lrsql.util :as u]))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
Expand Down Expand Up @@ -80,15 +82,18 @@
(catch clojure.lang.ExceptionInfo e#
(is (= ~code (-> e# ex-data :status))))))

(def sec-header-names (mapv #(lower-case (% h/sec-head-names))
(keys h/sec-head-names)))

(deftest admin-routes-test
(let [sys (support/test-system)
sys' (component/start sys)
;; Seed information
{:keys [api-key-default
api-secret-default]} (get-in sys' [:lrs :config])
{:keys [admin-user-default
admin-pass-default]} (get-in sys' [:lrs :config])
seed-body (u/write-json-str
{"username" api-key-default
"password" api-secret-default})
{"username" admin-user-default
"password" admin-pass-default})
seed-jwt (-> (login-account content-type seed-body)
:body
u/parse-json
Expand Down Expand Up @@ -134,7 +139,7 @@
;; success
(is (= 200 status))
;; is the created user
(is (= (get edn-body "username") api-key-default))))
(is (= (get edn-body "username") admin-user-default))))
(testing "log into the `myname` account"
(let [{:keys [status body]}
(login-account content-type req-body)
Expand Down Expand Up @@ -283,6 +288,41 @@
:throw false})]
;; failure
(is (= 400 status))))))
(testing "omitted sec headers because not configured"
(let [{:keys [headers]} (get-env content-type)]
(is (empty? (select-keys headers sec-header-names)))))
(component/stop sys')))

(def custom-sec-header-config
{:sec-head-hsts h/default-value
:sec-head-frame "Chocolate"
:sec-head-content-type h/default-value
:sec-head-xss "Banana"
:sec-head-download h/default-value
:sec-head-cross-domain "Pancakes"
:sec-head-content h/default-value})

(def custom-sec-header-expected
(reduce-kv
(fn [hdrs k v]
(assoc hdrs (lower-case (k h/sec-head-names))
(if (= v h/default-value)
(k h/sec-head-defaults)
v)))
{} custom-sec-header-config))

(deftest custom-header-admin-routes
(let [hdr-conf (reduce-kv (fn [m k v] (assoc m [:webserver k] v))
{} custom-sec-header-config)
sys (support/test-system
:conf-overrides hdr-conf)
sys' (component/start sys)]
(testing "Custom Sec Headers"
;; Run a basic admin routes call and verify success
(let [{:keys [headers]} (get-env content-type)]
;; equals the same combination of custom and default hdr values
(is (= custom-sec-header-expected
(select-keys headers sec-header-names)))))
(component/stop sys')))

(def proxy-jwt-body
Expand Down

0 comments on commit 2b60690

Please sign in to comment.