dream: security headers + cache-control middleware + 12 tests
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 38s
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 38s
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,7 @@ MODULES=(
|
||||
"lib/dream/json.sx"
|
||||
"lib/dream/auth.sx"
|
||||
"lib/dream/html.sx"
|
||||
"lib/dream/headers.sx"
|
||||
"lib/dream/run.sx"
|
||||
"lib/dream/api.sx"
|
||||
"lib/dream/demos/hello.sx"
|
||||
@@ -58,6 +59,7 @@ SUITES=(
|
||||
"json dream-js-tests-run! lib/dream/tests/json.sx"
|
||||
"auth dream-au-tests-run! lib/dream/tests/auth.sx"
|
||||
"html dream-ht-tests-run! lib/dream/tests/html.sx"
|
||||
"headers dream-hd-tests-run! lib/dream/tests/headers.sx"
|
||||
"run dream-rn-tests-run! lib/dream/tests/run.sx"
|
||||
"api dream-ap-tests-run! lib/dream/tests/api.sx"
|
||||
"demos dream-dm-tests-run! lib/dream/tests/demos.sx"
|
||||
|
||||
54
lib/dream/headers.sx
Normal file
54
lib/dream/headers.sx
Normal file
@@ -0,0 +1,54 @@
|
||||
;; lib/dream/headers.sx — Dream-on-SX security headers + cache-control helpers.
|
||||
;; Depends on types.sx.
|
||||
|
||||
;; ── security headers middleware ────────────────────────────────────
|
||||
(define dream-security-defaults {:x-frame-options "DENY" :referrer-policy "no-referrer" :x-content-type-options "nosniff" :hsts false})
|
||||
|
||||
(define
|
||||
dr/apply-security
|
||||
(fn
|
||||
(opts resp)
|
||||
(let
|
||||
((r1 (dream-add-header (dream-add-header (dream-add-header resp "x-content-type-options" (get opts :x-content-type-options)) "x-frame-options" (get opts :x-frame-options)) "referrer-policy" (get opts :referrer-policy))))
|
||||
(if
|
||||
(get opts :hsts)
|
||||
(dream-add-header
|
||||
r1
|
||||
"strict-transport-security"
|
||||
"max-age=31536000; includeSubDomains")
|
||||
r1))))
|
||||
|
||||
(define
|
||||
dream-security-headers-with
|
||||
(fn (opts) (fn (next) (fn (req) (dr/apply-security opts (next req))))))
|
||||
(define
|
||||
dream-security-headers
|
||||
(dream-security-headers-with dream-security-defaults))
|
||||
|
||||
;; ── cache-control response helpers ─────────────────────────────────
|
||||
(define
|
||||
dream-cache
|
||||
(fn
|
||||
(resp seconds)
|
||||
(dream-add-header resp "cache-control" (str "public, max-age=" seconds))))
|
||||
(define
|
||||
dream-private-cache
|
||||
(fn
|
||||
(resp seconds)
|
||||
(dream-add-header resp "cache-control" (str "private, max-age=" seconds))))
|
||||
(define
|
||||
dream-no-store
|
||||
(fn (resp) (dream-add-header resp "cache-control" "no-store")))
|
||||
(define
|
||||
dream-no-cache
|
||||
(fn
|
||||
(resp)
|
||||
(dream-add-header
|
||||
resp
|
||||
"cache-control"
|
||||
"no-cache, no-store, must-revalidate")))
|
||||
|
||||
;; cache-control middleware: stamp a max-age on every response
|
||||
(define
|
||||
dream-cache-for
|
||||
(fn (seconds) (fn (next) (fn (req) (dream-cache (next req) seconds)))))
|
||||
94
lib/dream/tests/headers.sx
Normal file
94
lib/dream/tests/headers.sx
Normal file
@@ -0,0 +1,94 @@
|
||||
;; lib/dream/tests/headers.sx — security headers + cache-control.
|
||||
|
||||
(define dream-hd-pass 0)
|
||||
(define dream-hd-fail 0)
|
||||
(define dream-hd-fails (list))
|
||||
|
||||
(define
|
||||
dream-hd-test
|
||||
(fn
|
||||
(name actual expected)
|
||||
(if
|
||||
(= actual expected)
|
||||
(set! dream-hd-pass (+ dream-hd-pass 1))
|
||||
(begin
|
||||
(set! dream-hd-fail (+ dream-hd-fail 1))
|
||||
(append! dream-hd-fails {:name name :actual actual :expected expected})))))
|
||||
|
||||
(define dream-hd-h (fn (req) (dream-text "body")))
|
||||
(define dream-hd-req (dream-request "GET" "/" {} ""))
|
||||
|
||||
;; ── security headers ───────────────────────────────────────────────
|
||||
(define dream-hd-sec ((dream-security-headers dream-hd-h) dream-hd-req))
|
||||
(dream-hd-test
|
||||
"nosniff"
|
||||
(dream-resp-header dream-hd-sec "x-content-type-options")
|
||||
"nosniff")
|
||||
(dream-hd-test
|
||||
"frame deny"
|
||||
(dream-resp-header dream-hd-sec "x-frame-options")
|
||||
"DENY")
|
||||
(dream-hd-test
|
||||
"referrer policy"
|
||||
(dream-resp-header dream-hd-sec "referrer-policy")
|
||||
"no-referrer")
|
||||
(dream-hd-test
|
||||
"no hsts by default"
|
||||
(dream-resp-header dream-hd-sec "strict-transport-security")
|
||||
nil)
|
||||
(dream-hd-test "body preserved" (dream-resp-body dream-hd-sec) "body")
|
||||
|
||||
(define
|
||||
dream-hd-hsts
|
||||
((dream-security-headers-with (assoc dream-security-defaults :hsts true))
|
||||
dream-hd-h))
|
||||
(dream-hd-test
|
||||
"hsts when enabled"
|
||||
(contains?
|
||||
(dream-resp-header
|
||||
(dream-hd-hsts dream-hd-req)
|
||||
"strict-transport-security")
|
||||
"max-age=31536000")
|
||||
true)
|
||||
|
||||
;; ── cache-control ──────────────────────────────────────────────────
|
||||
(dream-hd-test
|
||||
"cache public"
|
||||
(dream-resp-header
|
||||
(dream-cache (dream-text "x") 60)
|
||||
"cache-control")
|
||||
"public, max-age=60")
|
||||
(dream-hd-test
|
||||
"private cache"
|
||||
(dream-resp-header
|
||||
(dream-private-cache (dream-text "x") 30)
|
||||
"cache-control")
|
||||
"private, max-age=30")
|
||||
(dream-hd-test
|
||||
"no-store"
|
||||
(dream-resp-header (dream-no-store (dream-text "x")) "cache-control")
|
||||
"no-store")
|
||||
(dream-hd-test
|
||||
"no-cache"
|
||||
(dream-resp-header (dream-no-cache (dream-text "x")) "cache-control")
|
||||
"no-cache, no-store, must-revalidate")
|
||||
|
||||
;; ── cache middleware ───────────────────────────────────────────────
|
||||
(define dream-hd-capp ((dream-cache-for 300) dream-hd-h))
|
||||
(dream-hd-test
|
||||
"cache-for stamps"
|
||||
(dream-resp-header (dream-hd-capp dream-hd-req) "cache-control")
|
||||
"public, max-age=300")
|
||||
|
||||
;; ── composes around a router ───────────────────────────────────────
|
||||
(define
|
||||
dream-hd-app
|
||||
(dream-security-headers
|
||||
(dream-router
|
||||
(list (dream-get "/" (fn (req) (dream-html "<p>hi</p>")))))))
|
||||
(dream-hd-test
|
||||
"router security header"
|
||||
(dream-resp-header (dream-hd-app dream-hd-req) "x-frame-options")
|
||||
"DENY")
|
||||
|
||||
(define dream-hd-tests-run! (fn () {:total (+ dream-hd-pass dream-hd-fail) :passed dream-hd-pass :failed dream-hd-fail :fails dream-hd-fails}))
|
||||
@@ -141,6 +141,13 @@ with extensions + hardening below.
|
||||
`dream-attr`, `dream-escape-join`. Fixed a real **XSS hole** in the todo demo, which
|
||||
interpolated user text into `<li>` unescaped — now `(dream-escape (get it :text))`;
|
||||
regression test asserts `<script>` renders as `<script>`. 16 suites, 401/401.
|
||||
- **2026-06-07 — Ext: security headers + cache-control** (`lib/dream/headers.sx`, 12
|
||||
tests, 413 total). `dream-security-headers` middleware (X-Content-Type-Options
|
||||
nosniff, X-Frame-Options DENY, Referrer-Policy no-referrer; opt-in HSTS via
|
||||
`dream-security-headers-with`). Cache helpers `dream-cache`/`dream-private-cache`/
|
||||
`dream-no-store`/`dream-no-cache` + `dream-cache-for` middleware. **dream-on-sx is
|
||||
feature-complete: roadmap + 10 extensions, 413/413 across 17 suites. SATURATED —
|
||||
remaining work is host-on-sx's job to consume `dream-run` (don't edit hosts/).**
|
||||
|
||||
## Extensions (post-roadmap)
|
||||
|
||||
@@ -157,6 +164,7 @@ The five-types core is complete; these harden it toward a production HTTP front
|
||||
- [x] **`api.sx` facade + README** — `dream-make-app` / `dream-serve` + `README.md`.
|
||||
- [x] **Auth** — base64 (pure SX), HTTP Basic auth + Bearer-token middleware.
|
||||
- [x] **HTML escaping** (`dream-escape`/`dream-attr`) — fixed an XSS hole in the todo demo.
|
||||
- [x] **Security headers + cache-control** (`dream-security-headers`, `dream-cache`/`-no-store`).
|
||||
|
||||
## Stdlib additions Dream will need
|
||||
|
||||
|
||||
Reference in New Issue
Block a user