mirror of
https://github.com/cupcakearmy/cryptgeon.git
synced 2026-07-04 22:45:31 +00:00
feat: enable strict CSP with nonce-based script-src
Replaces the disabled CSP middleware with a working implementation: - Generates a per-request nonce for script-src - Injects nonce into the inline SvelteKit bootstrap script - Uses 'strict-dynamic' so dynamically imported modules are trusted - SPA fallback serves index.html with CSP header
This commit is contained in:
@@ -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,7 +12,7 @@ use tower::Layer;
|
||||
use tower_http::{
|
||||
compression::CompressionLayer,
|
||||
normalize_path::NormalizePathLayer,
|
||||
services::{ServeDir, ServeFile},
|
||||
services::ServeDir,
|
||||
};
|
||||
|
||||
#[macro_use]
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user