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}