Skip to main content

axum_security/headers/
mod.rs

1//! Security headers middleware.
2//!
3//! This module provides Tower layers that add security-related HTTP response headers.
4//! Headers can be applied individually as layers or combined with [`SecurityHeaders`].
5//!
6//! # Available headers
7//!
8//! | Type | Header |
9//! |------|--------|
10//! | [`ContentSecurityPolicy`] | `Content-Security-Policy` |
11//! | [`StrictTransportSecurity`] | `Strict-Transport-Security` |
12//! | [`CrossOriginEmbedderPolicy`] | `Cross-Origin-Embedder-Policy` |
13//! | [`CrossOriginOpenerPolicy`] | `Cross-Origin-Opener-Policy` |
14//! | [`CrossOriginResourcePolicy`] | `Cross-Origin-Resource-Policy` |
15//! | [`OriginAgentCluster`] | `Origin-Agent-Cluster` |
16//! | [`ReferrerPolicy`] | `Referrer-Policy` |
17//! | [`ContentTypeOptions`] | `X-Content-Type-Options` |
18//! | [`DnsPrefetchControl`] | `X-DNS-Prefetch-Control` |
19//! | [`FrameOptions`] | `X-Frame-Options` |
20//! | [`XssProtection`] | `X-XSS-Protection` |
21//!
22//! Each header type implements [`Layer`](tower::Layer), so it can be applied directly
23//! to a router. It also implements [`IntoSecurityHeader`], so it can be added to a
24//! [`SecurityHeaders`] bundle.
25//!
26//! # Examples
27//!
28//! Apply a single header as a layer:
29//!
30//! ```rust
31//! use axum::Router;
32//! use axum_security::headers::CrossOriginOpenerPolicy;
33//!
34//! let app = Router::<()>::new()
35//!     .layer(CrossOriginOpenerPolicy::SAME_ORIGIN);
36//! ```
37//!
38//! Bundle multiple headers with [`SecurityHeaders`]:
39//!
40//! ```rust
41//! use axum::Router;
42//! use axum_security::headers::SecurityHeaders;
43//!
44//! let app = Router::<()>::new()
45//!     .layer(SecurityHeaders::recommended());
46//! ```
47//!
48//! Build a Content-Security-Policy:
49//!
50//! ```rust
51//! use axum::Router;
52//! use axum_security::headers::{ContentSecurityPolicy, CspSource};
53//!
54//! let csp = ContentSecurityPolicy::builder()
55//!     .default_src(CspSource::SELF)
56//!     .script_src([CspSource::SELF, CspSource::host("https://cdn.example.com")])
57//!     .upgrade_insecure_requests()
58//!     .build();
59//!
60//! let app = Router::<()>::new().layer(csp);
61//! ```
62
63mod csp;
64mod hsts;
65mod service;
66
67use std::{borrow::Borrow, collections::HashSet, hash::Hash, sync::Arc};
68
69pub use csp::{ContentSecurityPolicy, CspBuilder, CspSource};
70pub use hsts::{StrictTransportSecurity, StrictTransportSecurityBuilderError};
71use http::{HeaderName, HeaderValue};
72
73#[macro_export]
74macro_rules! define_header {
75    (
76        $struct_name:ident($header_name:ident = $header_value:literal),
77        $($const_name:ident => $value:literal),+ $(,)?
78    ) => {
79        const $header_name: ::http::HeaderName = ::http::HeaderName::from_static($header_value);
80
81
82        #[derive(Clone)]
83        pub struct $struct_name {
84            header_value: ::http::HeaderValue,
85        }
86
87        impl $struct_name {
88            $(
89                pub const $const_name: $struct_name = Self {
90                    header_value: ::http::HeaderValue::from_static($value),
91                };
92            )+
93        }
94
95        impl<S> tower::Layer<S> for $struct_name {
96            type Service = $crate::utils::headers::InsertHeadersService<S>;
97
98            fn layer(&self, inner: S) -> Self::Service {
99                $crate::utils::headers::InsertHeadersService {
100                    header_name: $header_name,
101                    header_value: self.header_value.clone(),
102                    inner,
103                }
104            }
105        }
106
107        impl IntoSecurityHeader for $struct_name {
108            fn into_header(self) -> (HeaderName, HeaderValue) {
109                ($header_name, self.header_value)
110            }
111        }
112    };
113}
114
115define_header!(
116    CrossOriginEmbedderPolicy(CROSS_ORIGIN_EMBEDDER_POLICY = "cross-origin-embedder-policy"),
117    UNSAFE_NONE => "unsafe-none",
118    REQUIRE_CORP => "require-corp",
119    CREDENTIALLESS => "credentialless",
120);
121
122define_header!(
123    CrossOriginOpenerPolicy(CROSS_ORIGIN_OPENER_POLICY = "cross-origin-opener-policy"),
124    UNSAFE_NONE => "unsafe-none",
125    SAME_ORIGIN_ALLOW_POPUPS => "same-origin-allow-popups",
126    SAME_ORIGIN => "same-origin",
127    NOOPENER_ALLOW_POPUPS => "noopener-allow-popups"
128);
129
130define_header!(
131    CrossOriginResourcePolicy(CROSS_ORIGIN_RESOURCE_POLICY = "cross-origin-resource-policy"),
132    SAME_SITE => "same-site",
133    SAME_ORIGIN => "same-origin",
134    CROSS_ORIGIN => "cross-origin",
135);
136
137define_header!(
138    OriginAgentCluster(ORIGIN_AGENT_CLUSTER = "origin-agent-cluster"),
139    ON => "?1",
140    OFF => "?0",
141);
142
143define_header!(
144    ReferrerPolicy(REFERRER_POLICY = "referrer-policy"),
145    NO_REFERRER => "no-referrer",
146    NO_REFERRER_WHEN_DOWNGRADE => "no-referrer-when-downgrade",
147    ORIGIN => "origin",
148    ORIGIN_WHEN_CROSS_ORIGIN => "origin-when-cross-origin",
149    SAME_ORIGIN => "same-origin",
150    STRICT_ORIGIN => "strict-origin",
151    STRICT_ORIGIN_WHEN_CROSS_ORIGIN => "strict-origin-when-cross-origin",
152    UNSAFE_URL => "unsafe-url",
153);
154
155define_header!(
156    ContentTypeOptions(X_CONTENT_TYPE_OPTIONS = "x-content-type-options"),
157    NO_SNIFF => "nosniff"
158);
159
160define_header!(
161    DnsPrefetchControl(X_DNS_PREFETCH_CONTROL = "x-dns-prefetch-control"),
162    ON => "on",
163    OFF => "off",
164);
165
166define_header!(
167    FrameOptions(X_FRAME_OPTIONS= "x-frame-options"),
168    DENY => "DENY",
169    SAMEORIGIN => "SAMEORIGIN",
170);
171
172define_header!(
173    XssProtection(X_XSS_PROTECTION= "x-xss-protection"),
174    ZERO => "0",
175);
176
177#[derive(Clone, Eq)]
178struct SecurityHeader {
179    name: HeaderName,
180    value: HeaderValue,
181}
182
183/// Converts a header type into a `(HeaderName, HeaderValue)` pair.
184///
185/// All header types in this module implement this trait, allowing them to be
186/// passed to [`SecurityHeaders::add`] and [`SecurityHeaders::try_add`].
187pub trait IntoSecurityHeader {
188    fn into_header(self) -> (HeaderName, HeaderValue);
189}
190
191impl Hash for SecurityHeader {
192    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
193        self.name.hash(state);
194    }
195}
196
197impl PartialEq for SecurityHeader {
198    fn eq(&self, other: &Self) -> bool {
199        self.name == other.name
200    }
201}
202
203impl Borrow<HeaderName> for SecurityHeader {
204    fn borrow(&self) -> &HeaderName {
205        &self.name
206    }
207}
208
209impl From<(HeaderName, HeaderValue)> for SecurityHeader {
210    fn from(value: (HeaderName, HeaderValue)) -> Self {
211        Self {
212            name: value.0,
213            value: value.1,
214        }
215    }
216}
217
218/// A bundle of security headers applied as a single Tower [`Layer`](tower::Layer).
219///
220/// Use [`SecurityHeaders::recommended`] for a sensible default set, or build a
221/// custom set with [`SecurityHeaders::new`] and [`add`](SecurityHeaders::add).
222///
223/// Call [`use_dev_headers(true)`](SecurityHeaders::use_dev_headers) in development
224/// to skip all headers (e.g. HSTS) that interfere with `localhost` workflows.
225///
226/// # Example
227///
228/// ```rust
229/// use axum::Router;
230/// use axum_security::headers::{SecurityHeaders, XssProtection};
231///
232/// let headers = SecurityHeaders::recommended()
233///     .add(XssProtection::ZERO);
234///
235/// let app = Router::<()>::new().layer(headers);
236/// ```
237#[derive(Clone)]
238pub struct SecurityHeaders {
239    headers: Arc<HashSet<SecurityHeader>>,
240    dev: bool,
241}
242
243impl Default for SecurityHeaders {
244    fn default() -> Self {
245        Self::new()
246    }
247}
248
249impl SecurityHeaders {
250    /// Create an empty `SecurityHeaders` set.
251    pub fn new() -> Self {
252        Self {
253            headers: HashSet::new().into(),
254            dev: false,
255        }
256    }
257
258    /// Create a `SecurityHeaders` set with sensible defaults.
259    ///
260    /// Includes: `Cross-Origin-Opener-Policy: same-origin`,
261    /// `Cross-Origin-Resource-Policy: same-origin`, `Origin-Agent-Cluster: ?1`,
262    /// `Referrer-Policy: no-referrer`, `Strict-Transport-Security: max-age=31536000; includeSubDomains`,
263    /// `X-Content-Type-Options: nosniff`, `X-Frame-Options: SAMEORIGIN`, `X-XSS-Protection: 0`.
264    pub fn recommended() -> Self {
265        Self::new()
266            .add(CrossOriginOpenerPolicy::SAME_ORIGIN)
267            .add(CrossOriginResourcePolicy::SAME_ORIGIN)
268            .add(OriginAgentCluster::ON)
269            .add(ReferrerPolicy::NO_REFERRER)
270            .add(
271                StrictTransportSecurity::builder()
272                    .max_age_years(1)
273                    .include_subdomains(),
274            )
275            .add(ContentTypeOptions::NO_SNIFF)
276            .add(FrameOptions::SAMEORIGIN)
277            .add(XssProtection::ZERO)
278    }
279
280    /// When `true`, clears all headers so nothing is added to responses.
281    ///
282    /// Use this in development to avoid HSTS and other headers that interfere
283    /// with `localhost` or plain HTTP.
284    pub fn use_dev_headers(mut self, dev_headers: bool) -> Self {
285        self.dev = dev_headers;
286        Arc::make_mut(&mut self.headers).clear();
287        self
288    }
289
290    /// Add a header, replacing any existing header with the same name.
291    #[allow(clippy::should_implement_trait)]
292    pub fn add(mut self, header: impl IntoSecurityHeader) -> Self {
293        if !self.dev {
294            Arc::make_mut(&mut self.headers).replace(header.into_header().into());
295        }
296        self
297    }
298
299    /// Add a header only if no header with that name exists yet.
300    pub fn try_add(mut self, header: impl IntoSecurityHeader) -> Self {
301        if !self.dev {
302            Arc::make_mut(&mut self.headers).insert(header.into_header().into());
303        }
304        self
305    }
306}
307
308#[cfg(test)]
309mod header_tests {
310    use std::collections::HashSet;
311
312    use http::HeaderValue;
313
314    use crate::headers::{
315        CROSS_ORIGIN_OPENER_POLICY, CrossOriginOpenerPolicy, SecurityHeader, SecurityHeaders,
316        X_XSS_PROTECTION,
317    };
318
319    #[test]
320    fn sec_header() {
321        let mut map = HashSet::new();
322        map.insert(SecurityHeader {
323            name: X_XSS_PROTECTION,
324            value: HeaderValue::from_static("1"),
325        });
326
327        let removed = map.remove(&SecurityHeader {
328            name: X_XSS_PROTECTION,
329            value: HeaderValue::from_static("2"),
330        });
331
332        assert!(removed);
333    }
334
335    #[test]
336    fn sec_headers() {
337        let headers = SecurityHeaders::new().add(CrossOriginOpenerPolicy::SAME_ORIGIN);
338        let header = headers.headers.get(&CROSS_ORIGIN_OPENER_POLICY).unwrap();
339        assert!(header.value == CrossOriginOpenerPolicy::SAME_ORIGIN.header_value);
340
341        let headers = headers.add(CrossOriginOpenerPolicy::UNSAFE_NONE);
342        let header = headers.headers.get(&CROSS_ORIGIN_OPENER_POLICY).unwrap();
343        assert!(header.value == CrossOriginOpenerPolicy::UNSAFE_NONE.header_value);
344
345        let headers = headers.try_add(CrossOriginOpenerPolicy::SAME_ORIGIN);
346        let header = headers.headers.get(&CROSS_ORIGIN_OPENER_POLICY).unwrap();
347        assert!(header.value == CrossOriginOpenerPolicy::UNSAFE_NONE.header_value);
348    }
349}