Skip to main content

actix_cloud/
security.rs

1//! Middleware to add security headers to every response.
2//!
3//! Wrap the app with [`SecurityHeader::default().build()`](SecurityHeader::build) and
4//! customize fields or enable HSTS (see [`SecurityHeader::set_default_hsts`]) as needed.
5use std::fmt::Display;
6
7use actix_web::middleware;
8
9/// Value of the `Referrer-Policy` header.
10#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11#[derive(Clone, Debug)]
12pub enum RefererPolicy {
13    NoReferrer,
14    NoReferrerWhenDowngrade,
15    Origin,
16    OriginWhenCrossOrigin,
17    SameOrigin,
18    StrictOrigin,
19    StrictOriginWhenCrossOrigin,
20    UnsafeUrl,
21}
22
23impl Display for RefererPolicy {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        f.write_str(match self {
26            RefererPolicy::NoReferrer => "no-referrer",
27            RefererPolicy::NoReferrerWhenDowngrade => "no-referrer-when-downgrade",
28            RefererPolicy::Origin => "origin",
29            RefererPolicy::OriginWhenCrossOrigin => "origin-when-cross-origin",
30            RefererPolicy::SameOrigin => "same-origin",
31            RefererPolicy::StrictOrigin => "strict-origin",
32            RefererPolicy::StrictOriginWhenCrossOrigin => "strict-origin-when-cross-origin",
33            RefererPolicy::UnsafeUrl => "unsafe-url",
34        })
35    }
36}
37
38/// Value of the `X-Frame-Options` header.
39#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
40#[derive(Clone, Debug)]
41pub enum XFrameOptions {
42    Deny,
43    SameOrigin,
44}
45
46impl Display for XFrameOptions {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        f.write_str(match self {
49            XFrameOptions::Deny => "DENY",
50            XFrameOptions::SameOrigin => "SAMEORIGIN",
51        })
52    }
53}
54
55/// Value of the `Cross-Origin-Opener-Policy` header.
56#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
57#[derive(Clone, Debug)]
58pub enum CrossOriginOpenerPolicy {
59    UnsafeNone,
60    SameOriginAllowPopups,
61    SameOrigin,
62}
63
64impl Display for CrossOriginOpenerPolicy {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        f.write_str(match self {
67            CrossOriginOpenerPolicy::UnsafeNone => "unsafe-none",
68            CrossOriginOpenerPolicy::SameOriginAllowPopups => "same-origin-allow-popups",
69            CrossOriginOpenerPolicy::SameOrigin => "same-origin",
70        })
71    }
72}
73
74/// Value of the `Strict-Transport-Security` (HSTS) header. The payload is `max-age` in
75/// seconds; variants add the `includeSubDomains` / `preload` directives.
76#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
77#[derive(Clone, Debug)]
78pub enum StrictTransportSecurity {
79    MaxAge(u32),
80    IncludeSubDomains(u32),
81    Preload(u32),
82}
83
84impl Display for StrictTransportSecurity {
85    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86        match self {
87            StrictTransportSecurity::MaxAge(x) => f.write_str(&format!("max-age={}", x)),
88            StrictTransportSecurity::IncludeSubDomains(x) => {
89                f.write_str(&format!("max-age={}; includeSubDomains", x))
90            }
91            StrictTransportSecurity::Preload(x) => {
92                f.write_str(&format!("max-age={}; includeSubDomains; preload", x))
93            }
94        }
95    }
96}
97
98/// Security headers applied to every response by [`SecurityHeader::build`].
99#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
100#[derive(Clone, Debug)]
101pub struct SecurityHeader {
102    pub referer_policy: RefererPolicy,
103    pub x_frame_options: XFrameOptions,
104    pub cross_origin_opener_policy: CrossOriginOpenerPolicy,
105    pub content_security_policy: String,
106    pub strict_transport_security: Option<StrictTransportSecurity>,
107}
108
109impl Default for SecurityHeader {
110    fn default() -> Self {
111        Self {
112            referer_policy: RefererPolicy::StrictOriginWhenCrossOrigin,
113            x_frame_options: XFrameOptions::Deny,
114            cross_origin_opener_policy: CrossOriginOpenerPolicy::SameOrigin,
115            content_security_policy: String::from("default-src 'none'; script-src 'none'; object-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'"),
116            strict_transport_security: None,
117        }
118    }
119}
120
121impl SecurityHeader {
122    /// Set default HSTS to 1 year, includeSubDomains and preload.
123    ///
124    /// `max-age=31536000; includeSubDomains; preload`
125    pub fn set_default_hsts(&mut self) {
126        self.strict_transport_security = Some(StrictTransportSecurity::Preload(31536000));
127    }
128
129    /// Build the actix-web [`DefaultHeaders`](middleware::DefaultHeaders) middleware
130    /// carrying all configured security headers (plus HSTS when set).
131    pub fn build(self) -> middleware::DefaultHeaders {
132        let mut ret = middleware::DefaultHeaders::new()
133            .add(("X-Content-Type-Options", "nosniff"))
134            .add(("Referrer-Policy", self.referer_policy.to_string()))
135            .add(("X-Frame-Options", self.x_frame_options.to_string()))
136            .add((
137                "Cross-Origin-Opener-Policy",
138                self.cross_origin_opener_policy.to_string(),
139            ))
140            .add(("Content-Security-Policy", self.content_security_policy));
141        if let Some(hsts) = self.strict_transport_security {
142            ret = ret.add(("Strict-Transport-Security", hsts.to_string()));
143        }
144        ret
145    }
146}