Skip to main content

static_web_server/
security_headers.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// This file is part of Static Web Server.
3// See https://static-web-server.net/ for more information
4// Copyright (C) 2019-present Jose Quintana <joseluisq.net>
5
6//! The module provides several HTTP security headers support.
7//!
8
9use http::header::{
10    CONTENT_SECURITY_POLICY, STRICT_TRANSPORT_SECURITY, X_CONTENT_TYPE_OPTIONS, X_FRAME_OPTIONS,
11};
12use hyper::{Request, Response, header::HeaderValue};
13
14use crate::body::Body;
15use crate::{Error, handler::RequestHandlerOpts};
16
17// Pre-computed static header values to avoid per-response parsing
18static HSTS_VALUE: HeaderValue =
19    HeaderValue::from_static("max-age=63072000; includeSubDomains; preload");
20static XFO_VALUE: HeaderValue = HeaderValue::from_static("DENY");
21static XCTO_VALUE: HeaderValue = HeaderValue::from_static("nosniff");
22static CSP_VALUE: HeaderValue = HeaderValue::from_static("frame-ancestors 'self'");
23static RP_VALUE: HeaderValue = HeaderValue::from_static("strict-origin-when-cross-origin");
24
25pub(crate) fn init(enabled: bool, handler_opts: &mut RequestHandlerOpts) {
26    handler_opts.security_headers = enabled;
27    tracing::info!(enabled, "security headers");
28}
29
30/// Appends security headers to a response if necessary
31pub(crate) fn post_process<T>(
32    opts: &RequestHandlerOpts,
33    _req: &Request<T>,
34    mut resp: Response<Body>,
35) -> Result<Response<Body>, Error> {
36    if opts.security_headers {
37        append_headers(&mut resp);
38    }
39    Ok(resp)
40}
41
42/// It appends security headers like `Strict-Transport-Security: max-age=63072000; includeSubDomains; preload` (2 years max-age),
43///`X-Frame-Options: DENY` and `Content-Security-Policy: frame-ancestors 'self'`.
44pub fn append_headers(resp: &mut Response<Body>) {
45    // Strict-Transport-Security (HSTS)
46    resp.headers_mut()
47        .insert(STRICT_TRANSPORT_SECURITY, HSTS_VALUE.clone());
48
49    // X-Frame-Options
50    resp.headers_mut()
51        .insert(X_FRAME_OPTIONS, XFO_VALUE.clone());
52
53    // X-Content-Type-Options
54    resp.headers_mut()
55        .insert(X_CONTENT_TYPE_OPTIONS, XCTO_VALUE.clone());
56
57    // Content Security Policy (CSP)
58    resp.headers_mut()
59        .insert(CONTENT_SECURITY_POLICY, CSP_VALUE.clone());
60
61    // Referrer-Policy (RP)
62    resp.headers_mut()
63        .insert("referrer-policy", RP_VALUE.clone());
64}
65
66#[cfg(test)]
67mod tests {
68    use http::header::{
69        CONTENT_SECURITY_POLICY, STRICT_TRANSPORT_SECURITY, X_CONTENT_TYPE_OPTIONS, X_FRAME_OPTIONS,
70    };
71    use hyper::{Response, StatusCode};
72
73    use super::append_headers;
74
75    #[test]
76    fn append_headers_sets_hsts() {
77        let mut resp = Response::new(crate::body::empty());
78        *resp.status_mut() = StatusCode::OK;
79        append_headers(&mut resp);
80        let hsts = resp.headers().get(STRICT_TRANSPORT_SECURITY).unwrap();
81        assert_eq!(
82            hsts.to_str().unwrap(),
83            "max-age=63072000; includeSubDomains; preload"
84        );
85    }
86
87    #[test]
88    fn append_headers_sets_x_frame_options() {
89        let mut resp = Response::new(crate::body::empty());
90        *resp.status_mut() = StatusCode::OK;
91        append_headers(&mut resp);
92        let xfo = resp.headers().get(X_FRAME_OPTIONS).unwrap();
93        assert_eq!(xfo.to_str().unwrap(), "DENY");
94    }
95
96    #[test]
97    fn append_headers_sets_x_content_type_options() {
98        let mut resp = Response::new(crate::body::empty());
99        *resp.status_mut() = StatusCode::OK;
100        append_headers(&mut resp);
101        let xcto = resp.headers().get(X_CONTENT_TYPE_OPTIONS).unwrap();
102        assert_eq!(xcto.to_str().unwrap(), "nosniff");
103    }
104
105    #[test]
106    fn append_headers_sets_csp() {
107        let mut resp = Response::new(crate::body::empty());
108        *resp.status_mut() = StatusCode::OK;
109        append_headers(&mut resp);
110        let csp = resp.headers().get(CONTENT_SECURITY_POLICY).unwrap();
111        assert_eq!(csp.to_str().unwrap(), "frame-ancestors 'self'");
112    }
113
114    #[test]
115    fn append_headers_sets_all_four_headers() {
116        let mut resp = Response::new(crate::body::empty());
117        append_headers(&mut resp);
118        assert!(resp.headers().contains_key(STRICT_TRANSPORT_SECURITY));
119        assert!(resp.headers().contains_key(X_FRAME_OPTIONS));
120        assert!(resp.headers().contains_key(X_CONTENT_TYPE_OPTIONS));
121        assert!(resp.headers().contains_key(CONTENT_SECURITY_POLICY));
122    }
123
124    #[test]
125    fn append_headers_sets_referrer_policy() {
126        let mut resp = Response::new(crate::body::empty());
127        *resp.status_mut() = StatusCode::OK;
128        append_headers(&mut resp);
129        let rp = resp.headers().get("referrer-policy").unwrap();
130        assert_eq!(rp.to_str().unwrap(), "strict-origin-when-cross-origin");
131    }
132}