mirror of
https://github.com/narrrl/narl-workspace.git
synced 2026-11-03 16:41:27 +01:00
feat: send security headers on every response
Nothing set a CSP, a referrer policy, nosniff or a framing rule, so the app relied entirely on the same-origin policy. The referrer policy is the one with a concrete leak behind it: a share token is part of the URL path, so following any outbound link from a shared entry handed a third party a working link to a private entry. `no-referrer` closes that. The layer is `if_not_present`, so the media routes keep the stricter sandbox CSP they set for themselves.
This commit is contained in:
1 parent
00c22aa195
commit
8da0a30497
2 files changed
+33
-2
No files matched your search
+1
-1
@@ -9,7 +9,7 @@ axum = { version = "0.8", features = ["multipart", "macros"] }
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal", "fs"] }
|
||||
tower = "0.5"
|
||||
tokio-util = { version = "0.7", features = ["io"] }
|
||||
tower-http = { version = "0.6", features = ["trace", "compression-gzip", "limit"] }
|
||||
tower-http = { version = "0.6", features = ["trace", "compression-gzip", "limit", "set-header"] }
|
||||
sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio", "sqlite", "migrate", "macros"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
+32
-1
@@ -10,7 +10,10 @@ use std::sync::Arc;
|
||||
use anyhow::{Context, Result};
|
||||
use axum::Router;
|
||||
use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions};
|
||||
use tower_http::{compression::CompressionLayer, trace::TraceLayer};
|
||||
use axum::http::{header, HeaderValue};
|
||||
use tower_http::{
|
||||
compression::CompressionLayer, set_header::SetResponseHeaderLayer, trace::TraceLayer,
|
||||
};
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||
|
||||
use crate::{config::Config, state::AppState};
|
||||
@@ -66,6 +69,7 @@ async fn main() -> Result<()> {
|
||||
.fallback(static_files::handler)
|
||||
.layer(axum::extract::DefaultBodyLimit::max(max_upload + 1024 * 1024))
|
||||
.layer(CompressionLayer::new())
|
||||
.layer(security_headers())
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.with_state(state);
|
||||
|
||||
@@ -81,6 +85,33 @@ async fn main() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Content-Security-Policy for the app shell. `unsafe-inline` is granted to
|
||||
/// styles only, because CodeMirror injects its theme as a <style> element at
|
||||
/// runtime; scripts stay confined to the bundled files.
|
||||
const CSP: &str = "default-src 'self'; img-src 'self' data:; media-src 'self'; \
|
||||
style-src 'self' 'unsafe-inline'; script-src 'self'; connect-src 'self'; \
|
||||
object-src 'none'; base-uri 'none'; form-action 'self'; frame-ancestors 'none'";
|
||||
|
||||
type HeaderLayer = SetResponseHeaderLayer<HeaderValue>;
|
||||
|
||||
/// Headers every response carries. `if_not_present` throughout, so a handler
|
||||
/// that has already said something stricter about itself — the media routes and
|
||||
/// their sandbox CSP — keeps its own answer.
|
||||
fn security_headers() -> (HeaderLayer, HeaderLayer, HeaderLayer, HeaderLayer) {
|
||||
let header = |name: header::HeaderName, value: &'static str| {
|
||||
SetResponseHeaderLayer::if_not_present(name, HeaderValue::from_static(value))
|
||||
};
|
||||
|
||||
(
|
||||
// A share token lives in the URL path, so an outbound link from a shared
|
||||
// entry would otherwise hand the whole private link to a third party.
|
||||
header(header::REFERRER_POLICY, "no-referrer"),
|
||||
header(header::X_CONTENT_TYPE_OPTIONS, "nosniff"),
|
||||
header(header::X_FRAME_OPTIONS, "DENY"),
|
||||
header(header::CONTENT_SECURITY_POLICY, CSP),
|
||||
)
|
||||
}
|
||||
|
||||
async fn shutdown_signal() {
|
||||
let _ = tokio::signal::ctrl_c().await;
|
||||
tracing::info!("shutting down");
|
||||
|
||||
Reference in new issue
Block a user