You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
server: let an operator frame the admin console, and nothing else
Second of the features split out of #30. FRAME_ANCESTORS is a space-separated
list of origins allowed to embed /admin/ in a frame; when set, the admin routes
send `Content-Security-Policy: frame-ancestors <list>`. Unset, nothing is sent
and /admin/ behaves exactly as it does today.
Three deliberate refusals, each of which is the interesting half:
⛔ Scoped to the admin SPA, and it must stay that way. The public booking pages
set `frame-ancestors 'none'` plus `X-Frame-Options: DENY` in their own handlers
and this never reaches them. They are unauthenticated pages that collect names,
emails and card details, and clickjacking one is worth more to an attacker than
framing a console nobody can open without a session.
⛔ An entry that is not `https://host[:port]` or `'self'` stops the process
booting rather than being dropped. A browser discards a source list it cannot
parse, so a typo would leave the admin UI MORE embeddable than leaving the
setting unset — the one failure mode a validation error must not have.
⛔ No X-Frame-Options is sent beside the CSP. That header has no allow-list form
(its ALLOW-FROM was implemented by one browser and is dead), so the only value
it could carry is SAMEORIGIN, which every browser that reads it applies INSTEAD
of honouring frame-ancestors — breaking the embedding this exists to enable.
With the list empty the wrapped handler sends no frame header at all rather than
a default deny, because adding one would be a behaviour change smuggled in on an
opt-in setting. TestAdminSPA_sendsNoFrameHeadersWhenUnset pins that.
Mutation-checked rather than assumed: with the Set call replaced by a discard,
TestAdminSPA_frameAncestorsWhenConfigured and ...OnSPAFallback both fail.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|`COOKIE_SECURE`| no | https→true | Override cookie Secure flag; defaults from `BASE_URL` scheme. |
38
38
|`TRUSTED_PROXY_CIDRS`| no | — | Comma-separated CIDRs (a bare address = one host) whose `X-Forwarded-For` is believed when keying per-IP rate limits, e.g. `10.0.0.0/8`. Include a fronting CDN's own ranges so the walk steps over its edge and lands on the visitor. Unset ⇒ the header is ignored and the limit keys on the TCP peer, so behind a CDN every visitor shares one bucket. **Only list networks you control**: anything in the list can name any client IP it likes. Single-value vendor headers (`CF-Connecting-IP`, `X-Real-IP`) are never read, from any peer. |
39
+
|`FRAME_ANCESTORS`| no | — |**Space**-separated origins allowed to embed the **admin UI** in a frame, e.g. `https://console.example.com 'self'`. Each entry must be `https://host[:port]` or `'self'` — anything else and **the app refuses to start**, because browsers drop a policy they cannot parse. Does not affect the public booking pages, which always deny framing. |
39
40
|`LOG_LEVEL`| no |`info`|`debug`/`info`/`warn`/`error`. |
40
41
41
42
¹ Email is optional to boot, but bookings won't send confirmations until SMTP is configured (env **or** the admin UI). Precedence is **env var > DB setting > default**.
0 commit comments