Skip to main content

axum_security/headers/
csp.rs

1use std::borrow::Cow;
2
3use http::{HeaderName, HeaderValue};
4use tower::Layer;
5
6use crate::{headers::IntoSecurityHeader, utils::headers::InsertHeadersService};
7
8const CONTENT_SECURITY_POLICY: HeaderName = HeaderName::from_static("content-security-policy");
9
10/// `Content-Security-Policy` header.
11///
12/// Build one with [`ContentSecurityPolicy::builder`]. The result implements
13/// [`Layer`] (for standalone use) and [`IntoSecurityHeader`](super::IntoSecurityHeader)
14/// (for use with [`SecurityHeaders`](super::SecurityHeaders)).
15///
16/// # Example
17///
18/// ```rust
19/// use axum_security::headers::{ContentSecurityPolicy, CspSource};
20///
21/// let csp = ContentSecurityPolicy::builder()
22///     .default_src(CspSource::SELF)
23///     .script_src([CspSource::SELF, CspSource::host("https://cdn.example.com")])
24///     .upgrade_insecure_requests()
25///     .build();
26/// ```
27#[derive(Clone)]
28pub struct ContentSecurityPolicy {
29    header_value: HeaderValue,
30}
31
32impl ContentSecurityPolicy {
33    /// Create a [`CspBuilder`] to construct a Content-Security-Policy header.
34    pub fn builder() -> CspBuilder {
35        CspBuilder {
36            directives: Vec::new(),
37        }
38    }
39}
40
41/// Builder for [`ContentSecurityPolicy`].
42///
43/// Add directives with methods like [`default_src`](CspBuilder::default_src),
44/// [`script_src`](CspBuilder::script_src), etc., then call [`build`](CspBuilder::build).
45pub struct CspBuilder {
46    directives: Vec<(Cow<'static, str>, Vec<CspSource>)>,
47}
48
49/// A CSP source expression.
50///
51/// Use the provided constants ([`SELF`](CspSource::SELF), [`NONE`](CspSource::NONE), etc.)
52/// or construct one with [`host`](CspSource::host) / [`scheme`](CspSource::scheme).
53///
54/// Pass a single source directly or wrap multiple sources in an array:
55///
56/// ```rust
57/// use axum_security::headers::{ContentSecurityPolicy, CspSource};
58///
59/// let csp = ContentSecurityPolicy::builder()
60///     .default_src(CspSource::NONE) // single source
61///     .script_src([CspSource::SELF, CspSource::host("https://cdn.example.com")]) // multiple
62///     .build();
63/// ```
64pub struct CspSource(CspSourceInner);
65
66#[derive(Clone)]
67enum CspSourceInner {
68    None,
69    Self_,
70    UnsafeInline,
71    UnsafeEval,
72    StrictDynamic,
73    Host(Cow<'static, str>),
74    Scheme(Cow<'static, str>),
75}
76
77impl CspSource {
78    pub const NONE: CspSource = CspSource(CspSourceInner::None);
79    pub const SELF: CspSource = CspSource(CspSourceInner::Self_);
80    pub const UNSAFE_INLINE: CspSource = CspSource(CspSourceInner::UnsafeInline);
81    pub const UNSAFE_EVAL: CspSource = CspSource(CspSourceInner::UnsafeEval);
82    pub const STRICT_DYNAMIC: CspSource = CspSource(CspSourceInner::StrictDynamic);
83
84    pub fn host(host: impl Into<Cow<'static, str>>) -> Self {
85        Self(CspSourceInner::Host(host.into()))
86    }
87
88    pub fn scheme(scheme: impl Into<Cow<'static, str>>) -> Self {
89        Self(CspSourceInner::Scheme(scheme.into()))
90    }
91
92    fn serialize(&self) -> Cow<'static, str> {
93        match &self.0 {
94            CspSourceInner::None => "'none'".into(),
95            CspSourceInner::Self_ => "'self'".into(),
96            CspSourceInner::UnsafeInline => "'unsafe-inline'".into(),
97            CspSourceInner::UnsafeEval => "'unsafe-eval'".into(),
98            CspSourceInner::StrictDynamic => "'strict-dynamic'".into(),
99            CspSourceInner::Host(h) => h.clone(),
100            CspSourceInner::Scheme(s) => s.clone(),
101        }
102    }
103}
104
105impl From<CspSource> for Vec<CspSource> {
106    fn from(source: CspSource) -> Self {
107        vec![source]
108    }
109}
110
111impl CspBuilder {
112    pub(crate) fn directive(
113        mut self,
114        name: impl Into<Cow<'static, str>>,
115        sources: impl Into<Vec<CspSource>>,
116    ) -> Self {
117        self.directives.push((name.into(), sources.into()));
118        self
119    }
120
121    pub fn default_src(self, sources: impl Into<Vec<CspSource>>) -> Self {
122        self.directive("default-src", sources)
123    }
124
125    pub fn script_src(self, sources: impl Into<Vec<CspSource>>) -> Self {
126        self.directive("script-src", sources)
127    }
128
129    pub fn style_src(self, sources: impl Into<Vec<CspSource>>) -> Self {
130        self.directive("style-src", sources)
131    }
132
133    pub fn img_src(self, sources: impl Into<Vec<CspSource>>) -> Self {
134        self.directive("img-src", sources)
135    }
136
137    pub fn font_src(self, sources: impl Into<Vec<CspSource>>) -> Self {
138        self.directive("font-src", sources)
139    }
140
141    pub fn connect_src(self, sources: impl Into<Vec<CspSource>>) -> Self {
142        self.directive("connect-src", sources)
143    }
144
145    pub fn frame_src(self, sources: impl Into<Vec<CspSource>>) -> Self {
146        self.directive("frame-src", sources)
147    }
148
149    pub fn frame_ancestors(self, sources: impl Into<Vec<CspSource>>) -> Self {
150        self.directive("frame-ancestors", sources)
151    }
152
153    pub fn base_uri(self, sources: impl Into<Vec<CspSource>>) -> Self {
154        self.directive("base-uri", sources)
155    }
156
157    pub fn form_action(self, sources: impl Into<Vec<CspSource>>) -> Self {
158        self.directive("form-action", sources)
159    }
160
161    pub fn upgrade_insecure_requests(mut self) -> Self {
162        self.directives
163            .push(("upgrade-insecure-requests".into(), Vec::new()));
164        self
165    }
166
167    pub fn build(self) -> ContentSecurityPolicy {
168        let mut buff = String::new();
169
170        for (i, (name, sources)) in self.directives.iter().enumerate() {
171            if i != 0 {
172                buff.push_str("; ");
173            }
174            buff.push_str(name);
175
176            if !sources.is_empty() {
177                for s in sources {
178                    buff.push(' ');
179                    buff.push_str(&s.serialize());
180                }
181            }
182        }
183
184        let header_value =
185            HeaderValue::from_str(&buff).expect("CSP header does not contain invalid bytes");
186
187        ContentSecurityPolicy { header_value }
188    }
189}
190
191impl<S> Layer<S> for ContentSecurityPolicy {
192    type Service = InsertHeadersService<S>;
193
194    fn layer(&self, inner: S) -> Self::Service {
195        InsertHeadersService {
196            inner,
197            header_name: CONTENT_SECURITY_POLICY,
198            header_value: self.header_value.clone(),
199        }
200    }
201}
202
203impl IntoSecurityHeader for ContentSecurityPolicy {
204    fn into_header(self) -> (HeaderName, HeaderValue) {
205        (CONTENT_SECURITY_POLICY, self.header_value)
206    }
207}
208
209impl IntoSecurityHeader for CspBuilder {
210    fn into_header(self) -> (HeaderName, HeaderValue) {
211        self.build().into_header()
212    }
213}
214
215#[cfg(test)]
216mod csp_tests {
217    use axum::{Router, body::Body, extract::Request};
218    use tower::ServiceExt;
219
220    use super::*;
221    use crate::headers::SecurityHeaders;
222
223    #[test]
224    fn default_src_none() {
225        let csp = ContentSecurityPolicy::builder()
226            .default_src(CspSource::NONE)
227            .build();
228        assert_eq!(csp.header_value, "default-src 'none'");
229    }
230
231    #[test]
232    fn multiple_sources() {
233        let csp = ContentSecurityPolicy::builder()
234            .default_src([CspSource::SELF, CspSource::host("https://example.com")])
235            .build();
236        assert_eq!(csp.header_value, "default-src 'self' https://example.com");
237    }
238
239    #[test]
240    fn multiple_directives() {
241        let csp = ContentSecurityPolicy::builder()
242            .default_src(CspSource::SELF)
243            .script_src([CspSource::SELF, CspSource::host("https://cdn.example.com")])
244            .build();
245        assert_eq!(
246            csp.header_value,
247            "default-src 'self'; script-src 'self' https://cdn.example.com"
248        );
249    }
250
251    #[test]
252    fn upgrade_insecure_requests() {
253        let csp = ContentSecurityPolicy::builder()
254            .default_src(CspSource::SELF)
255            .upgrade_insecure_requests()
256            .build();
257        assert_eq!(
258            csp.header_value,
259            "default-src 'self'; upgrade-insecure-requests"
260        );
261    }
262
263    #[test]
264    fn into_security_header() {
265        let csp = ContentSecurityPolicy::builder()
266            .default_src(CspSource::SELF)
267            .build();
268        let headers = SecurityHeaders::new().add(csp);
269        let header = headers.headers.get(&CONTENT_SECURITY_POLICY).unwrap();
270        assert_eq!(header.value, "default-src 'self'");
271    }
272
273    #[tokio::test]
274    async fn layer() {
275        let csp = ContentSecurityPolicy::builder()
276            .default_src(CspSource::NONE)
277            .script_src(CspSource::SELF)
278            .build();
279
280        let router = Router::<()>::new().layer(csp);
281
282        let res = router
283            .oneshot(Request::get("/").body(Body::empty()).unwrap())
284            .await
285            .unwrap();
286
287        assert_eq!(
288            res.headers()["content-security-policy"],
289            "default-src 'none'; script-src 'self'"
290        );
291    }
292}