Skip to content

Commit 2b60690

Browse files
Merge pull request #329 from yetanalytics/sql-210
[SQL-210] Security Header Interceptors
2 parents 5779c2e + 1750111 commit 2b60690

File tree

7 files changed

+204
-24
lines changed

7 files changed

+204
-24
lines changed

doc/env_vars.md

Lines changed: 56 additions & 14 deletions
Large diffs are not rendered by default.

resources/lrsql/config/prod/default/webserver.edn

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@
1111
:jwt-no-val-issuer #or [#env LRSQL_JWT_NO_VAL_ISSUER nil]
1212
:jwt-no-val-role-key #or [#env LRSQL_JWT_NO_VAL_ROLE_KEY nil]
1313
:jwt-no-val-role #or [#env LRSQL_JWT_NO_VAL_ROLE nil]
14+
:sec-head-hsts #or [#env LRSQL_SEC_HEAD_HSTS nil]
15+
:sec-head-frame #or [#env LRSQL_SEC_HEAD_FRAME nil]
16+
:sec-head-content-type #or [#env LRSQL_SEC_HEAD_CONTENT_TYPE nil]
17+
:sec-head-xss #or [#env LRSQL_SEC_HEAD_XSS nil]
18+
:sec-head-download #or [#env LRSQL_SEC_HEAD_DOWNLOAD nil]
19+
:sec-head-cross-domain #or [#env LRSQL_SEC_HEAD_CROSS_DOMAIN nil]
20+
:sec-head-content #or [#env LRSQL_SEC_HEAD_CONTENT nil]
1421
:enable-http #boolean #or [#env LRSQL_ENABLE_HTTP true]
1522
:enable-http2 #boolean #or [#env LRSQL_ENABLE_HTTP2 true]
1623
:http-host #or [#env LRSQL_HTTP_HOST "0.0.0.0"]

src/main/lrsql/admin/routes.clj

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@
88
[lrsql.admin.interceptors.ui :as ui]
99
[lrsql.admin.interceptors.jwt :as ji]
1010
[lrsql.admin.interceptors.status :as si]
11-
[lrsql.util.interceptor :as util-i]))
11+
[lrsql.util.interceptor :as util-i]
12+
[lrsql.util.headers :as h]))
1213

