Skip to main content

actix_security_core/http/security/
headers.rs

1//! Security headers middleware for HTTP security.
2//!
3//! # Spring Security Equivalent
4//! `HttpSecurity.headers()` configuration
5//!
6//! # Overview
7//! Adds security-related HTTP headers to responses:
8//!
9//! - `X-Content-Type-Options: nosniff` - Prevents MIME-sniffing
10//! - `X-Frame-Options: DENY` - Prevents clickjacking
11//! - `X-XSS-Protection: 0` - Disables XSS Auditor (deprecated but safe)
12//! - `Strict-Transport-Security` - Forces HTTPS (HSTS)
13//! - `Content-Security-Policy` - Controls resource loading
14//! - `Referrer-Policy` - Controls referrer information
15//! - `Permissions-Policy` - Controls browser features
16//!
17//! # Usage
18//! ```ignore
19//! use actix_web::{App, HttpServer};
20//! use actix_security_core::http::security::headers::SecurityHeaders;
21//!
22//! HttpServer::new(|| {
23//!     App::new()
24//!         .wrap(SecurityHeaders::default())
25//!         // ... routes
26//! })
27//! ```
28
29use 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/// Frame options for X-Frame-Options header.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub enum FrameOptions {
42    /// Prevents the page from being framed entirely.
43    Deny,
44    /// Allows framing by the same origin only.
45    SameOrigin,
46    /// Disables X-Frame-Options header.
47    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/// Referrer policy options.
61#[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/// Security headers configuration.
91///
92/// # Spring Security Equivalent
93/// `HttpSecurity.headers()`
94///
95/// # Example
96/// ```ignore
97/// use actix_security_core::http::security::headers::{SecurityHeaders, FrameOptions};
98///
99/// let headers = SecurityHeaders::new()
100///     .frame_options(FrameOptions::SameOrigin)
101///     .content_security_policy("default-src 'self'")
102///     .hsts(true, 31536000); // 1 year
103/// ```
104#[derive(Debug, Clone)]
105pub struct SecurityHeaders {
106    /// X-Content-Type-Options header (default: nosniff)
107    pub content_type_options: bool,
108    /// X-Frame-Options header (default: DENY)
109    pub frame_options: FrameOptions,
110    /// X-XSS-Protection header (default: 0)
111    pub xss_protection: bool,
112    /// Content-Security-Policy header (default: None)
113    pub content_security_policy: Option<String>,
114    /// Strict-Transport-Security header (default: disabled)
115    pub hsts_enabled: bool,
116    /// HSTS max-age in seconds (default: 31536000 = 1 year)
117    pub hsts_max_age: u64,
118    /// HSTS include subdomains (default: false)
119    pub hsts_include_subdomains: bool,
120    /// HSTS preload (default: false)
121    pub hsts_preload: bool,
122    /// Referrer-Policy header (default: strict-origin-when-cross-origin)
123    pub referrer_policy: ReferrerPolicy,
124    /// Permissions-Policy header (default: None)
125    pub permissions_policy: Option<String>,
126    /// Cache-Control header for sensitive content (default: None)
127    pub cache_control: Option<String>,
128}
129
130impl Default for SecurityHeaders {
131    /// Creates security headers with sensible defaults.
132    ///
133    /// # Default Values
134    /// - `X-Content-Type-Options: nosniff`
135    /// - `X-Frame-Options: DENY`
136    /// - `X-XSS-Protection: 0` (disabled as recommended)
137    /// - `Referrer-Policy: strict-origin-when-cross-origin`
138    fn default() -> Self {
139        SecurityHeaders {
140            content_type_options: true,
141            frame_options: FrameOptions::Deny,
142            xss_protection: false, // XSS Auditor is deprecated
143            content_security_policy: None,
144            hsts_enabled: false,
145            hsts_max_age: 31536000, // 1 year
146            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    /// Creates a new security headers configuration with defaults.
157    pub fn new() -> Self {
158        Self::default()
159    }
160
161    /// Creates a strict security headers configuration.
162    ///
163    /// Enables all security headers with strict values.
164    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    /// Sets the X-Frame-Options header.
181    ///
182    /// # Spring Security Equivalent
183    /// `headers().frameOptions().deny()` or `.sameOrigin()`
184    pub fn frame_options(mut self, options: FrameOptions) -> Self {
185        self.frame_options = options;
186        self
187    }
188
189    /// Sets the Content-Security-Policy header.
190    ///
191    /// # Spring Security Equivalent
192    /// `headers().contentSecurityPolicy("policy")`
193    ///
194    /// # Example
195    /// ```ignore
196    /// let headers = SecurityHeaders::new()
197    ///     .content_security_policy("default-src 'self'; script-src 'self' 'unsafe-inline'");
198    /// ```
199    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    /// Enables HTTP Strict Transport Security (HSTS).
205    ///
206    /// # Spring Security Equivalent
207    /// `headers().httpStrictTransportSecurity()`
208    ///
209    /// # Arguments
210    /// * `enabled` - Whether to enable HSTS
211    /// * `max_age` - Max-age value in seconds
212    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    /// Sets HSTS to include subdomains.
219    pub fn hsts_include_subdomains(mut self, include: bool) -> Self {
220        self.hsts_include_subdomains = include;
221        self
222    }
223
224    /// Sets HSTS preload flag.
225    ///
226    /// # Warning
227    /// Only enable this if you've submitted your domain to the HSTS preload list.
228    pub fn hsts_preload(mut self, preload: bool) -> Self {
229        self.hsts_preload = preload;
230        self
231    }
232
233    /// Sets the Referrer-Policy header.
234    ///
235    /// # Spring Security Equivalent
236    /// `headers().referrerPolicy(ReferrerPolicy.STRICT_ORIGIN)`
237    pub fn referrer_policy(mut self, policy: ReferrerPolicy) -> Self {
238        self.referrer_policy = policy;
239        self
240    }
241
242    /// Sets the Permissions-Policy header.
243    ///
244    /// # Example
245    /// ```ignore
246    /// let headers = SecurityHeaders::new()
247    ///     .permissions_policy("geolocation=(), microphone=(), camera=()");
248    /// ```
249    pub fn permissions_policy(mut self, policy: impl Into<String>) -> Self {
250        self.permissions_policy = Some(policy.into());
251        self
252    }
253
254    /// Sets the Cache-Control header for sensitive content.
255    pub fn cache_control(mut self, value: impl Into<String>) -> Self {
256        self.cache_control = Some(value.into());
257        self
258    }
259
260    /// Disables X-Content-Type-Options header.
261    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
297/// Security headers middleware service.
298pub 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            // X-Content-Type-Options
326            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            // X-Frame-Options
334            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            // X-XSS-Protection (disabled by default, set to 0)
342            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            // Content-Security-Policy
352            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            // Strict-Transport-Security (HSTS)
359            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            // Referrer-Policy
367            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            // Permissions-Policy
375            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            // Cache-Control
382            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}