axum_security/headers/
mod.rs1mod 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
183pub 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#[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 pub fn new() -> Self {
252 Self {
253 headers: HashSet::new().into(),
254 dev: false,
255 }
256 }
257
258 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 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 #[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 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}