1314
(defn- make-common-interceptors
14-
[lrs]
15+
[lrs sec-head-opts]
1516
[i/error-interceptor
1617
(util-i/handle-json-parse-exn true)
1718
i/x-forwarded-for-interceptor
19+
(h/secure-headers sec-head-opts)
1820
json-body
1921
(body-params)
2022
(i/lrs-interceptor lrs)])
@@ -144,12 +146,13 @@
144146
enable-admin-status
145147
enable-account-routes
146148
oidc-interceptors
147-
oidc-ui-interceptors]
149+
oidc-ui-interceptors
150+
head-opts]
148151
:or {oidc-interceptors []
149152
oidc-ui-interceptors []
150153
enable-account-routes true}}
151154
routes]
152-
(let [common-interceptors (make-common-interceptors lrs)
155+
(let [common-interceptors (make-common-interceptors lrs head-opts)
153156
common-interceptors-oidc (into common-interceptors oidc-interceptors)
154157
no-val-opts {:no-val? no-val?
155158
:no-val-uname no-val-uname

src/main/lrsql/spec/config.clj

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,14 @@
173173
(s/def ::oidc-verify-remote-issuer boolean?)
174174
(s/def ::oidc-enable-local-admin boolean?)
175175

176+
(s/def ::sec-head-hsts (s/nilable string?))
177+
(s/def ::sec-head-frame (s/nilable string?))
178+
(s/def ::sec-head-content-type (s/nilable string?))
179+
(s/def ::sec-head-xss (s/nilable string?))
180+
(s/def ::sec-head-download (s/nilable string?))
181+
(s/def ::sec-head-cross-domain (s/nilable string?))
182+
(s/def ::sec-head-content (s/nilable string?))
183+
176184
(s/def ::webserver
177185
(s/and
178186
(s/keys :req-un [::http-host
@@ -202,6 +210,13 @@
202210
::jwt-no-val-issuer
203211
::jwt-no-val-role
204212
::jwt-no-val-role-key
213+
::sec-head-hsts
214+
::sec-head-frame
215+
::sec-head-content-type
216+
::sec-head-xss
217+
::sec-head-download
218+
::sec-head-cross-domain
219+
::sec-head-content
205220
::oidc-issuer
206221
::oidc-audience
207222
::oidc-client-id])

src/main/lrsql/system/webserver.clj

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@
2727
enable-admin-ui
2828
enable-admin-status
2929
enable-stmt-html
30+
sec-head-hsts
31+
sec-head-frame
32+
sec-head-content-type
33+
sec-head-xss
34+
sec-head-download
35+
sec-head-cross-domain
36+
sec-head-content
3037
allow-all-origins
3138
allowed-origins
3239
jwt-no-val
@@ -72,7 +79,15 @@
7279
:enable-admin-status enable-admin-status
7380
:enable-account-routes enable-local-admin
7481
:oidc-interceptors oidc-admin-interceptors
75-
:oidc-ui-interceptors oidc-admin-ui-interceptors}))
82+
:oidc-ui-interceptors oidc-admin-ui-interceptors
83+
:head-opts
84+
{:sec-head-hsts sec-head-hsts
85+
:sec-head-frame sec-head-frame
86+
:sec-head-content-type sec-head-content-type
87+
:sec-head-xss sec-head-xss
88+
:sec-head-download sec-head-download
89+
:sec-head-cross-domain sec-head-cross-domain
90+
:sec-head-content sec-head-content}}))
7691
;; Build allowed-origins list. Add without ports as well for
7792
;; default ports
7893
allowed-list

src/main/lrsql/util/headers.clj

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
(ns lrsql.util.headers
2+
(:require [io.pedestal.http.secure-headers :as hsh]
3+
[io.pedestal.interceptor :refer [interceptor]]))
4+
5+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
6+
;; General
7+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
8+
9+
(defn headers-interceptor
10+
"Takes a map of header names to values and creates an interceptor to inject
11+
them in response."
12+
[headers]
13+
(interceptor
14+
{:leave (fn [{response :response :as context}]
15+
(assoc-in context [:response :headers]
16+
(merge headers (:headers response))))}))
17+
18+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
19+
;; Security Headers
20+
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
21+
22+
(def default-value "[default]")
23+
24+
(def sec-head-defaults
25+
{:sec-head-hsts (hsh/hsts-header)
26+
:sec-head-frame (hsh/frame-options-header)
27+
:sec-head-content-type (hsh/content-type-header)
28+
:sec-head-xss (hsh/xss-protection-header)
29+
:sec-head-download (hsh/download-options-header)
30+
:sec-head-cross-domain (hsh/cross-domain-policies-header)
31+
:sec-head-content (hsh/content-security-policy-header)})
32+
33+
(def sec-head-names
34+
{:sec-head-hsts "Strict-Transport-Security"
35+
:sec-head-frame "X-Frame-Options"
36+
:sec-head-content-type "X-Content-Type-Options"
37+
:sec-head-xss "X-XSS-Protection"
38+
:sec-head-download "X-Download-Options"
39+
:sec-head-cross-domain "X-Permitted-Cross-Domain-Policies"
40+
:sec-head-content "Content-Security-Policy"})
41+
42+
(defn build-sec-headers
43+
[sec-header-opts]
44+
(reduce-kv
45+
(fn [agg h-key h-val]
46+
(if (string? h-val)
47+
(assoc agg (get sec-head-names h-key)
48+
(if (= default-value h-val)
49+
(get sec-head-defaults h-key)
50+
h-val))
51+
agg)) {} sec-header-opts))
52+
53+
(defn secure-headers
54+
"Iterate header-opts, generating values for each header and returning an
55+
interceptor"
56+
[sec-header-opts]
57+
(let [sec-headers (build-sec-headers sec-header-opts)]
58+
(headers-interceptor sec-headers)))

src/test/lrsql/admin/route_test.clj

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22
"Test for admin-related interceptors + routes
33
(as opposed to just the protocol)."
44
(:require [clojure.test :refer [deftest testing is use-fixtures]]
5+
[clojure.string :refer [lower-case]]
56
[babashka.curl :as curl]
67
[com.stuartsierra.component :as component]
78
[xapi-schema.spec.regex :refer [Base64RegEx]]
89
[lrsql.test-support :as support]
10+
[lrsql.util.headers :as h]
911
[lrsql.util :as u]))
1012

