asobi_console_csp (asobi v0.72.6)

View Source

The 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:

DirectiveValueWhy
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 baseThe 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

api_base()

-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.

asset()

-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.

nonce()

-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.

shell(Nonce)

-spec shell(binary()) -> binary().

The shell document's policy, built around Nonce.