Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 28 additions & 9 deletions packages/backend/src/csp.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,35 @@
use axum::{body::Body, extract::Request, http::HeaderValue, middleware::Next, response::Response};
use axum::{
http::HeaderValue,
response::{Html, IntoResponse, Response},
};
use ring::rand::SecureRandom;
use std::sync::OnceLock;

const CUSTOM_HEADER_NAME: &str = "Content-Security-Policy";
const CUSTOM_HEADER_VALUE: &str = "default-src 'self'; script-src 'report-sample' 'self'; style-src 'report-sample' 'self'; object-src 'none'; base-uri 'self'; connect-src 'self' data:; font-src 'self'; frame-src 'self'; img-src 'self'; manifest-src 'self'; media-src 'self'; worker-src 'none';";
const CSP_POLICY: &str = "default-src 'self'; script-src 'nonce-{nonce}' 'strict-dynamic'; style-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'self'; connect-src 'self'";

lazy_static! {
static ref HEADER_VALUE: HeaderValue = HeaderValue::from_static(CUSTOM_HEADER_VALUE);
fn index_html() -> &'static str {
static HTML: OnceLock<String> = OnceLock::new();
HTML.get_or_init(|| {
let path = format!("{}index.html", *crate::config::FRONTEND_PATH);
std::fs::read_to_string(&path).expect("Failed to read index.html for CSP injection")
})
}

pub async fn add_csp_header(request: Request<Body>, next: Next) -> Response {
let mut response = next.run(request).await;
fn generate_nonce() -> String {
let rng = ring::rand::SystemRandom::new();
let mut bytes = [0u8; 32];
rng.fill(&mut bytes).expect("Failed to generate CSP nonce");
bs62::encode_data(&bytes)
}

pub async fn spa_fallback() -> Response {
let nonce = generate_nonce();
let csp = CSP_POLICY.replace("{nonce}", &nonce);
let html = index_html().replace("<script>", &format!("<script nonce=\"{}\">", nonce));

let mut response = Html(html).into_response();
response
.headers_mut()
.append(CUSTOM_HEADER_NAME, HEADER_VALUE.clone());
.insert("Content-Security-Policy", HeaderValue::from_str(&csp).unwrap());
response
}
}
12 changes: 5 additions & 7 deletions packages/backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use tower::Layer;
use tower_http::{
compression::CompressionLayer,
normalize_path::NormalizePathLayer,
services::{ServeDir, ServeFile},
services::ServeDir,
};

#[macro_use]
Expand Down Expand Up @@ -50,14 +50,12 @@ async fn main() {
.merge(health_routes)
.merge(status_routes);

let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html");
let serve_dir =
ServeDir::new(config::FRONTEND_PATH.to_string()).not_found_service(ServeFile::new(index));
let app = Router::new()
.nest("/api", api_routes)
.fallback_service(serve_dir)
// Disabled for now, as svelte inlines scripts
// .layer(middleware::from_fn(csp::add_csp_header))
.fallback_service(
ServeDir::new(config::FRONTEND_PATH.to_string())
.not_found_service(axum::Router::new().fallback(csp::spa_fallback)),
)
.layer(DefaultBodyLimit::max(*config::LIMIT))
.layer(
CompressionLayer::new()
Expand Down
Loading