1113
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -80,15 +82,18 @@
8082
(catch clojure.lang.ExceptionInfo e#
8183
(is (= ~code (-> e# ex-data :status))))))
8284

85+
(def sec-header-names (mapv #(lower-case (% h/sec-head-names))
86+
(keys h/sec-head-names)))
87+
8388
(deftest admin-routes-test
8489
(let [sys (support/test-system)
8590
sys' (component/start sys)
8691
;; Seed information
87-
{:keys [api-key-default
88-
api-secret-default]} (get-in sys' [:lrs :config])
92+
{:keys [admin-user-default
93+
admin-pass-default]} (get-in sys' [:lrs :config])
8994
seed-body (u/write-json-str
90-
{"username" api-key-default
91-
"password" api-secret-default})
95+
{"username" admin-user-default
96+
"password" admin-pass-default})
9297
seed-jwt (-> (login-account content-type seed-body)
9398
:body
9499
u/parse-json
@@ -134,7 +139,7 @@
134139
;; success
135140
(is (= 200 status))
136141
;; is the created user
137-
(is (= (get edn-body "username") api-key-default))))
142+
(is (= (get edn-body "username") admin-user-default))))
138143
(testing "log into the `myname` account"
139144
(let [{:keys [status body]}
140145
(login-account content-type req-body)
@@ -283,6 +288,41 @@
283288
:throw false})]
284289
;; failure
285290
(is (= 400 status))))))
291+
(testing "omitted sec headers because not configured"
292+
(let [{:keys [headers]} (get-env content-type)]
293+
(is (empty? (select-keys headers sec-header-names)))))
294+
(component/stop sys')))
295+
296+
(def custom-sec-header-config
297+
{:sec-head-hsts h/default-value
298+
:sec-head-frame "Chocolate"
299+
:sec-head-content-type h/default-value
300+
:sec-head-xss "Banana"
301+
:sec-head-download h/default-value
302+
:sec-head-cross-domain "Pancakes"
303+
:sec-head-content h/default-value})
304+
305+
(def custom-sec-header-expected
306+
(reduce-kv
307+
(fn [hdrs k v]
308+
(assoc hdrs (lower-case (k h/sec-head-names))
309+
(if (= v h/default-value)
310+
(k h/sec-head-defaults)
311+
v)))
312+
{} custom-sec-header-config))
313+
314+
(deftest custom-header-admin-routes
315+
(let [hdr-conf (reduce-kv (fn [m k v] (assoc m [:webserver k] v))
316+
{} custom-sec-header-config)
317+
sys (support/test-system
318+
:conf-overrides hdr-conf)
319+
sys' (component/start sys)]
320+
(testing "Custom Sec Headers"
321+
;; Run a basic admin routes call and verify success
322+
(let [{:keys [headers]} (get-env content-type)]
323+
;; equals the same combination of custom and default hdr values
324+
(is (= custom-sec-header-expected
325+
(select-keys headers sec-header-names)))))
286326
(component/stop sys')))
287327

288328
(def proxy-jwt-body

0 commit comments

Comments
 (0)