dream: security headers + cache-control middleware + 12 tests
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:
2026-06-07 15:20:55 +00:00
parent 0366373c8a
commit bd1e78c40f
4 changed files with 158 additions and 0 deletions

View File

@@ -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
View 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)))))

View 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}))

View File

@@ -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 `&lt;script&gt;`. 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