actix_security_core/http/security/
headers.rs1use std::future::{ready, Future, Ready};
30use std::pin::Pin;
31use std::rc::Rc;
32use std::task::{Context, Poll};
33
34use actix_service::{Service, Transform};
35use actix_web::dev::{ServiceRequest, ServiceResponse};
36use actix_web::http::header::{HeaderName, HeaderValue};
37use actix_web::Error;
38
39#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum FrameOptions {
42 Deny,
44 SameOrigin,
46 Disabled,
48}
49
50impl FrameOptions {
51 fn to_header_value(&self) -> Option<&'static str> {
52 match self {
53 FrameOptions::Deny => Some("DENY"),
54 FrameOptions::SameOrigin => Some("SAMEORIGIN"),
55 FrameOptions::Disabled => None,
56 }
57 }
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum ReferrerPolicy {
63 NoReferrer,
64 NoReferrerWhenDowngrade,
65 Origin,
66 OriginWhenCrossOrigin,
67 SameOrigin,
68 StrictOrigin,
69 StrictOriginWhenCrossOrigin,
70 UnsafeUrl,
71 Disabled,
72}
73
74impl ReferrerPolicy {
75 fn to_header_value(&self) -> Option<&'static str> {
76 match self {
77 ReferrerPolicy::NoReferrer => Some("no-referrer"),
78 ReferrerPolicy::NoReferrerWhenDowngrade => Some("no-referrer-when-downgrade"),
79 ReferrerPolicy::Origin => Some("origin"),
80 ReferrerPolicy::OriginWhenCrossOrigin => Some("origin-when-cross-origin"),
81 ReferrerPolicy::SameOrigin => Some("same-origin"),
82 ReferrerPolicy::StrictOrigin => Some("strict-origin"),
83 ReferrerPolicy::StrictOriginWhenCrossOrigin => Some("strict-origin-when-cross-origin"),
84 ReferrerPolicy::UnsafeUrl => Some("unsafe-url"),
85 ReferrerPolicy::Disabled => None,
86 }
87 }
88}
89
90#[derive(Debug, Clone)]
105pub struct SecurityHeaders {
106 pub content_type_options: bool,
108 pub frame_options: FrameOptions,
110 pub xss_protection: bool,
112 pub content_security_policy: Option<String>,
114 pub hsts_enabled: bool,
116 pub hsts_max_age: u64,
118 pub hsts_include_subdomains: bool,
120 pub hsts_preload: bool,
122 pub referrer_policy: ReferrerPolicy,
124 pub permissions_policy: Option<String>,
126 pub cache_control: Option<String>,
128}
129
130impl Default for SecurityHeaders {
131 fn default() -> Self {
139 SecurityHeaders {
140 content_type_options: true,
141 frame_options: FrameOptions::Deny,
142 xss_protection: false, content_security_policy: None,
144 hsts_enabled: false,
145 hsts_max_age: 31536000, hsts_include_subdomains: false,
147 hsts_preload: false,
148 referrer_policy: ReferrerPolicy::StrictOriginWhenCrossOrigin,
149 permissions_policy: None,
150 cache_control: None,
151 }
152 }
153}
154
155impl SecurityHeaders {
156 pub fn new() -> Self {
158 Self::default()
159 }
160
161 pub fn strict() -> Self {
165 SecurityHeaders {
166 content_type_options: true,
167 frame_options: FrameOptions::Deny,
168 xss_protection: false,
169 content_security_policy: Some("default-src 'self'".to_string()),
170 hsts_enabled: true,
171 hsts_max_age: 31536000,
172 hsts_include_subdomains: true,
173 hsts_preload: false,
174 referrer_policy: ReferrerPolicy::NoReferrer,
175 permissions_policy: Some("geolocation=(), microphone=(), camera=()".to_string()),
176 cache_control: Some("no-cache, no-store, must-revalidate".to_string()),
177 }
178 }
179
180 pub fn frame_options(mut self, options: FrameOptions) -> Self {
185 self.frame_options = options;
186 self
187 }
188
189 pub fn content_security_policy(mut self, policy: impl Into<String>) -> Self {
200 self.content_security_policy = Some(policy.into());
201 self
202 }
203
204 pub fn hsts(mut self, enabled: bool, max_age: u64) -> Self {
213 self.hsts_enabled = enabled;
214 self.hsts_max_age = max_age;
215 self
216 }
217
218 pub fn hsts_include_subdomains(mut self, include: bool) -> Self {
220 self.hsts_include_subdomains = include;
221 self
222 }
223
224 pub fn hsts_preload(mut self, preload: bool) -> Self {
229 self.hsts_preload = preload;
230 self
231 }
232
233 pub fn referrer_policy(mut self, policy: ReferrerPolicy) -> Self {
238 self.referrer_policy = policy;
239 self
240 }
241
242 pub fn permissions_policy(mut self, policy: impl Into<String>) -> Self {
250 self.permissions_policy = Some(policy.into());
251 self
252 }
253
254 pub fn cache_control(mut self, value: impl Into<String>) -> Self {
256 self.cache_control = Some(value.into());
257 self
258 }
259
260 pub fn disable_content_type_options(mut self) -> Self {
262 self.content_type_options = false;
263 self
264 }
265
266 fn build_hsts_value(&self) -> String {
267 let mut value = format!("max-age={}", self.hsts_max_age);
268 if self.hsts_include_subdomains {
269 value.push_str("; includeSubDomains");
270 }
271 if self.hsts_preload {
272 value.push_str("; preload");
273 }
274 value
275 }
276}
277
278impl<S, B> Transform<S, ServiceRequest> for SecurityHeaders
279where
280 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
281 B: 'static,
282{
283 type Response = ServiceResponse<B>;
284 type Error = Error;
285 type Transform = SecurityHeadersMiddleware<S>;
286 type InitError = ();
287 type Future = Ready<Result<Self::Transform, Self::InitError>>;
288
289 fn new_transform(&self, service: S) -> Self::Future {
290 ready(Ok(SecurityHeadersMiddleware {
291 service: Rc::new(service),
292 config: self.clone(),
293 }))
294 }
295}
296
297pub struct SecurityHeadersMiddleware<S> {
299 service: Rc<S>,
300 config: SecurityHeaders,
301}
302
303impl<S, B> Service<ServiceRequest> for SecurityHeadersMiddleware<S>
304where
305 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
306 B: 'static,
307{
308 type Response = ServiceResponse<B>;
309 type Error = Error;
310 type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
311
312 fn poll_ready(&self, ctx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
313 self.service.poll_ready(ctx)
314 }
315
316 fn call(&self, req: ServiceRequest) -> Self::Future {
317 let service = Rc::clone(&self.service);
318 let config = self.config.clone();
319
320 Box::pin(async move {
321 let mut response = service.call(req).await?;
322
323 let headers = response.headers_mut();
324
325 if config.content_type_options {
327 headers.insert(
328 HeaderName::from_static("x-content-type-options"),
329 HeaderValue::from_static("nosniff"),
330 );
331 }
332
333 if let Some(value) = config.frame_options.to_header_value() {
335 headers.insert(
336 HeaderName::from_static("x-frame-options"),
337 HeaderValue::from_static(value),
338 );
339 }
340
341 headers.insert(
343 HeaderName::from_static("x-xss-protection"),
344 HeaderValue::from_static(if config.xss_protection {
345 "1; mode=block"
346 } else {
347 "0"
348 }),
349 );
350
351 if let Some(ref csp) = config.content_security_policy {
353 if let Ok(value) = HeaderValue::from_str(csp) {
354 headers.insert(HeaderName::from_static("content-security-policy"), value);
355 }
356 }
357
358 if config.hsts_enabled {
360 let hsts_value = config.build_hsts_value();
361 if let Ok(value) = HeaderValue::from_str(&hsts_value) {
362 headers.insert(HeaderName::from_static("strict-transport-security"), value);
363 }
364 }
365
366 if let Some(value) = config.referrer_policy.to_header_value() {
368 headers.insert(
369 HeaderName::from_static("referrer-policy"),
370 HeaderValue::from_static(value),
371 );
372 }
373
374 if let Some(ref policy) = config.permissions_policy {
376 if let Ok(value) = HeaderValue::from_str(policy) {
377 headers.insert(HeaderName::from_static("permissions-policy"), value);
378 }
379 }
380
381 if let Some(ref cache) = config.cache_control {
383 if let Ok(value) = HeaderValue::from_str(cache) {
384 headers.insert(HeaderName::from_static("cache-control"), value);
385 }
386 }
387
388 Ok(response)
389 })
390 }
391}