hennery_kernel/csp.rs
1//! Kernel spec §7.2: every HTML response carries the
2//! `Content-Security-Policy`. One layer over the whole router sets it, so a
3//! page cannot be added without it.
4
5use axum::http::{HeaderValue, header};
6use axum::response::Response;
7
8/// Kernel spec §7.2's policy. No page has an inline script, so it names no
9/// hash: the web UI's theme bootstrap is a file of its own (`/theme.js`).
10/// Everything else comes from this origin: scripts, styles, fonts, API
11/// calls and form targets (plan 4b).
12pub const POLICY: &str = "script-src 'self'; worker-src 'self'; manifest-src 'self'; default-src 'self'; connect-src 'self'; img-src 'self' data: blob:; \
13 object-src 'none'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'";
14
15/// The policy of a hat's logo (plan 4d-B2; the security review's A9): it
16/// is served from this origin, so opened as a page it must still run
17/// nothing, load nothing and be framed nowhere, whatever it holds.
18pub const LOGO_POLICY: &str = "default-src 'none'; sandbox; frame-ancestors 'none'";
19
20/// The worker can execute and fetch only from its own origin.
21pub const WORKER_POLICY: &str = "default-src 'none'; script-src 'self'; connect-src 'self'; img-src 'self'";
22
23/// Set `POLICY` on a `text/html` response. Use as
24/// `axum::middleware::map_response(csp::on_html)`, outermost.
25pub async fn on_html(mut response: Response) -> Response {
26 let is_html = response
27 .headers()
28 .get(header::CONTENT_TYPE)
29 .and_then(|t| t.to_str().ok())
30 .is_some_and(|t| t.trim_start().to_ascii_lowercase().starts_with("text/html"));
31 if is_html {
32 response
33 .headers_mut()
34 .insert(header::CONTENT_SECURITY_POLICY, HeaderValue::from_static(POLICY));
35 }
36 response
37}