ricecoder_providers/
security_headers.rs

1//! Security headers for HTTP responses
2//!
3//! This module provides utilities for adding security headers to HTTP responses
4//! to prevent common web vulnerabilities.
5
6use std::collections::HashMap;
7
8/// Security headers builder
9pub struct SecurityHeadersBuilder {
10    headers: HashMap<String, String>,
11}
12
13impl SecurityHeadersBuilder {
14    /// Create a new security headers builder with default headers
15    pub fn new() -> Self {
16        let mut headers = HashMap::new();
17
18        // Prevent clickjacking
19        headers.insert(
20            "X-Frame-Options".to_string(),
21            "DENY".to_string(),
22        );
23
24        // Prevent MIME type sniffing
25        headers.insert(
26            "X-Content-Type-Options".to_string(),
27            "nosniff".to_string(),
28        );
29
30        // Enable XSS protection (for older browsers)
31        headers.insert(
32            "X-XSS-Protection".to_string(),
33            "1; mode=block".to_string(),
34        );
35
36        // Referrer policy
37        headers.insert(
38            "Referrer-Policy".to_string(),
39            "strict-origin-when-cross-origin".to_string(),
40        );
41
42        // Permissions policy (formerly Feature-Policy)
43        headers.insert(
44            "Permissions-Policy".to_string(),
45            "geolocation=(), microphone=(), camera=()".to_string(),
46        );
47
48        // Strict Transport Security (HSTS)
49        headers.insert(
50            "Strict-Transport-Security".to_string(),
51            "max-age=31536000; includeSubDomains".to_string(),
52        );
53
54        // Content Security Policy (CSP)
55        headers.insert(
56            "Content-Security-Policy".to_string(),
57            "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:".to_string(),
58        );
59
60        Self { headers }
61    }
62
63    /// Add a custom header
64    pub fn add_header(&mut self, name: &str, value: &str) -> &mut Self {
65        self.headers.insert(name.to_string(), value.to_string());
66        self
67    }
68
69    /// Remove a header
70    pub fn remove_header(&mut self, name: &str) -> &mut Self {
71        self.headers.remove(name);
72        self
73    }
74
75    /// Get all headers
76    pub fn build(&self) -> HashMap<String, String> {
77        self.headers.clone()
78    }
79
80    /// Get a specific header
81    pub fn get_header(&self, name: &str) -> Option<&str> {
82        self.headers.get(name).map(|s| s.as_str())
83    }
84
85    /// Check if a header is set
86    pub fn has_header(&self, name: &str) -> bool {
87        self.headers.contains_key(name)
88    }
89}
90
91impl Default for SecurityHeadersBuilder {
92    fn default() -> Self {
93        Self::new()
94    }
95}
96
97/// Validate security headers
98pub struct SecurityHeadersValidator;
99
100impl SecurityHeadersValidator {
101    /// Check if required security headers are present
102    pub fn validate(headers: &HashMap<String, String>) -> Result<(), Vec<String>> {
103        let mut missing = Vec::new();
104
105        let required_headers = vec![
106            "X-Frame-Options",
107            "X-Content-Type-Options",
108            "Referrer-Policy",
109            "Strict-Transport-Security",
110        ];
111
112        for header in required_headers {
113            if !headers.contains_key(header) {
114                missing.push(format!("Missing required header: {}", header));
115            }
116        }
117
118        if missing.is_empty() {
119            Ok(())
120        } else {
121            Err(missing)
122        }
123    }
124
125    /// Check if a header value is secure
126    pub fn is_secure_header(name: &str, value: &str) -> bool {
127        match name {
128            "X-Frame-Options" => {
129                value == "DENY" || value == "SAMEORIGIN"
130            }
131            "X-Content-Type-Options" => {
132                value == "nosniff"
133            }
134            "Referrer-Policy" => {
135                matches!(
136                    value,
137                    "no-referrer"
138                        | "no-referrer-when-downgrade"
139                        | "same-origin"
140                        | "origin"
141                        | "strict-origin"
142                        | "origin-when-cross-origin"
143                        | "strict-origin-when-cross-origin"
144                        | "unsafe-url"
145                )
146            }
147            "Strict-Transport-Security" => {
148                value.contains("max-age=") && value.contains("31536000")
149            }
150            "Content-Security-Policy" => {
151                !value.contains("unsafe-inline") || value.contains("'unsafe-inline'")
152            }
153            _ => true,
154        }
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn test_security_headers_builder_default() {
164        let builder = SecurityHeadersBuilder::new();
165        let headers = builder.build();
166
167        assert!(headers.contains_key("X-Frame-Options"));
168        assert!(headers.contains_key("X-Content-Type-Options"));
169        assert!(headers.contains_key("Referrer-Policy"));
170        assert!(headers.contains_key("Strict-Transport-Security"));
171    }
172
173    #[test]
174    fn test_security_headers_builder_add_header() {
175        let mut builder = SecurityHeadersBuilder::new();
176        builder.add_header("Custom-Header", "custom-value");
177
178        let headers = builder.build();
179        assert_eq!(headers.get("Custom-Header"), Some(&"custom-value".to_string()));
180    }
181
182    #[test]
183    fn test_security_headers_builder_remove_header() {
184        let mut builder = SecurityHeadersBuilder::new();
185        builder.remove_header("X-Frame-Options");
186
187        let headers = builder.build();
188        assert!(!headers.contains_key("X-Frame-Options"));
189    }
190
191    #[test]
192    fn test_security_headers_builder_get_header() {
193        let builder = SecurityHeadersBuilder::new();
194        assert_eq!(builder.get_header("X-Frame-Options"), Some("DENY"));
195    }
196
197    #[test]
198    fn test_security_headers_builder_has_header() {
199        let builder = SecurityHeadersBuilder::new();
200        assert!(builder.has_header("X-Frame-Options"));
201        assert!(!builder.has_header("Non-Existent-Header"));
202    }
203
204    #[test]
205    fn test_security_headers_validator_validate() {
206        let mut headers = HashMap::new();
207        headers.insert("X-Frame-Options".to_string(), "DENY".to_string());
208        headers.insert("X-Content-Type-Options".to_string(), "nosniff".to_string());
209        headers.insert("Referrer-Policy".to_string(), "strict-origin-when-cross-origin".to_string());
210        headers.insert("Strict-Transport-Security".to_string(), "max-age=31536000".to_string());
211
212        let result = SecurityHeadersValidator::validate(&headers);
213        assert!(result.is_ok());
214    }
215
216    #[test]
217    fn test_security_headers_validator_missing_headers() {
218        let headers = HashMap::new();
219        let result = SecurityHeadersValidator::validate(&headers);
220        assert!(result.is_err());
221        let errors = result.unwrap_err();
222        assert!(!errors.is_empty());
223    }
224
225    #[test]
226    fn test_security_headers_validator_is_secure_header() {
227        assert!(SecurityHeadersValidator::is_secure_header("X-Frame-Options", "DENY"));
228        assert!(SecurityHeadersValidator::is_secure_header("X-Frame-Options", "SAMEORIGIN"));
229        assert!(!SecurityHeadersValidator::is_secure_header("X-Frame-Options", "ALLOW-FROM"));
230
231        assert!(SecurityHeadersValidator::is_secure_header("X-Content-Type-Options", "nosniff"));
232        assert!(!SecurityHeadersValidator::is_secure_header("X-Content-Type-Options", "sniff"));
233
234        assert!(SecurityHeadersValidator::is_secure_header("Referrer-Policy", "no-referrer"));
235        assert!(SecurityHeadersValidator::is_secure_header("Referrer-Policy", "strict-origin-when-cross-origin"));
236    }
237
238    #[test]
239    fn test_security_headers_builder_default_values() {
240        let builder = SecurityHeadersBuilder::new();
241        assert_eq!(builder.get_header("X-Frame-Options"), Some("DENY"));
242        assert_eq!(builder.get_header("X-Content-Type-Options"), Some("nosniff"));
243        assert!(builder.get_header("Strict-Transport-Security").unwrap().contains("31536000"));
244    }
245}