axum_security/headers/
csp.rs1use 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#[derive(Clone)]
28pub struct ContentSecurityPolicy {
29 header_value: HeaderValue,
30}
31
32impl ContentSecurityPolicy {
33 pub fn builder() -> CspBuilder {
35 CspBuilder {
36 directives: Vec::new(),
37 }
38 }
39}
40
41pub struct CspBuilder {
46 directives: Vec<(Cow<'static, str>, Vec<CspSource>)>,
47}
48
49pub 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}