use std::sync::Arc; use api_types::{ ADMIN_SETTINGS, ADMIN_SHARES, ADMIN_USERS, AUTH_APP_PASSWORDS, AUTH_LOGIN, AUTH_LOGOUT, AUTH_ME, AUTH_MODE, AUTH_PASSKEY_LOGIN, AUTH_PASSKEYS, AUTH_PASSKEYS_REGISTER, AUTH_PASSWORD, AUTH_SETUP, DAV, DAV_SHARE, FILES, FINISH_SUFFIX, PIM, SEARCH, SHARE, SHARE_UNLOCK_SUFFIX, SHARES, WELL_KNOWN_CALDAV, WELL_KNOWN_CARDDAV, }; use axum::Router; use axum::http::HeaderValue; use axum::routing::{any, delete, get, post, put}; use tower_http::set_header::SetResponseHeaderLayer; use crate::error::AppState; /// Content-Security-Policy tuned to the built Trunk frontend. /// /// * `script-src 'unsafe-inline'` — Trunk emits one inline bootstrap module /// in index.html; every real JS file also carries an SRI integrity hash. /// * `'wasm-unsafe-eval'` — compiling the same-origin WASM module. /// * `style-src 'unsafe-inline'` — the context menu sets an inline `style=`. /// * Everything else locked to the same origin; frames/plugins banned. const CSP: &str = "default-src 'self'; script-src 'self' 'unsafe-inline' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; media-src 'self' blob:; connect-src 'self'; font-src 'self' data:; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';"; /// Content-Security-Policy for served *user files* that the browser would /// treat as a scripting document (HTML, SVG, XML). Lets such a file render as /// a real page — the "share an HTML page" flow — without letting it act as the /// app. /// /// The security hinges on one omission: `allow-scripts` **without** /// `allow-same-origin`. That forces the document into a unique opaque origin, /// so its JavaScript cannot read the session cookie's origin, cannot touch /// `localStorage`, and cannot call `/api` as the viewer (the server sends no /// CORS headers, so every cross-origin read fails). Never add /// `allow-same-origin` here. /// /// Also deliberately absent: /// * `allow-top-navigation` — a shared page cannot silently redirect the /// viewer elsewhere. `-by-user-activation` still lets links work on click. /// * `allow-popups-to-escape-sandbox` — a popup would drop the sandbox. /// /// `connect-src *` is deliberate: a shared page may call third-party APIs. /// The trade-off is that it can also beacon (report that the link was opened, /// and anything the page itself contains). It cannot exfiltrate anything of /// the viewer's — the opaque origin means it has no session and no CORS read /// access to this server. pub(crate) const FILE_CSP: &str = "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; media-src 'self' blob:; font-src 'self' data:; connect-src *; object-src 'none'; frame-ancestors 'none'; sandbox allow-scripts allow-forms allow-modals allow-downloads allow-popups allow-top-navigation-by-user-activation;"; /// Apply [`FILE_CSP`] to a response that declares a scriptable type. /// /// The router's CSP layer is `if_not_present`, so without this such a response /// keeps the *app* policy: same origin, scripts allowed. A `GET` of a shared /// HTML file is a top-level navigation, which carries the session cookie under /// `SameSite=Lax`, so the page would then run as the viewer against `/api`. pub(crate) fn sandbox_scriptable(resp: &mut axum::http::Response) { let scriptable = resp .headers() .get(axum::http::header::CONTENT_TYPE) .and_then(|v| v.to_str().ok()) .is_some_and(is_scriptable_mime); if scriptable { resp.headers_mut().insert( "content-security-policy", HeaderValue::from_static(FILE_CSP), ); } } /// Content-Security-Policy for inline (preview) files that are *not* /// scripting documents (PDF, media, …): the preview modal embeds them in a /// same-origin `