armature_security/
lib.rs

1//! Security middleware for Armature - inspired by Helmet for Express.js
2//!
3//! This module provides a comprehensive set of security headers and protections
4//! to help secure your Armature applications against common web vulnerabilities.
5//!
6//! ## Features
7//!
8//! - 🛡️ **Content Security Policy** - Prevent XSS attacks
9//! - 🔒 **HSTS** - Force HTTPS connections
10//! - 🚫 **Frame Guard** - Prevent clickjacking
11//! - 🎭 **XSS Filter** - Browser XSS protection
12//! - 📝 **Content Type Options** - Prevent MIME sniffing
13//! - 🌐 **Referrer Policy** - Control referrer information
14//! - 🔐 **11 Security Headers** - Comprehensive protection
15//!
16//! ## Quick Start - Default Security
17//!
18//! ```
19//! use armature_security::SecurityMiddleware;
20//!
21//! // Use default security settings (recommended for most apps)
22//! let security = SecurityMiddleware::default();
23//!
24//! // Apply to a response
25//! let mut response = armature_core::HttpResponse::ok();
26//! let response = security.apply(response);
27//!
28//! // Now response has all default security headers
29//! assert!(response.headers.contains_key("X-Frame-Options"));
30//! assert!(response.headers.contains_key("X-Content-Type-Options"));
31//! ```
32//!
33//! ## Custom Configuration
34//!
35//! ```
36//! use armature_security::SecurityMiddleware;
37//! use armature_security::hsts::HstsConfig;
38//! use armature_security::frame_guard::FrameGuard;
39//! use armature_security::referrer_policy::ReferrerPolicy;
40//!
41//! let security = SecurityMiddleware::new()
42//!     .with_hsts(HstsConfig::new(31536000).include_subdomains(true))
43//!     .with_frame_guard(FrameGuard::Deny)
44//!     .with_referrer_policy(ReferrerPolicy::StrictOrigin)
45//!     .hide_powered_by(true);
46//!
47//! let response = security.apply(armature_core::HttpResponse::ok());
48//! assert_eq!(response.headers.get("X-Frame-Options"), Some(&"DENY".to_string()));
49//! ```
50//!
51//! ## Content Security Policy
52//!
53//! ```
54//! use armature_security::SecurityMiddleware;
55//! use armature_security::content_security_policy::CspConfig;
56//!
57//! let csp = CspConfig::new()
58//!     .default_src(vec!["'self'".to_string()])
59//!     .script_src(vec!["'self'".to_string(), "'unsafe-inline'".to_string()])
60//!     .style_src(vec!["'self'".to_string(), "https://fonts.googleapis.com".to_string()]);
61//!
62//! let security = SecurityMiddleware::new().with_csp(csp);
63//! let response = security.apply(armature_core::HttpResponse::ok());
64//!
65//! assert!(response.headers.contains_key("Content-Security-Policy"));
66//! ```
67//!
68//! ## HSTS (HTTP Strict Transport Security)
69//!
70//! ```
71//! use armature_security::SecurityMiddleware;
72//! use armature_security::hsts::HstsConfig;
73//!
74//! // HSTS for 1 year with subdomains
75//! let hsts = HstsConfig::new(31536000)
76//!     .include_subdomains(true)
77//!     .preload(true);
78//!
79//! let security = SecurityMiddleware::new().with_hsts(hsts);
80//! let response = security.apply(armature_core::HttpResponse::ok());
81//!
82//! let hsts_header = response.headers.get("Strict-Transport-Security").unwrap();
83//! assert!(hsts_header.contains("max-age=31536000"));
84//! assert!(hsts_header.contains("includeSubDomains"));
85//! ```
86//!
87//! ## Frame Guard (Clickjacking Protection)
88//!
89//! ```
90//! use armature_security::SecurityMiddleware;
91//! use armature_security::frame_guard::FrameGuard;
92//!
93//! // Deny all framing
94//! let security = SecurityMiddleware::new()
95//!     .with_frame_guard(FrameGuard::Deny);
96//!
97//! let response = security.apply(armature_core::HttpResponse::ok());
98//! assert_eq!(response.headers.get("X-Frame-Options"), Some(&"DENY".to_string()));
99//!
100//! // Allow framing from same origin
101//! let security = SecurityMiddleware::new()
102//!     .with_frame_guard(FrameGuard::SameOrigin);
103//!
104//! let response = security.apply(armature_core::HttpResponse::ok());
105//! assert_eq!(response.headers.get("X-Frame-Options"), Some(&"SAMEORIGIN".to_string()));
106//! ```
107
108pub mod content_security_policy;
109pub mod content_type_options;
110pub mod cors;
111pub mod dns_prefetch_control;
112pub mod download_options;
113pub mod expect_ct;
114pub mod frame_guard;
115pub mod hsts;
116pub mod permitted_cross_domain_policies;
117pub mod powered_by;
118pub mod referrer_policy;
119pub mod request_signing;
120pub mod xss_filter;
121
122use armature_core::HttpResponse;
123use std::collections::HashMap;
124
125/// Main security middleware that combines all security features
126#[derive(Debug, Clone)]
127pub struct SecurityMiddleware {
128    /// Content Security Policy configuration
129    pub csp: Option<content_security_policy::CspConfig>,
130
131    /// DNS Prefetch Control
132    pub dns_prefetch_control: dns_prefetch_control::DnsPrefetchControl,
133
134    /// Expect-CT configuration
135    pub expect_ct: Option<expect_ct::ExpectCtConfig>,
136
137    /// Frame Guard (X-Frame-Options)
138    pub frame_guard: frame_guard::FrameGuard,
139
140    /// HSTS (Strict-Transport-Security)
141    pub hsts: Option<hsts::HstsConfig>,
142
143    /// Hide X-Powered-By header
144    pub hide_powered_by: bool,
145
146    /// Referrer Policy
147    pub referrer_policy: referrer_policy::ReferrerPolicy,
148
149    /// X-XSS-Protection
150    pub xss_filter: xss_filter::XssFilter,
151
152    /// X-Content-Type-Options
153    pub content_type_options: content_type_options::ContentTypeOptions,
154
155    /// X-Download-Options
156    pub download_options: download_options::DownloadOptions,
157
158    /// X-Permitted-Cross-Domain-Policies
159    pub permitted_cross_domain_policies:
160        permitted_cross_domain_policies::PermittedCrossDomainPolicies,
161}
162
163impl SecurityMiddleware {
164    /// Create a new security middleware with no protections enabled
165    pub fn new() -> Self {
166        Self {
167            csp: None,
168            dns_prefetch_control: dns_prefetch_control::DnsPrefetchControl::Off,
169            expect_ct: None,
170            frame_guard: frame_guard::FrameGuard::Deny,
171            hsts: None,
172            hide_powered_by: false,
173            referrer_policy: referrer_policy::ReferrerPolicy::NoReferrer,
174            xss_filter: xss_filter::XssFilter::Enabled,
175            content_type_options: content_type_options::ContentTypeOptions::NoSniff,
176            download_options: download_options::DownloadOptions::NoOpen,
177            permitted_cross_domain_policies:
178                permitted_cross_domain_policies::PermittedCrossDomainPolicies::None,
179        }
180    }
181
182    /// Enable Content Security Policy
183    pub fn with_csp(mut self, config: content_security_policy::CspConfig) -> Self {
184        self.csp = Some(config);
185        self
186    }
187
188    /// Enable DNS Prefetch Control
189    pub fn with_dns_prefetch_control(
190        mut self,
191        control: dns_prefetch_control::DnsPrefetchControl,
192    ) -> Self {
193        self.dns_prefetch_control = control;
194        self
195    }
196
197    /// Enable Expect-CT
198    pub fn with_expect_ct(mut self, config: expect_ct::ExpectCtConfig) -> Self {
199        self.expect_ct = Some(config);
200        self
201    }
202
203    /// Set Frame Guard policy
204    pub fn with_frame_guard(mut self, guard: frame_guard::FrameGuard) -> Self {
205        self.frame_guard = guard;
206        self
207    }
208
209    /// Enable HSTS
210    pub fn with_hsts(mut self, config: hsts::HstsConfig) -> Self {
211        self.hsts = Some(config);
212        self
213    }
214
215    /// Hide X-Powered-By header
216    pub fn hide_powered_by(mut self, hide: bool) -> Self {
217        self.hide_powered_by = hide;
218        self
219    }
220
221    /// Set Referrer Policy
222    pub fn with_referrer_policy(mut self, policy: referrer_policy::ReferrerPolicy) -> Self {
223        self.referrer_policy = policy;
224        self
225    }
226
227    /// Set XSS Filter
228    pub fn with_xss_filter(mut self, filter: xss_filter::XssFilter) -> Self {
229        self.xss_filter = filter;
230        self
231    }
232
233    /// Apply security headers to a response
234    pub fn apply(&self, mut response: HttpResponse) -> HttpResponse {
235        let mut headers = HashMap::new();
236
237        // Content Security Policy
238        if let Some(ref csp) = self.csp {
239            headers.insert("Content-Security-Policy".to_string(), csp.to_header_value());
240        }
241
242        // DNS Prefetch Control
243        headers.insert(
244            "X-DNS-Prefetch-Control".to_string(),
245            self.dns_prefetch_control.to_header_value(),
246        );
247
248        // Expect-CT
249        if let Some(ref expect_ct) = self.expect_ct {
250            headers.insert("Expect-CT".to_string(), expect_ct.to_header_value());
251        }
252
253        // Frame Guard
254        headers.insert(
255            "X-Frame-Options".to_string(),
256            self.frame_guard.to_header_value(),
257        );
258
259        // HSTS
260        if let Some(ref hsts) = self.hsts {
261            headers.insert(
262                "Strict-Transport-Security".to_string(),
263                hsts.to_header_value(),
264            );
265        }
266
267        // Referrer Policy
268        headers.insert(
269            "Referrer-Policy".to_string(),
270            self.referrer_policy.to_header_value(),
271        );
272
273        // XSS Filter
274        headers.insert(
275            "X-XSS-Protection".to_string(),
276            self.xss_filter.to_header_value(),
277        );
278
279        // Content Type Options
280        headers.insert(
281            "X-Content-Type-Options".to_string(),
282            self.content_type_options.to_header_value(),
283        );
284
285        // Download Options
286        headers.insert(
287            "X-Download-Options".to_string(),
288            self.download_options.to_header_value(),
289        );
290
291        // Permitted Cross Domain Policies
292        headers.insert(
293            "X-Permitted-Cross-Domain-Policies".to_string(),
294            self.permitted_cross_domain_policies.to_header_value(),
295        );
296
297        // Apply all headers
298        for (key, value) in headers {
299            response.headers.insert(key, value);
300        }
301
302        // Remove X-Powered-By if requested
303        if self.hide_powered_by {
304            response.headers.remove("X-Powered-By");
305        }
306
307        response
308    }
309
310    /// Convenience method to enable all common security features (recommended defaults)
311    pub fn enable_all(max_age_seconds: u64) -> Self {
312        Self {
313            csp: Some(content_security_policy::CspConfig::default()),
314            dns_prefetch_control: dns_prefetch_control::DnsPrefetchControl::Off,
315            expect_ct: Some(expect_ct::ExpectCtConfig::new(max_age_seconds)),
316            frame_guard: frame_guard::FrameGuard::Deny,
317            hsts: Some(hsts::HstsConfig::new(max_age_seconds)),
318            hide_powered_by: true,
319            referrer_policy: referrer_policy::ReferrerPolicy::NoReferrer,
320            xss_filter: xss_filter::XssFilter::Enabled,
321            content_type_options: content_type_options::ContentTypeOptions::NoSniff,
322            download_options: download_options::DownloadOptions::NoOpen,
323            permitted_cross_domain_policies:
324                permitted_cross_domain_policies::PermittedCrossDomainPolicies::None,
325        }
326    }
327}
328
329impl Default for SecurityMiddleware {
330    fn default() -> Self {
331        Self::enable_all(31536000) // 1 year
332    }
333}
334
335/// Prelude for common imports.
336///
337/// ```
338/// use armature_security::prelude::*;
339/// ```
340pub mod prelude {
341    pub use crate::SecurityMiddleware;
342    pub use crate::content_security_policy::CspConfig;
343    pub use crate::cors::CorsConfig;
344    pub use crate::frame_guard::FrameGuard;
345    pub use crate::hsts::HstsConfig;
346    pub use crate::referrer_policy::ReferrerPolicy;
347    pub use crate::request_signing::{RequestSigner, RequestSigningMiddleware, RequestVerifier};
348}
349
350/// Implement the core Middleware trait for SecurityMiddleware
351/// This allows it to be used in a MiddlewareChain
352#[async_trait::async_trait]
353impl armature_core::Middleware for SecurityMiddleware {
354    async fn handle(
355        &self,
356        req: armature_core::HttpRequest,
357        next: Box<
358            dyn FnOnce(
359                    armature_core::HttpRequest,
360                ) -> std::pin::Pin<
361                    Box<
362                        dyn std::future::Future<
363                                Output = Result<armature_core::HttpResponse, armature_core::Error>,
364                            > + Send,
365                    >,
366                > + Send,
367        >,
368    ) -> Result<armature_core::HttpResponse, armature_core::Error> {
369        // Call the next handler first
370        let response = next(req).await?;
371        // Apply security headers to the response
372        Ok(self.apply(response))
373    }
374}
375
376#[cfg(test)]
377mod tests {
378    use super::*;
379
380    #[test]
381    fn test_security_middleware_new() {
382        let middleware = SecurityMiddleware::new();
383        assert!(middleware.csp.is_none());
384        assert!(!middleware.hide_powered_by);
385    }
386
387    #[test]
388    fn test_security_middleware_default() {
389        let middleware = SecurityMiddleware::default();
390        assert!(middleware.csp.is_some());
391        assert!(middleware.hsts.is_some());
392        assert!(middleware.hide_powered_by);
393    }
394
395    #[test]
396    fn test_security_middleware_apply() {
397        let middleware = SecurityMiddleware::default();
398        let response = HttpResponse::ok();
399        let secured = middleware.apply(response);
400
401        assert!(secured.headers.contains_key("X-Frame-Options"));
402        assert!(secured.headers.contains_key("X-Content-Type-Options"));
403        assert!(secured.headers.contains_key("X-XSS-Protection"));
404        assert!(secured.headers.contains_key("Strict-Transport-Security"));
405        assert!(secured.headers.contains_key("Content-Security-Policy"));
406    }
407
408    #[test]
409    fn test_hide_powered_by() {
410        let middleware = SecurityMiddleware::new().hide_powered_by(true);
411        let mut response = HttpResponse::ok();
412        response
413            .headers
414            .insert("X-Powered-By".to_string(), "Armature".to_string());
415
416        let secured = middleware.apply(response);
417        assert!(!secured.headers.contains_key("X-Powered-By"));
418    }
419
420    #[test]
421    fn test_custom_configuration() {
422        let middleware = SecurityMiddleware::new()
423            .with_frame_guard(frame_guard::FrameGuard::SameOrigin)
424            .with_referrer_policy(referrer_policy::ReferrerPolicy::StrictOriginWhenCrossOrigin)
425            .hide_powered_by(true);
426
427        let response = HttpResponse::ok();
428        let secured = middleware.apply(response);
429
430        assert_eq!(
431            secured.headers.get("X-Frame-Options"),
432            Some(&"SAMEORIGIN".to_string())
433        );
434        assert_eq!(
435            secured.headers.get("Referrer-Policy"),
436            Some(&"strict-origin-when-cross-origin".to_string())
437        );
438    }
439}