tower_sombrero/headers/
csp.rs

1use std::borrow::Cow;
2
3use http::{header::InvalidHeaderValue, HeaderValue};
4
5// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash)]
8pub struct ContentSecurityPolicy {
9    // fetch directives
10    pub default_src: Vec<CspSource>,
11    pub child_src: Vec<CspSource>,
12    pub connect_src: Vec<CspSource>,
13    pub font_src: Vec<CspSource>,
14    pub frame_src: Vec<CspSource>,
15    pub img_src: Vec<CspSource>,
16    pub manifest_src: Vec<CspSource>,
17    pub media_src: Vec<CspSource>,
18    pub object_src: Vec<CspSource>,
19    pub script_src: Vec<CspSource>,
20    pub script_src_elem: Vec<CspSource>,
21    pub script_src_attr: Vec<CspSource>,
22    pub style_src: Vec<CspSource>,
23    pub style_src_elem: Vec<CspSource>,
24    pub style_src_attr: Vec<CspSource>,
25    pub worker_src: Vec<CspSource>,
26    // Document directives
27    pub base_uri: Vec<CspSource>,
28    pub sandbox: Vec<CspSource>,
29    // Navigation directives
30    pub form_action: Vec<CspSource>,
31    pub frame_ancestors: Vec<CspSource>,
32    // Misc
33    pub upgrade_insecure_requests: bool,
34}
35
36impl ContentSecurityPolicy {
37    // i don't want any footguns around here
38    pub const fn new_empty() -> Self {
39        Self {
40            default_src: vec![],
41            child_src: vec![],
42            connect_src: vec![],
43            font_src: vec![],
44            frame_src: vec![],
45            img_src: vec![],
46            manifest_src: vec![],
47            media_src: vec![],
48            object_src: vec![],
49            script_src: vec![],
50            script_src_elem: vec![],
51            script_src_attr: vec![],
52            style_src: vec![],
53            style_src_elem: vec![],
54            style_src_attr: vec![],
55            worker_src: vec![],
56            base_uri: vec![],
57            sandbox: vec![],
58            form_action: vec![],
59            frame_ancestors: vec![],
60            upgrade_insecure_requests: false,
61        }
62    }
63
64    pub fn strict_default() -> Self {
65        Self {
66            default_src: vec![CspSource::SelfOrigin],
67            base_uri: vec![CspSource::SelfOrigin],
68            font_src: vec![
69                CspSource::SelfOrigin,
70                CspSource::Scheme(CspSchemeSource::Https),
71                CspSource::Scheme(CspSchemeSource::Data),
72            ],
73            form_action: vec![CspSource::SelfOrigin],
74            frame_ancestors: vec![CspSource::SelfOrigin],
75            img_src: vec![
76                CspSource::SelfOrigin,
77                CspSource::Scheme(CspSchemeSource::Data),
78            ],
79            object_src: vec![CspSource::None],
80            script_src: vec![CspSource::SelfOrigin],
81            script_src_attr: vec![CspSource::None],
82            style_src: vec![
83                CspSource::SelfOrigin,
84                CspSource::Scheme(CspSchemeSource::Https),
85                CspSource::UnsafeInline,
86            ],
87            upgrade_insecure_requests: true,
88            ..Self::new_empty()
89        }
90    }
91}
92
93impl ContentSecurityPolicy {
94    pub fn value(&self, nonce: &str) -> Result<HeaderValue, InvalidHeaderValue> {
95        let mut output = String::with_capacity(256);
96        serialize_header(&mut output, nonce, "default-src", &self.default_src);
97        serialize_header(&mut output, nonce, "child-src", &self.child_src);
98        serialize_header(&mut output, nonce, "connect-src", &self.connect_src);
99        serialize_header(&mut output, nonce, "font-src", &self.font_src);
100        serialize_header(&mut output, nonce, "frame-src", &self.frame_src);
101        serialize_header(&mut output, nonce, "img-src", &self.img_src);
102        serialize_header(&mut output, nonce, "manifest-src", &self.manifest_src);
103        serialize_header(&mut output, nonce, "media-src", &self.media_src);
104        serialize_header(&mut output, nonce, "object-src", &self.object_src);
105        serialize_header(&mut output, nonce, "script-src", &self.script_src);
106        serialize_header(&mut output, nonce, "script-src-elem", &self.script_src_elem);
107        serialize_header(&mut output, nonce, "script-src-attr", &self.script_src_attr);
108        serialize_header(&mut output, nonce, "style-src", &self.style_src);
109        serialize_header(&mut output, nonce, "style-src-elem", &self.style_src_elem);
110        serialize_header(&mut output, nonce, "style-src-attr", &self.style_src_attr);
111        serialize_header(&mut output, nonce, "worker-src", &self.worker_src);
112        serialize_header(&mut output, nonce, "base-uri", &self.base_uri);
113        serialize_header(&mut output, nonce, "sandbox", &self.sandbox);
114        serialize_header(&mut output, nonce, "form-action", &self.form_action);
115        serialize_header(&mut output, nonce, "frame-ancestors", &self.frame_ancestors);
116        HeaderValue::from_str(output.as_str())
117    }
118}
119
120impl ContentSecurityPolicy {
121    pub fn upgrade_insecure_requests(self, doit: bool) -> Self {
122        Self {
123            upgrade_insecure_requests: doit,
124            ..self
125        }
126    }
127}
128
129macro_rules! csp_builder_add {
130    ($id:ident) => {
131        #[must_use]
132        pub fn $id(
133            self,
134            new: impl ::std::convert::Into<::std::vec::Vec<$crate::headers::csp::CspSource>>,
135        ) -> Self {
136            Self {
137                $id: ::std::convert::Into::into(new),
138                ..self
139            }
140        }
141    };
142}
143
144macro_rules! csp_builder_remove {
145    ($id:ident, $func:ident) => {
146        #[must_use]
147        pub fn $func(mut self) -> Self {
148            ::std::vec::Vec::clear(&mut self.$id);
149            self
150        }
151    };
152}
153
154#[rustfmt::skip]
155impl ContentSecurityPolicy { 
156    csp_builder_add!(default_src);
157    csp_builder_add!(child_src);
158    csp_builder_add!(connect_src);
159    csp_builder_add!(font_src);
160    csp_builder_add!(frame_src);
161    csp_builder_add!(img_src);
162    csp_builder_add!(manifest_src);
163    csp_builder_add!(media_src);
164    csp_builder_add!(object_src);
165    csp_builder_add!(script_src);
166    csp_builder_add!(script_src_elem);
167    csp_builder_add!(script_src_attr);
168    csp_builder_add!(style_src);
169    csp_builder_add!(style_src_elem);
170    csp_builder_add!(style_src_attr);
171    csp_builder_add!(worker_src);
172    csp_builder_add!(base_uri);
173    csp_builder_add!(sandbox);
174    csp_builder_add!(form_action);
175    csp_builder_add!(frame_ancestors);
176    csp_builder_remove!(default_src, remove_default_src);
177    csp_builder_remove!(child_src, remove_child_src);
178    csp_builder_remove!(connect_src, remove_connect_src);
179    csp_builder_remove!(font_src, remove_font_src);
180    csp_builder_remove!(frame_src, remove_frame_src);
181    csp_builder_remove!(img_src, remove_img_src);
182    csp_builder_remove!(manifest_src, remove_manifest_src);
183    csp_builder_remove!(media_src, remove_media_src);
184    csp_builder_remove!(object_src, remove_object_src);
185    csp_builder_remove!(script_src, remove_script_src);
186    csp_builder_remove!(script_src_elem, remove_script_src_elem);
187    csp_builder_remove!(script_src_attr, remove_script_src_attr);
188    csp_builder_remove!(style_src, remove_style_src);
189    csp_builder_remove!(style_src_elem, remove_style_src_elem);
190    csp_builder_remove!(style_src_attr, remove_style_src_attr);
191    csp_builder_remove!(worker_src, remove_worker_src);
192    csp_builder_remove!(base_uri, remove_base_uri);
193    csp_builder_remove!(sandbox, remove_sandbox);
194    csp_builder_remove!(form_action, remove_form_action);
195    csp_builder_remove!(frame_ancestors, remove_frame_ancestors);
196}
197
198#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
199pub enum CspSchemeSource {
200    Data,
201    Mediastream,
202    Blob,
203    Filesystem,
204    Http,
205    Https,
206}
207
208impl AsRef<str> for CspSchemeSource {
209    fn as_ref(&self) -> &str {
210        match self {
211            Self::Data => "data:",
212            Self::Mediastream => "mediastream:",
213            Self::Blob => "blob:",
214            Self::Filesystem => "filesystem:",
215            Self::Http => "http:",
216            Self::Https => "https:",
217        }
218    }
219}
220
221impl From<CspSchemeSource> for CspSource {
222    fn from(value: CspSchemeSource) -> Self {
223        Self::Scheme(value)
224    }
225}
226
227#[derive(Debug, Clone, PartialEq, Eq, Hash)]
228pub enum CspHashAlgorithm {
229    Sha256,
230    Sha384,
231    Sha512,
232    Custom(String),
233}
234
235impl AsRef<str> for CspHashAlgorithm {
236    fn as_ref(&self) -> &str {
237        match self {
238            Self::Sha256 => "sha256",
239            Self::Sha384 => "sha384",
240            Self::Sha512 => "sha512",
241            Self::Custom(s) => s.as_ref(),
242        }
243    }
244}
245
246#[derive(Debug, Clone, PartialEq, Eq, Hash)]
247pub enum CspSource {
248    Host(String),
249    Scheme(CspSchemeSource),
250    /// Nonce has special handling by the library.
251    Nonce,
252    Hash(CspHashAlgorithm, String),
253    /// Self is a keyword in rust, so [`Self::SelfOrigin`] serializes to `'self'` in the header.
254    SelfOrigin,
255    UnsafeEval,
256    WasmUnsafeEval,
257    UnsafeHashes,
258    UnsafeInline,
259    StrictDynamic,
260    ReportSample,
261    InlineSpeculationRules,
262    None,
263}
264
265impl CspSource {
266    fn as_cow(&self, nonce: &str) -> Cow<'_, str> {
267        let borrowed = match self {
268            Self::Host(s) => s.as_str(),
269            Self::Scheme(s) => s.as_ref(),
270            Self::Nonce => return Cow::Owned(format!("'nonce-{nonce}'")),
271            Self::Hash(algo, data) => return Cow::Owned(format!("'{}-{data}'", algo.as_ref())),
272            Self::SelfOrigin => "'self'",
273            Self::UnsafeEval => "'unsafe-eval'",
274            Self::WasmUnsafeEval => "'wasm-unsafe-eval'",
275            Self::UnsafeHashes => "'unsafe-hashes'",
276            Self::UnsafeInline => "'unsafe-inline'",
277            Self::StrictDynamic => "'strict-dynamic'",
278            Self::ReportSample => "'report-sample'",
279            Self::InlineSpeculationRules => "'inline-speculation-rules'",
280            Self::None => "'none'",
281        };
282        Cow::Borrowed(borrowed)
283    }
284}
285
286impl From<CspSource> for Vec<CspSource> {
287    fn from(value: CspSource) -> Self {
288        vec![value]
289    }
290}
291
292fn serialize_header(s: &mut String, nonce: &str, name: &str, sources: &[CspSource]) {
293    if sources.is_empty() {
294        return;
295    }
296    s.push_str(name);
297    for source in sources {
298        s.push(' ');
299        s.push_str(source.as_cow(nonce).as_ref());
300    }
301    s.push(';');
302}