use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine as _};
use uuid::Uuid;
pub mod defaults {
pub const HSTS: &str = "max-age=63072000; includeSubDomains; preload";
pub const CSP: &str = "default-src 'none'; frame-ancestors 'none'";
pub const XCTO: &str = "nosniff";
pub const XFO: &str = "DENY";
pub const PERMISSIONS_POLICY: &str = "camera=(), microphone=(), geolocation=()";
pub const CACHE_CONTROL: &str = "no-store";
pub const COEP: &str = "require-corp";
pub const COOP: &str = "same-origin";
pub const CORP: &str = "same-origin";
pub const X_DNS_PREFETCH_CONTROL: &str = "off";
pub const X_PERMITTED_CROSS_DOMAIN_POLICIES: &str = "none";
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
#[must_use]
pub struct CspNonce(String);
impl CspNonce {
pub fn generate() -> Self {
Self(STANDARD_NO_PAD.encode(Uuid::new_v4().as_bytes()))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for CspNonce {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl From<&str> for CspNonce {
fn from(value: &str) -> Self {
Self(value.to_owned())
}
}
#[derive(Clone, Debug)]
#[allow(dead_code)] pub struct SecurityHeadersLayer {
csp: String,
hsts: String,
xcto: String,
xfo: String,
permissions_policy: String,
cache_control: String,
coep: String,
coop: String,
corp: String,
x_dns_prefetch_control: String,
x_permitted_cross_domain_policies: String,
pub(crate) include_csp_nonce: bool,
}
impl Default for SecurityHeadersLayer {
fn default() -> Self {
Self {
csp: defaults::CSP.to_owned(),
hsts: defaults::HSTS.to_owned(),
xcto: defaults::XCTO.to_owned(),
xfo: defaults::XFO.to_owned(),
permissions_policy: defaults::PERMISSIONS_POLICY.to_owned(),
cache_control: defaults::CACHE_CONTROL.to_owned(),
coep: defaults::COEP.to_owned(),
coop: defaults::COOP.to_owned(),
corp: defaults::CORP.to_owned(),
x_dns_prefetch_control: defaults::X_DNS_PREFETCH_CONTROL.to_owned(),
x_permitted_cross_domain_policies: defaults::X_PERMITTED_CROSS_DOMAIN_POLICIES
.to_owned(),
include_csp_nonce: false,
}
}
}
impl SecurityHeadersLayer {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_csp(mut self, csp: impl Into<String>) -> Self {
self.csp = csp.into();
self
}
#[must_use]
pub fn with_hsts(mut self, hsts: impl Into<String>) -> Self {
self.hsts = hsts.into();
self
}
#[must_use]
pub fn with_csp_nonce(mut self) -> Self {
self.include_csp_nonce = true;
self
}
#[must_use]
pub fn with_permissions_policy(mut self, permissions_policy: impl Into<String>) -> Self {
self.permissions_policy = permissions_policy.into();
self
}
#[cfg_attr(not(any(feature = "axum", feature = "actix-web")), allow(dead_code))]
pub(crate) fn csp_value(&self, nonce: Option<&CspNonce>) -> String {
match nonce {
Some(nonce) if self.csp.contains("{nonce}") => {
self.csp.replace("{nonce}", nonce.as_str())
}
Some(nonce) => format!(
"{}; script-src 'nonce-{}'; style-src 'nonce-{}'",
self.csp,
nonce.as_str(),
nonce.as_str()
),
None => self.csp.clone(),
}
}
}
#[cfg(any(feature = "axum", feature = "actix-web"))]
pub(crate) fn security_header_pairs(
layer: &SecurityHeadersLayer,
nonce: Option<&CspNonce>,
) -> Vec<(http::HeaderName, http::HeaderValue)> {
use http::header::{
HeaderName, HeaderValue, CACHE_CONTROL, STRICT_TRANSPORT_SECURITY, X_CONTENT_TYPE_OPTIONS,
X_FRAME_OPTIONS,
};
fn hv(s: &str) -> HeaderValue {
HeaderValue::from_str(s).expect("security header value is always valid ASCII")
}
vec![
(STRICT_TRANSPORT_SECURITY, hv(&layer.hsts)),
(
HeaderName::from_static("content-security-policy"),
hv(&layer.csp_value(nonce)),
),
(X_CONTENT_TYPE_OPTIONS, hv(&layer.xcto)),
(X_FRAME_OPTIONS, hv(&layer.xfo)),
(
HeaderName::from_static("permissions-policy"),
hv(&layer.permissions_policy),
),
(CACHE_CONTROL, hv(&layer.cache_control)),
(
HeaderName::from_static("cross-origin-embedder-policy"),
hv(&layer.coep),
),
(
HeaderName::from_static("cross-origin-opener-policy"),
hv(&layer.coop),
),
(
HeaderName::from_static("cross-origin-resource-policy"),
hv(&layer.corp),
),
(
HeaderName::from_static("x-dns-prefetch-control"),
hv(&layer.x_dns_prefetch_control),
),
(
HeaderName::from_static("x-permitted-cross-domain-policies"),
hv(&layer.x_permitted_cross_domain_policies),
),
]
}
#[cfg(feature = "axum")]
mod axum_impl {
use super::{security_header_pairs, CspNonce, SecurityHeadersLayer};
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use axum::{
body::Body,
http::{Request, Response},
};
use tower::{Layer, Service};
impl<S> Layer<S> for SecurityHeadersLayer {
type Service = SecurityHeadersService<S>;
fn layer(&self, inner: S) -> Self::Service {
SecurityHeadersService {
inner,
layer: self.clone(),
}
}
}
#[derive(Clone, Debug)]
pub struct SecurityHeadersService<S> {
inner: S,
layer: SecurityHeadersLayer,
}
impl<S> Service<Request<Body>> for SecurityHeadersService<S>
where
S: Service<Request<Body>, Response = Response<Body>> + Clone + Send + 'static,
S::Future: Send + 'static,
S::Error: Send + 'static,
{
type Response = Response<Body>;
type Error = S::Error;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, mut req: Request<Body>) -> Self::Future {
let layer = self.layer.clone();
let nonce = if layer.include_csp_nonce {
let nonce = CspNonce::generate();
req.extensions_mut().insert(nonce.clone());
Some(nonce)
} else {
None
};
let fut = self.inner.call(req);
Box::pin(async move {
let mut resp = fut.await?;
let headers = resp.headers_mut();
for (name, value) in security_header_pairs(&layer, nonce.as_ref()) {
headers.insert(name, value);
}
Ok(resp)
})
}
}
}
#[cfg(feature = "axum")]
pub use axum_impl::SecurityHeadersService;