owasp_headers/
lib.rs

1#![deny(clippy::all)]
2#![warn(missing_docs)]
3
4//! Modern web browsers may prevent or mitigate security vulnerabilities when they encounter the
5//! [HTTP response headers recommended by OWASP](https://owasp.org/www-project-secure-headers/).
6//!
7//! This crate offers these HTTP headers and their values,
8//! so that they may be more-conveniently used when developing web services in Rust.
9//!
10//! Example:
11//! ```
12//! use std::convert::Infallible;
13//! use http::response::Parts;
14//! use hyper::{Body, Request, Response};
15//!
16//! async fn handle(_req: Request<Body>) -> Result<Response<Body>, Infallible> {
17//!     let mut response = Response::new(Body::from("hello, world!"));
18//!     let mut headers = response.headers_mut();
19//!     headers.extend(owasp_headers::headers());
20//!     Ok(response)
21//! }
22//! ```
23//!
24//! If you are developing a web service using
25//! [`tower::Service`](https://docs.rs/tower/0.4.11/tower/trait.Service.html),
26//! you may find it even more convenient to apply these headers using
27//! [tower-default-headers](https://crates.io/crates/tower-default-headers).
28
29use http::header::{
30    HeaderMap, HeaderValue, CACHE_CONTROL, CONTENT_SECURITY_POLICY, PRAGMA, REFERRER_POLICY,
31    X_CONTENT_TYPE_OPTIONS, X_FRAME_OPTIONS,
32};
33
34/// produces an owned-collection of headers and their values
35pub fn headers() -> HeaderMap {
36    let mut h = HeaderMap::new();
37    h.reserve(13);
38    h.insert(
39        HTTP_STRICT_TRANSPORT_SECURITY,
40        HeaderValue::from_static(HTTP_STRICT_TRANSPORT_SECURITY_DEFAULT),
41    );
42    h.insert(
43        X_FRAME_OPTIONS,
44        HeaderValue::from_static(X_FRAME_OPTIONS_DEFAULT),
45    );
46    h.insert(
47        X_CONTENT_TYPE_OPTIONS,
48        HeaderValue::from_static(X_CONTENT_TYPE_OPTIONS_DEFAULT),
49    );
50    h.insert(
51        CONTENT_SECURITY_POLICY,
52        HeaderValue::from_static(CONTENT_SECURITY_POLICY_DEFAULT),
53    );
54    h.insert(
55        X_PERMITTED_CROSS_DOMAIN_POLICIES,
56        HeaderValue::from_static(X_PERMITTED_CROSS_DOMAIN_POLICIES_DEFAULT),
57    );
58    h.insert(
59        REFERRER_POLICY,
60        HeaderValue::from_static(REFERRER_POLICY_DEFAULT),
61    );
62    h.insert(
63        CLEAR_SITE_DATA,
64        HeaderValue::from_static(CLEAR_SITE_DATA_DEFAULT),
65    );
66    h.insert(
67        CROSS_ORIGIN_EMBEDDER_POLICY,
68        HeaderValue::from_static(CROSS_ORIGIN_EMBEDDER_POLICY_DEFAULT),
69    );
70    h.insert(
71        CROSS_ORIGIN_OPENER_POLICY,
72        HeaderValue::from_static(SAME_ORIGIN),
73    );
74    h.insert(
75        CROSS_ORIGIN_RESOURCE_POLICY,
76        HeaderValue::from_static(SAME_ORIGIN),
77    );
78    h.insert(
79        PERMISSIONS_POLICY,
80        HeaderValue::from_static(PERMISSIONS_POLICY_DEFAULT),
81    );
82    h.insert(
83        CACHE_CONTROL,
84        HeaderValue::from_static(CACHE_CONTROL_DEFAULT),
85    );
86    h.insert(PRAGMA, HeaderValue::from_static(PRAGMA_DEFAULT));
87    h
88}
89
90const CACHE_CONTROL_DEFAULT: &str = "no-store, max-age=0";
91const CLEAR_SITE_DATA: &str = "clear-site-data";
92const CLEAR_SITE_DATA_DEFAULT: &str = r#""cache","cookies","storage""#;
93const CONTENT_SECURITY_POLICY_DEFAULT: &str = "default-src 'self'; object-src 'none'; child-src 'self'; frame-ancestors 'none'; upgrade-insecure-requests; block-all-mixed-content";
94const CROSS_ORIGIN_EMBEDDER_POLICY: &str = "cross-origin-embedder-policy";
95const CROSS_ORIGIN_EMBEDDER_POLICY_DEFAULT: &str = "require-corp";
96const CROSS_ORIGIN_OPENER_POLICY: &str = "cross-origin-opener-policy";
97const CROSS_ORIGIN_RESOURCE_POLICY: &str = "cross-origin-resource-policy";
98const HTTP_STRICT_TRANSPORT_SECURITY: &str = "http-strict-transport-security";
99const HTTP_STRICT_TRANSPORT_SECURITY_DEFAULT: &str = "max-age=31536000 ; includeSubDomains";
100const PERMISSIONS_POLICY: &str = "permissions-policy";
101const PERMISSIONS_POLICY_DEFAULT: &str = "accelerometer=(),autoplay=(),camera=(),display-capture=(),document-domain=(),encrypted-media=(),fullscreen=(),geolocation=(),gyroscope=(),magnetometer=(),microphone=(),midi=(),payment=(),picture-in-picture=(),publickey-credentials-get=(),screen-wake-lock=(),sync-xhr=(self),usb=(),web-share=(),xr-spatial-tracking=()";
102const PRAGMA_DEFAULT: &str = "no-cache";
103const REFERRER_POLICY_DEFAULT: &str = "no-referrer";
104const SAME_ORIGIN: &str = "same-origin";
105const X_CONTENT_TYPE_OPTIONS_DEFAULT: &str = "nosniff";
106const X_FRAME_OPTIONS_DEFAULT: &str = "deny";
107const X_PERMITTED_CROSS_DOMAIN_POLICIES: &str = "x-permitted-cross-domain-policies";
108const X_PERMITTED_CROSS_DOMAIN_POLICIES_DEFAULT: &str = "none";
109
110#[cfg(test)]
111mod tests {
112    use std::{fs::read_to_string, path::Path};
113
114    use toml::value::Value;
115
116    use super::*;
117
118    #[test]
119    fn headers_returns_headermap_with_expected_len() {
120        let got = headers();
121        assert_eq!(got.len(), 13);
122    }
123
124    #[test]
125    fn headers_returns_headermap_with_expected_contents() {
126        let got = headers();
127
128        let fixture_path = Path::new("./fixtures/headers.toml");
129        let fixture_data =
130            read_to_string(fixture_path).expect("could not read fixtures/headers.toml");
131        let fixture = fixture_data.parse::<Value>().unwrap();
132
133        if let Value::Table(table) = fixture {
134            assert_eq!(table.len(), 13);
135            for (name, value) in table.iter() {
136                assert_eq!(got[name], value.as_str().unwrap());
137            }
138        } else {
139            panic!("unexpected TOML structure");
140        }
141    }
142}