Skip to main content

actix_web_helmet/
lib.rs

1//! `actix-web-helmet` is a middleware for securing your Actix-Web application with various HTTP headers.
2//!
3//! `actix_web_helmet::Helmet` is a middleware that can be used to set various HTTP headers that can help protect your app from well-known web vulnerabilities.
4//!
5//! It is based on the [Helmet](https://helmetjs.github.io/) middleware for Express.js.
6//!
7//! # Usage
8//!
9//! ```no_run
10//! use actix_web::{web, App, HttpServer, Responder, get};
11//! use actix_web_helmet::{Helmet, HelmetMiddleware};
12//!
13//! #[get("/")]
14//! async fn index() -> impl Responder {
15//!   "Hello, World!"
16//! }
17//!
18//! #[actix_web::main]
19//! async fn main() -> std::io::Result<()> {
20//!  let helmet: HelmetMiddleware = Helmet::default().try_into().expect("valid headers");
21//!  HttpServer::new(move || App::new().wrap(helmet.clone()).service(index))
22//!      .bind(("127.0.0.1", 8080))?
23//!      .run()
24//!      .await
25//! }
26//! ```
27//!
28//! By default Helmet will set the following headers:
29//!
30//! ```text
31//! Content-Security-Policy: default-src 'self'; base-uri 'self'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src 'self'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; upgrade-insecure-requests
32//! Cross-Origin-Opener-Policy: same-origin
33//! Cross-Origin-Resource-Policy: same-origin
34//! Origin-Agent-Cluster: ?1
35//! Referrer-Policy: no-referrer
36//! Strict-Transport-Security: max-age=15552000; includeSubDomains
37//! X-Content-Type-Options: nosniff
38//! X-DNS-Prefetch-Control: off
39//! X-Download-Options: noopen
40//! X-Frame-Options: sameorigin
41//! X-Permitted-Cross-Domain-Policies: none
42//! X-XSS-Protection: 0
43//! ```
44//!
45//! This might be a good starting point for most users, but it is highly recommended to spend some time with the documentation for each header, and adjust them to your needs.
46//!
47//! # Configuration
48//!
49//! By default if you construct a new instance of `Helmet` it will not set any headers.
50//!
51//! It is possible to configure `Helmet` to set only the headers you want, by using the `add` method.
52//!
53//! ```no_run
54//! use actix_web::{get, web, App, HttpServer, Responder};
55//! use actix_web_helmet::{Helmet, HelmetMiddleware, ContentSecurityPolicy, CrossOriginOpenerPolicy};
56//!
57//! #[get("/")]
58//! async fn index() -> impl Responder {
59//!     "Hello, World!"
60//! }
61//!
62//! #[actix_web::main]
63//! async fn main() -> std::io::Result<()> {
64//!     let helmet: HelmetMiddleware = Helmet::new()
65//!         .add(
66//!             ContentSecurityPolicy::new()
67//!                 .child_src(vec!["'self'"])
68//!                 .child_src(vec!["'self'", "https://youtube.com"])
69//!                 .connect_src(vec!["'self'", "https://youtube.com"])
70//!                 .default_src(vec!["'self'", "https://youtube.com"])
71//!                 .font_src(vec!["'self'", "https://youtube.com"]),
72//!         )
73//!         .add(CrossOriginOpenerPolicy::same_origin_allow_popups())
74//!         .try_into()
75//!         .expect("valid headers");
76//!
77//!     HttpServer::new(move || {
78//!         App::new().wrap(helmet.clone()).service(index)
79//!     })
80//!     .bind(("127.0.0.1", 8080))?
81//!     .run()
82//!     .await
83//! }
84//! ```
85use std::future::Future;
86use std::pin::Pin;
87
88use actix_web::dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform};
89use actix_web::http::header::{HeaderName, HeaderValue};
90use actix_web::Error;
91use futures::future::{ok, Ready};
92
93use helmet_core::Helmet as HelmetCore;
94
95// re-export helmet_core::*, except for the `Helmet` struct
96pub use helmet_core::*;
97
98/// Helmet header configuration wrapper.
99///
100/// Use `Helmet::default()` for a sensible set of default security headers,
101/// or `Helmet::new()` to start with no headers and add only the ones you need.
102///
103/// Convert to [`HelmetMiddleware`] via `try_into()` to use as actix-web middleware.
104///
105/// ```rust
106/// use actix_web_helmet::{Helmet, HelmetMiddleware};
107///
108/// let mw: HelmetMiddleware = Helmet::default().try_into().unwrap();
109/// ```
110#[derive(Default)]
111pub struct Helmet(HelmetCore);
112
113impl Helmet {
114    /// Create a new instance of `Helmet` with no headers set.
115    pub fn new() -> Self {
116        Self(HelmetCore::new())
117    }
118
119    /// Add a header.
120    #[allow(clippy::should_implement_trait)]
121    pub fn add(self, header: impl Into<helmet_core::Header>) -> Self {
122        Self(self.0.add(header))
123    }
124
125    pub fn into_middleware(self) -> Result<HelmetMiddleware, HelmetError> {
126        self.try_into()
127    }
128}
129
130/// The actix-web middleware created by converting a [`Helmet`] configuration.
131#[derive(Clone)]
132pub struct HelmetMiddleware {
133    headers: Vec<(HeaderName, HeaderValue)>,
134}
135
136impl TryFrom<Helmet> for HelmetMiddleware {
137    type Error = HelmetError;
138
139    fn try_from(helmet: Helmet) -> Result<Self, Self::Error> {
140        let mut headers = Vec::new();
141        for header in helmet.0.headers.iter() {
142            let name = HeaderName::from_bytes(header.0.as_bytes())
143                .map_err(|_| HelmetError::InvalidHeaderName(header.0.to_string()))?;
144            let value = HeaderValue::from_str(&header.1)
145                .map_err(|_| HelmetError::InvalidHeaderValue(header.1.clone()))?;
146            headers.push((name, value));
147        }
148        Ok(Self { headers })
149    }
150}
151
152pub struct HelmetService<S> {
153    headers: Vec<(HeaderName, HeaderValue)>,
154    service: S,
155}
156
157impl<S, B> Transform<S, ServiceRequest> for HelmetMiddleware
158where
159    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
160    S::Future: 'static,
161    B: 'static,
162{
163    type Response = ServiceResponse<B>;
164    type Error = Error;
165    type InitError = ();
166    type Transform = HelmetService<S>;
167    type Future = Ready<Result<Self::Transform, Self::InitError>>;
168
169    fn new_transform(&self, service: S) -> Self::Future {
170        ok(HelmetService {
171            headers: self.headers.clone(),
172            service,
173        })
174    }
175}
176
177type LocalBoxFuture<T> = Pin<Box<dyn Future<Output = T> + 'static>>;
178
179impl<S, B> Service<ServiceRequest> for HelmetService<S>
180where
181    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
182    S::Future: 'static,
183    B: 'static,
184{
185    type Response = ServiceResponse<B>;
186    type Error = Error;
187    type Future = LocalBoxFuture<Result<Self::Response, Self::Error>>;
188
189    forward_ready!(service);
190
191    fn call(&self, req: ServiceRequest) -> Self::Future {
192        let fut = self.service.call(req);
193        let headers = self.headers.clone();
194
195        Box::pin(async move {
196            let mut res = fut.await?;
197
198            for (name, value) in &headers {
199                res.headers_mut().insert(name.clone(), value.clone());
200            }
201            Ok(res)
202        })
203    }
204}
205
206#[cfg(test)]
207mod tests {
208    use actix_web::http::header::{HeaderName, HeaderValue};
209    use actix_web::{http, test, web, App, HttpResponse};
210
211    use super::*;
212
213    #[actix_web::test]
214    async fn test_helmet() {
215        let app = test::init_service(
216            App::new()
217                .wrap(
218                    Helmet::new()
219                        .add(ContentSecurityPolicy::new().child_src(vec!["'self'"]))
220                        .into_middleware()
221                        .unwrap(),
222                )
223                .route("/", web::get().to(|| async { HttpResponse::Ok().finish() })),
224        )
225        .await;
226
227        let req = test::TestRequest::get().uri("/").to_request();
228        let res = test::call_service(&app, req).await;
229
230        assert!(res.status().is_success());
231        assert_eq!(
232            res.headers()
233                .get(HeaderName::from_static("content-security-policy")),
234            Some(&HeaderValue::from_static("child-src 'self'"))
235        );
236    }
237
238    #[actix_web::test]
239    async fn test_helmet_default() {
240        let helmet: HelmetMiddleware = Helmet::default().try_into().unwrap();
241
242        let app = test::init_service(
243            App::new()
244                .wrap(helmet)
245                .route("/", web::get().to(|| async { HttpResponse::Ok().finish() })),
246        )
247        .await;
248
249        let req = test::TestRequest::get().uri("/").to_request();
250        let resp = test::call_service(&app, req).await;
251
252        assert!(resp.status().is_success());
253        assert_eq!(
254            resp.headers().get(http::header::X_FRAME_OPTIONS),
255            Some(&HeaderValue::from_static("SAMEORIGIN"))
256        );
257        assert_eq!(
258            resp.headers().get(http::header::X_XSS_PROTECTION),
259            Some(&HeaderValue::from_static("0"))
260        );
261    }
262}