asobi_console_csp (asobi v0.72.5)
View SourceThe console's Content-Security-Policy, and the nonce it is built around.
One emitter. The policy is assembled where the response is, because two of
its directives are not knowable at build time: the nonce is per response, and
connect-src depends on which origin this deployment's ops API answers on.
The shell policy, directive by directive, with the reason each one is there:
| Directive | Value | Why |
|---|---|---|
default-src | 'none' | Nothing falls back to a permissive default. Every fetch the page makes is listed below or refused. |
script-src | 'nonce-N' | Only the one script tag the shell emits runs. No 'self', so an injected <script src> pointing at our own origin is refused too. No 'unsafe-inline', no 'unsafe-eval'. |
style-src | 'self' | One content-hashed stylesheet. Inline style= attributes fall back to this directive and are therefore blocked - which is enforcement of a rule the console follows, not an accident. |
img-src | 'self' data: | The bundler inlines small icons as data: URIs. |
font-src | 'self' | No font CDN. See asobi_console_shell for what that costs. |
connect-src | 'self' + configured base | The ops API. Same-origin self-hosted; an explicit origin when the API is fronted elsewhere. |
base-uri | 'none' | An injected <base> would repoint every relative script and style URL on the page. |
form-action | 'none' | The console never navigates a form; it fetches. If script fails, the login form fails closed instead of posting a secret somewhere. |
frame-ancestors | 'none' | Clickjacking, and the modern half of the X-Frame-Options the security-headers plugin already sends. |
frame-src, object-src, worker-src | 'none' | Redundant under default-src 'none', and stated anyway: plugin and worker content are the classic fallback bypasses and cost nothing to name. |
script-src carrying a nonce and no host source is what forces the bundle
to be a single chunk. A nonce does not propagate to a module's static
imports, so a code-split build would load its second chunk and be refused.
console/vite.config.js pins inlineDynamicImports for that reason, and
asobi_console_shell emits exactly one script tag.
Assets get their own, much shorter policy: they are never a document.
Summary
Functions
The ops API base the shell advertises, or none for same-origin.
The policy served with a script, stylesheet or font.
A fresh nonce, base64.
The shell document's policy, built around Nonce.
Functions
-spec api_base() -> {ok, binary()} | none.
The ops API base the shell advertises, or none for same-origin.
console_api_base is configuration because one console binary serves both a
self-hosted node, where the API is same-origin, and a managed environment,
where the browser authenticates to the control plane and then calls the
environment's own ingress. Anything that is not an absolute http/https
origin is dropped rather than guessed at, so a typo degrades to same-origin
instead of widening connect-src to a string nobody checked.
-spec asset() -> binary().
The policy served with a script, stylesheet or font.
An asset is a subresource. Reached as a top-level document - which is how a stored-XSS-in-a-static-file gets its origin - it can load nothing and frame nothing.
-spec nonce() -> binary().
A fresh nonce, base64.
Per response, always. The shell is served no-store for exactly this reason:
a cached document would reuse its nonce, and a reused nonce is no nonce.
The shell document's policy, built around Nonce.