Web security measures that are neither authentication nor authorisation (SPEC §15): response headers added when absent, the host's Content Security Policy with a per-request nonce, opt-in trust of a reverse proxy's headers, and CSRF protection through ring-anti-forgery's synchroniser token.
Written down so it is not rediscovered: htmx's hx-on, hx-vals js: and
trigger filters need unsafe-eval or the hx-csp extension; a strict
policy means doing without them, which the demo does.
Web security measures that are neither authentication nor authorisation (SPEC §15): response headers added when absent, the host's Content Security Policy with a per-request nonce, opt-in trust of a reverse proxy's headers, and CSRF protection through ring-anti-forgery's synchroniser token. Written down so it is not rediscovered: htmx's `hx-on`, `hx-vals js:` and trigger filters need `unsafe-eval` or the `hx-csp` extension; a strict policy means doing without them, which the demo does.
(body-failure e)The request body's own failure somewhere in e's cause chain, as data — {:status 413} past its limit, {:status s :lost class-name} when the client did not send it —
or nil. Marked where the body is read, so a failure of anything else that merely looks
like Jetty's (an upstream client, a driver) is never mistaken for the client's.
The request body's own failure somewhere in `e`'s cause chain, as data — `{:status
413}` past its limit, `{:status s :lost class-name}` when the client did not send it —
or nil. Marked where the body is read, so a failure of anything else that merely looks
like Jetty's (an upstream client, a driver) is never mistaken for the client's.(csrf-field request)The hidden input a classic form needs; htmx requests carry the header
instead. Nothing when the request carries no token — under :csrf false
there is nothing to send, and an empty field would only earn a 403.
The hidden input a classic form needs; htmx requests carry the header instead. Nothing when the request carries no token — under `:csrf false` there is nothing to send, and an empty field would only earn a 403.
(csrf-token request)The request's token, for the host's own markup — and the read that makes it stick:
a token nobody read through here is not written into the session (wrap-csrf).
The request's token, for the host's own markup — and the read that makes it stick: a token nobody read through here is not written into the session (`wrap-csrf`).
The largest request body the base reads when the host names no other: 200 000 bytes,
Jetty's own form limit — which never applied here, because Ring reads the body itself
(wrap-params), so a single anonymous POST could fill the heap (measured: 1 GB raised
it by 2 GB; four at once, an OutOfMemoryError).
The largest request body the base reads when the host names no other: 200 000 bytes, Jetty's own form limit — which never applied here, because Ring reads the body itself (`wrap-params`), so a single anonymous POST could fill the heap (measured: 1 GB raised it by 2 GB; four at once, an OutOfMemoryError).
(private response)response with Cache-Control: no-store unless it already says how it may be cached.
For what is somebody's own — a signed-in page, a page carrying a session's CSRF token —
which a shared cache must never hand to the next visitor, nor a shared computer keep.
`response` with `Cache-Control: no-store` unless it already says how it may be cached. For what is somebody's own — a signed-in page, a page carrying a session's CSRF token — which a shared cache must never hand to the next visitor, nor a shared computer keep.
(rotate-token request)request carrying a fresh CSRF token for a response that rotates the session, and
the token recorded so the rotated session keeps exactly it. The render step calls it
before turning such a response into HTML, so a login page's forms carry the token its
new session holds. The request unchanged when it went through no CSRF, or carries no
token. A token read before this — a handler whose own content called csrf-field —
cannot follow the rotation, and is logged by name.
`request` carrying a fresh CSRF token for a response that rotates the session, and the token recorded so the rotated session keeps exactly it. The render step calls it before turning such a response into HTML, so a login page's forms carry the token its new session holds. The request unchanged when it went through no CSRF, or carries no token. A token read before this — a handler whose own content called `csrf-field` — cannot follow the rotation, and is logged by name.
(wrap-body-limit handler limit-for render)Refuses a request body larger than (limit-for request) bytes with a 413: at once,
unread, when its declared length says so; otherwise as it is read, whoever reads it.
render answers the refusal made here, before anything else runs.
Refuses a request body larger than `(limit-for request)` bytes with a 413: at once, unread, when its declared length says so; otherwise as it is read, whoever reads it. `render` answers the refusal made here, before anything else runs.
(wrap-csrf handler render-error)ring-anti-forgery inside the session: every request not GET/HEAD/OPTIONS
needs the session's token, read from the __anti-forgery-token form field
or the X-CSRF-Token header — which the shell makes htmx send on every
request (the library also honours X-XSRF-Token; the base documents one
name). A refusal is the base's own 403 datum, so an htmx swap receives a
fragment and a navigation a page.
A token reaches the session only if the request used it — through
csrf-token or csrf-field, which the shell calls for every page it renders,
and while the handler runs. A request that renders neither writes no session, so
an anonymous /health, a JSON answer or a redirect leaves no row behind. Reading
:anti-forgery-token or ring-anti-forgery's dynamic var directly, or reading the
token after the handler returned (a body built lazily later), mints a token that is
never stored: the form built with it earns a 403.
ring-anti-forgery inside the session: every request not GET/HEAD/OPTIONS needs the session's token, read from the `__anti-forgery-token` form field or the `X-CSRF-Token` header — which the shell makes htmx send on every request (the library also honours `X-XSRF-Token`; the base documents one name). A refusal is the base's own 403 datum, so an htmx swap receives a fragment and a navigation a page. **A token reaches the session only if the request used it** — through `csrf-token` or `csrf-field`, which the shell calls for every page it renders, and while the handler runs. A request that renders neither writes no session, so an anonymous `/health`, a JSON answer or a redirect leaves no row behind. Reading `:anti-forgery-token` or ring-anti-forgery's dynamic var directly, or reading the token after the handler returned (a body built lazily later), mints a token that is never stored: the form built with it earns a 403.
(wrap-headers handler {:keys [csp] :as config})Outer middleware: every response — routed, static, error — gets the
security headers it lacks; a header the handler set is kept. Every request
gets :wb/nonce; when the host configures :csp, {nonce} in it is
replaced by that request's nonce and the policy is sent. :frame-options
is DENY by default, a string to change it, false to omit it.
Outer middleware: every response — routed, static, error — gets the
security headers it lacks; a header the handler set is kept. Every request
gets `:wb/nonce`; when the host configures `:csp`, `{nonce}` in it is
replaced by that request's nonce and the policy is sent. `:frame-options`
is `DENY` by default, a string to change it, `false` to omit it.(wrap-proxy handler hops)Trusts X-Forwarded-For and X-Forwarded-Proto for :remote-addr and :scheme,
behind hops proxies the host runs, each APPENDING what it saw — nginx's
$proxy_add_x_forwarded_for, AWS's load balancers, Heroku's and Fly's routers all do.
The entry hops from the right is the address the outermost of them saw; everything
to its left the client could have written. A header with fewer entries than hops
leaves the socket's address, and a request that did not come through the proxies is
never trusted beyond it. X-Forwarded-Proto is taken from its last entry whatever the
count: proxies overwrite it rather than append, so it holds one value, the nearest
proxy's. Only behind proxies the host controls: anyone else can send these headers.
Trusts `X-Forwarded-For` and `X-Forwarded-Proto` for `:remote-addr` and `:scheme`, behind `hops` proxies the host runs, each APPENDING what it saw — nginx's `$proxy_add_x_forwarded_for`, AWS's load balancers, Heroku's and Fly's routers all do. The entry `hops` from the right is the address the outermost of them saw; everything to its left the client could have written. A header with fewer entries than `hops` leaves the socket's address, and a request that did not come through the proxies is never trusted beyond it. `X-Forwarded-Proto` is taken from its last entry whatever the count: proxies overwrite it rather than append, so it holds one value, the nearest proxy's. Only behind proxies the host controls: anyone else can send these headers.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |