From 3c558d9e5ba25eba7f057c2d7d8fbdbaede0516d Mon Sep 17 00:00:00 2001 From: Atrox Date: Wed, 22 Dec 2021 19:42:00 +0100 Subject: [PATCH] initial wip --- .gitignore | 2 + Cargo.toml | 19 + LICENSE | 22 ++ README.md | 40 ++ src/header/content_security_policy.rs | 135 +++++++ src/header/cross_origin_embedder_policy.rs | 23 ++ src/header/cross_origin_opener_policy.rs | 40 ++ src/header/cross_origin_resource_policy.rs | 40 ++ src/header/expect_ct.rs | 45 +++ src/header/mod.rs | 31 ++ src/header/origin_agent_cluster.rs | 23 ++ src/header/referrer_policy.rs | 51 +++ src/header/strict_transport_security.rs | 46 +++ src/header/x_content_type_options.rs | 24 ++ src/header/x_dns_prefetch_control.rs | 21 + src/header/x_download_options.rs | 24 ++ src/header/x_frame_options.rs | 41 ++ .../x_permitted_cross_domain_policies.rs | 42 ++ src/header/x_xss_protection.rs | 23 ++ src/lib.rs | 364 ++++++++++++++++++ 20 files changed, 1056 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/header/content_security_policy.rs create mode 100644 src/header/cross_origin_embedder_policy.rs create mode 100644 src/header/cross_origin_opener_policy.rs create mode 100644 src/header/cross_origin_resource_policy.rs create mode 100644 src/header/expect_ct.rs create mode 100644 src/header/mod.rs create mode 100644 src/header/origin_agent_cluster.rs create mode 100644 src/header/referrer_policy.rs create mode 100644 src/header/strict_transport_security.rs create mode 100644 src/header/x_content_type_options.rs create mode 100644 src/header/x_dns_prefetch_control.rs create mode 100644 src/header/x_download_options.rs create mode 100644 src/header/x_frame_options.rs create mode 100644 src/header/x_permitted_cross_domain_policies.rs create mode 100644 src/header/x_xss_protection.rs create mode 100644 src/lib.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6222840 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "tower-helmet" +description = "Helps with securing your tower servers with various HTTP headers " +version = "0.1.0" +authors = ["Atrox "] +edition = "2018" +license = "MIT" +repository = "https://github.com/atrox/tower-helmet" +homepage = "https://github.com/atrox/tower-helmet" +categories = ["asynchronous", "network-programming", "web-programming"] +keywords = ["http", "tower", "security", "service", "header"] + +[dependencies] +futures = "0.3.18" +http = "0.2.5" +pin-project-lite = "0.2.7" +tower-layer = "0.3.1" +tower-service = "0.3.1" +lazy_static = "1.4.0" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fd6d91d --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +The MIT License + +Copyright (c) 2021 Atrox + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f03ae84 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# tower-helmet + +this is still very **work in progress** + +a port of the beautiful [helmet.js](https://github.com/helmetjs/helmet) in the javascript world. + +`tower-helmet` helps you secure your tower server by setting various HTTP headers. _It's not a silver bullet_, but it can help! + +You can find a list of all available headers under the [header] module. By default (with [HelmetLayer::default]) **all of them** are enabled. +Please take a good look at [ContentSecurityPolicy]. Most of the time you will need to adapt this one to your needs. + +# Examples + +```rust +use tower_helmet::header::{ContentSecurityPolicy, ExpectCt, XFrameOptions}; +use tower_helmet::HelmetLayer; + +// default layer with all security headers active +let layer = HelmetLayer::default(); + +// default layer with customizations applied +let mut directives = HashMap::new(); +directives.insert("default-src", vec!["'self'", "https://example.com"]); +directives.insert("img-src", vec!["'self'", "data:", "https://example.com"]); +directives.insert("script-src", vec!["'self'", "'unsafe-inline'", "https://example.com"]); +let csp = ContentSecurityPolicy { + directives, + ..Default::default() +}; + +let layer = HelmetLayer::default() + .disable_strict_transport_security() + .disable_cross_origin_embedder_policy() + .content_security_policy(csp); + +// completely blank layer, selectively enable and add headers +let layer = HelmetLayer::new() + .x_frame_options(XFrameOptions::SameOrigin) + .expect_ct(ExpectCt::default()); +``` diff --git a/src/header/content_security_policy.rs b/src/header/content_security_policy.rs new file mode 100644 index 0000000..cf90134 --- /dev/null +++ b/src/header/content_security_policy.rs @@ -0,0 +1,135 @@ +use crate::IntoHeader; +use http::header::{HeaderName, InvalidHeaderValue}; +use http::HeaderValue; +use lazy_static::lazy_static; +use std::collections::HashMap; + +lazy_static! { + static ref DEFAULT_DIRECTIVES: HashMap<&'static str, Vec<&'static str>> = { + let mut m = HashMap::new(); + m.insert("default-src", vec!["'self'"]); + m.insert("base-uri", vec!["'self'"]); + m.insert("block-all-mixed-content", vec![]); + m.insert("font-src", vec!["'self'", "https:", "data:"]); + m.insert("frame-ancestors", vec!["'self'"]); + m.insert("img-src", vec!["'self'", "data:"]); + m.insert("object-src", vec!["'none'"]); + m.insert("script-src", vec!["'self'"]); + m.insert("script-src-attr", vec!["'none'"]); + m.insert("style-src", vec!["'self'", "https:", "'unsafe-inline'"]); + m.insert("upgrade-insecure-requests", vec![]); + m + }; +} + +/// `ContentSecurityPolicy` sets the `Content-Security-Policy` header which helps mitigate cross-site scripting attacks, among other things. +/// See [MDN's introductory article on Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP). +/// +/// This middleware performs very little validation. You should rely on CSP checkers like [CSP Evaluator](https://csp-evaluator.withgoogle.com/) instead. +/// +/// If no directive is supplied and `use_defaults` is `true`, the following policy is set (whitespace added for readability): +/// ```text +/// default-src 'self'; +/// base-uri 'self'; +/// block-all-mixed-content; +/// font-src 'self' https: data:; +/// 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 +/// ``` +/// +/// Examples: +/// TODO +pub struct ContentSecurityPolicy<'a> { + pub use_defaults: bool, + /// Each key is the directive name in kebab case (such as `default-src`). + /// Each value is a vector of strings for that directive + pub directives: HashMap<&'a str, Vec<&'a str>>, + /// If `true`, [the `Content-Security-Policy-Report-Only` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only) will be set instead. + pub report_only: bool, +} + +impl ContentSecurityPolicy<'static> { + /// Returns the default directives + /// + /// ```text + /// default-src 'self'; + /// base-uri 'self'; + /// block-all-mixed-content; + /// font-src 'self' https: data:; + /// 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 + /// ``` + pub fn default_directives() -> &'static HashMap<&'static str, Vec<&'static str>> { + &DEFAULT_DIRECTIVES + } +} + +impl<'a> Default for ContentSecurityPolicy<'a> { + fn default() -> Self { + ContentSecurityPolicy { + use_defaults: true, + directives: HashMap::new(), + report_only: false, + } + } +} + +impl<'a> IntoHeader for ContentSecurityPolicy<'a> { + fn header_name(&self) -> HeaderName { + if self.report_only { + http::header::CONTENT_SECURITY_POLICY_REPORT_ONLY + } else { + http::header::CONTENT_SECURITY_POLICY + } + } + + fn header_value(&self) -> Result { + let directives = if self.use_defaults { + if self.directives.is_empty() { + DEFAULT_DIRECTIVES.clone() + } else { + let mut directives = DEFAULT_DIRECTIVES.clone(); + directives.extend(self.directives.clone().into_iter()); + + directives + } + } else { + self.directives.clone() + }; + + let header = directives + .iter() + .map(|(key, values)| format!("{} {}", key, values.join(" "))) + .collect::>() + .join("; "); + + HeaderValue::from_str(header.trim()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_wow() { + for (k, v) in DEFAULT_DIRECTIVES.iter() { + println!("{}:{:?}", k, v) + } + + let csp = ContentSecurityPolicy::default(); + + assert_eq!(csp.header_name(), "Content-Security-Policy"); + assert_eq!(csp.header_value().unwrap(), "frame-ancestors 'self'; object-src 'none'; style-src 'self' https: 'unsafe-inline'; default-src 'self'; img-src 'self' data:; script-src-attr 'none'; upgrade-insecure-requests ; script-src 'self'; block-all-mixed-content ; base-uri 'self'; font-src 'self' https: data:"); + } +} diff --git a/src/header/cross_origin_embedder_policy.rs b/src/header/cross_origin_embedder_policy.rs new file mode 100644 index 0000000..5cb212a --- /dev/null +++ b/src/header/cross_origin_embedder_policy.rs @@ -0,0 +1,23 @@ +use crate::IntoHeader; +use http::header::{HeaderName, InvalidHeaderValue}; +use http::HeaderValue; + +/// `CrossOriginEmbedderPolicy` sets the `Cross-Origin-Embedder-Policy` header to `require-corp`. +/// See [MDN's article on this header](https://developer.cdn.mozilla.net/en-US/docs/Web/HTTP/Headers/Cross-Origin-Embedder-Policy) for more. +pub struct CrossOriginEmbedderPolicy; + +impl Default for CrossOriginEmbedderPolicy { + fn default() -> Self { + CrossOriginEmbedderPolicy + } +} + +impl IntoHeader for CrossOriginEmbedderPolicy { + fn header_name(&self) -> HeaderName { + HeaderName::from_static("cross-origin-embedder-policy") + } + + fn header_value(&self) -> Result { + HeaderValue::from_str("require-corp") + } +} diff --git a/src/header/cross_origin_opener_policy.rs b/src/header/cross_origin_opener_policy.rs new file mode 100644 index 0000000..3c8428a --- /dev/null +++ b/src/header/cross_origin_opener_policy.rs @@ -0,0 +1,40 @@ +use crate::IntoHeader; +use http::header::{HeaderName, InvalidHeaderValue}; +use http::HeaderValue; +use std::fmt::{Display, Formatter}; + +/// `CrossOriginOpenerPolicy` sets the `Cross-Origin-Opener-Policy` header. +/// For more, see [MDN's article on this header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Opener-Policy). +pub enum CrossOriginOpenerPolicy { + UnsafeNone, + SameOriginAllowPopups, + SameOrigin, +} + +impl Display for CrossOriginOpenerPolicy { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let s = match self { + CrossOriginOpenerPolicy::UnsafeNone => "unsafe-none", + CrossOriginOpenerPolicy::SameOriginAllowPopups => "same-origin-allow-popups", + CrossOriginOpenerPolicy::SameOrigin => "same-origin", + }; + + write!(f, "{}", s) + } +} + +impl Default for CrossOriginOpenerPolicy { + fn default() -> Self { + CrossOriginOpenerPolicy::SameOrigin + } +} + +impl IntoHeader for CrossOriginOpenerPolicy { + fn header_name(&self) -> HeaderName { + HeaderName::from_static("cross-origin-opener-policy") + } + + fn header_value(&self) -> Result { + HeaderValue::from_str(self.to_string().as_str()) + } +} diff --git a/src/header/cross_origin_resource_policy.rs b/src/header/cross_origin_resource_policy.rs new file mode 100644 index 0000000..2ca2cae --- /dev/null +++ b/src/header/cross_origin_resource_policy.rs @@ -0,0 +1,40 @@ +use crate::IntoHeader; +use http::header::{HeaderName, InvalidHeaderValue}; +use http::HeaderValue; +use std::fmt::{Display, Formatter}; + +/// `CrossOriginResourcePolicy` sets the `Cross-Origin-Resource-Policy` header. +/// For more, see ["Consider deploying Cross-Origin Resource Policy](https://resourcepolicy.fyi/) and [MDN's article on this header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy). +pub enum CrossOriginResourcePolicy { + SameSite, + SameOrigin, + CrossOrigin, +} + +impl Display for CrossOriginResourcePolicy { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let s = match self { + CrossOriginResourcePolicy::SameSite => "same-site", + CrossOriginResourcePolicy::SameOrigin => "same-origin", + CrossOriginResourcePolicy::CrossOrigin => "cross-origin", + }; + + write!(f, "{}", s) + } +} + +impl Default for CrossOriginResourcePolicy { + fn default() -> Self { + CrossOriginResourcePolicy::SameOrigin + } +} + +impl IntoHeader for CrossOriginResourcePolicy { + fn header_name(&self) -> HeaderName { + HeaderName::from_static("cross-origin-resource-policy") + } + + fn header_value(&self) -> Result { + HeaderValue::from_str(self.to_string().as_str()) + } +} diff --git a/src/header/expect_ct.rs b/src/header/expect_ct.rs new file mode 100644 index 0000000..50f545d --- /dev/null +++ b/src/header/expect_ct.rs @@ -0,0 +1,45 @@ +use crate::IntoHeader; +use http::header::{HeaderName, InvalidHeaderValue}; +use http::HeaderValue; +use std::time::Duration; + +/// `ExpectCt` sets the `Expect-CT` header which helps mitigate misissued SSL certificates. +/// See [MDN's article on Certificate Transparency](https://developer.mozilla.org/en-US/docs/Web/Security/Certificate_Transparency) and the [`Expect-CT` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect-CT) for more. +pub struct ExpectCt { + /// `max_age` is the number of seconds to expect Certificate Transparency. + pub max_age: Duration, + /// If `true`, the user agent (usually a browser) should refuse future connections that violate its Certificate Transparency policy. + pub enforce: bool, + /// If set, complying user agents will report Certificate Transparency failures to this URL. + pub report_uri: Option, +} + +impl Default for ExpectCt { + fn default() -> Self { + ExpectCt { + max_age: Duration::from_secs(0), + enforce: false, + report_uri: None, + } + } +} + +impl IntoHeader for ExpectCt { + fn header_name(&self) -> HeaderName { + HeaderName::from_static("expect-ct") + } + + fn header_value(&self) -> Result { + let mut directives = vec![format!("max-age={}", self.max_age.as_secs())]; + + if self.enforce { + directives.push("enforce".to_owned()); + } + + if let Some(report_uri) = self.report_uri.as_ref() { + directives.push(format!("report-uri={}", report_uri)); + } + + HeaderValue::from_str(directives.join(", ").as_str()) + } +} diff --git a/src/header/mod.rs b/src/header/mod.rs new file mode 100644 index 0000000..93e90af --- /dev/null +++ b/src/header/mod.rs @@ -0,0 +1,31 @@ +mod content_security_policy; +mod cross_origin_embedder_policy; +mod cross_origin_opener_policy; +mod cross_origin_resource_policy; +mod expect_ct; +mod origin_agent_cluster; +mod referrer_policy; +mod strict_transport_security; +mod x_content_type_options; +mod x_dns_prefetch_control; +mod x_download_options; +mod x_frame_options; +mod x_permitted_cross_domain_policies; +mod x_xss_protection; + +pub use self::{ + content_security_policy::ContentSecurityPolicy, + cross_origin_embedder_policy::CrossOriginEmbedderPolicy, + cross_origin_opener_policy::CrossOriginOpenerPolicy, + cross_origin_resource_policy::CrossOriginResourcePolicy, + expect_ct::ExpectCt, + origin_agent_cluster::OriginAgentCluster, + referrer_policy::{ReferrerPolicy, ReferrerPolicyValue}, + strict_transport_security::StrictTransportSecurity, + x_content_type_options::XContentTypeOptions, + x_dns_prefetch_control::XDnsPrefetchControl, + x_download_options::XDownloadOptions, + x_frame_options::XFrameOptions, + x_permitted_cross_domain_policies::XPermittedCrossDomainPolicies, + x_xss_protection::XXSSProtection, +}; diff --git a/src/header/origin_agent_cluster.rs b/src/header/origin_agent_cluster.rs new file mode 100644 index 0000000..631bc19 --- /dev/null +++ b/src/header/origin_agent_cluster.rs @@ -0,0 +1,23 @@ +use crate::IntoHeader; +use http::header::{HeaderName, InvalidHeaderValue}; +use http::HeaderValue; + +/// `OriginAgentCluster` sets the `Origin-Agent-Cluster` header, which provides a mechanism to allow web applications to isolate their origins. +/// Read more about it [in the spec](https://whatpr.org/html/6214/origin.html#origin-keyed-agent-clusters). +pub struct OriginAgentCluster; + +impl Default for OriginAgentCluster { + fn default() -> Self { + OriginAgentCluster + } +} + +impl IntoHeader for OriginAgentCluster { + fn header_name(&self) -> HeaderName { + HeaderName::from_static("origin-agent-cluster") + } + + fn header_value(&self) -> Result { + HeaderValue::from_str("?1") + } +} diff --git a/src/header/referrer_policy.rs b/src/header/referrer_policy.rs new file mode 100644 index 0000000..de483ba --- /dev/null +++ b/src/header/referrer_policy.rs @@ -0,0 +1,51 @@ +use crate::IntoHeader; +use http::header::{HeaderName, InvalidHeaderValue}; +use http::HeaderValue; +use std::fmt::{Display, Formatter}; + +/// `ReferrerPolicy` sets the `Referrer-Policy` header which controls what information is set in [the `Referer` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer). +/// See ["Referer header: privacy and security concerns"](https://developer.mozilla.org/en-US/docs/Web/Security/Referer_header:_privacy_and_security_concerns) and [the header's documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy) on MDN for more. +pub struct ReferrerPolicy(pub Vec); + +pub enum ReferrerPolicyValue { + NoReferrer, + NoReferrerWhenDowngrade, + Origin, + OriginWhenCrossOrigin, + SameOrigin, + StrictOrigin, + StrictOriginWhenCrossOrigin, +} + +impl Display for ReferrerPolicyValue { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let s = match self { + ReferrerPolicyValue::NoReferrer => "no-referrer", + ReferrerPolicyValue::NoReferrerWhenDowngrade => "no-referrer-when-downgrade", + ReferrerPolicyValue::Origin => "origin", + ReferrerPolicyValue::OriginWhenCrossOrigin => "origin-when-cross-origin", + ReferrerPolicyValue::SameOrigin => "same-origin", + ReferrerPolicyValue::StrictOrigin => "strict-origin", + ReferrerPolicyValue::StrictOriginWhenCrossOrigin => "strict-origin-when-cross-origin", + }; + + write!(f, "{}", s) + } +} + +impl Default for ReferrerPolicy { + fn default() -> Self { + ReferrerPolicy(vec![ReferrerPolicyValue::NoReferrer]) + } +} + +impl IntoHeader for ReferrerPolicy { + fn header_name(&self) -> HeaderName { + http::header::REFERRER_POLICY + } + + fn header_value(&self) -> Result { + let s: Vec = self.0.iter().map(|v| v.to_string()).collect(); + HeaderValue::from_str(s.join(",").as_str()) + } +} diff --git a/src/header/strict_transport_security.rs b/src/header/strict_transport_security.rs new file mode 100644 index 0000000..46f9965 --- /dev/null +++ b/src/header/strict_transport_security.rs @@ -0,0 +1,46 @@ +use crate::IntoHeader; +use http::header::{HeaderName, InvalidHeaderValue}; +use http::HeaderValue; +use std::time::Duration; + +/// `StrictTransportSecurity` sets the `Strict-Transport-Security` header which tells browsers to prefer HTTPS over insecure HTTP. +/// See [the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security) for more. +pub struct StrictTransportSecurity { + /// `max_age` is the number of seconds browsers should remember to prefer HTTPS. It defaults to `15552000`, which is 180 days. + pub max_age: Duration, + /// `include_subdomains` dictates whether to include the `includeSubDomains` directive, which makes this policy extend to subdomains. It defaults to `true`. + pub include_subdomains: bool, + /// If true, it adds the `preload` directive, expressing intent to add your HSTS policy to browsers. + /// See [the "Preloading Strict Transport Security" section on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security#Preloading_Strict_Transport_Security) for more. + pub preload: bool, +} + +impl Default for StrictTransportSecurity { + fn default() -> Self { + StrictTransportSecurity { + max_age: Duration::from_secs(15552000), + include_subdomains: true, + preload: false, + } + } +} + +impl IntoHeader for StrictTransportSecurity { + fn header_name(&self) -> HeaderName { + http::header::STRICT_TRANSPORT_SECURITY + } + + fn header_value(&self) -> Result { + let mut directives = vec![format!("max-age={}", self.max_age.as_secs())]; + + if self.include_subdomains { + directives.push("includeSubdomains".to_owned()); + } + + if self.preload { + directives.push("preload".to_owned()); + } + + HeaderValue::from_str(directives.join("; ").as_str()) + } +} diff --git a/src/header/x_content_type_options.rs b/src/header/x_content_type_options.rs new file mode 100644 index 0000000..f577515 --- /dev/null +++ b/src/header/x_content_type_options.rs @@ -0,0 +1,24 @@ +use crate::IntoHeader; +use http::header::{HeaderName, InvalidHeaderValue}; +use http::HeaderValue; + +/// `XContentTypeOptions` sets the `X-Content-Type-Options` header to `nosniff`. +/// This mitigates [MIME type sniffing](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#MIME_sniffing) which can cause security vulnerabilities. +/// See [documentation for this header on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options) for more. +pub struct XContentTypeOptions; + +impl Default for XContentTypeOptions { + fn default() -> Self { + XContentTypeOptions + } +} + +impl IntoHeader for XContentTypeOptions { + fn header_name(&self) -> HeaderName { + http::header::X_CONTENT_TYPE_OPTIONS + } + + fn header_value(&self) -> Result { + HeaderValue::from_str("nosniff") + } +} diff --git a/src/header/x_dns_prefetch_control.rs b/src/header/x_dns_prefetch_control.rs new file mode 100644 index 0000000..352325e --- /dev/null +++ b/src/header/x_dns_prefetch_control.rs @@ -0,0 +1,21 @@ +use crate::IntoHeader; +use http::header::{HeaderName, InvalidHeaderValue}; +use http::HeaderValue; + +/// `XDnsPrefetchControl` sets the `X-DNS-Prefetch-Control` header to help control DNS prefetching, which can improve user privacy at the expense of performance. +/// See [documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-DNS-Prefetch-Control) for more. +#[derive(Default)] +pub struct XDnsPrefetchControl( + /// Is indictating whether to enable DNS prefetching. + pub bool, +); + +impl IntoHeader for XDnsPrefetchControl { + fn header_name(&self) -> HeaderName { + http::header::X_DNS_PREFETCH_CONTROL + } + + fn header_value(&self) -> Result { + HeaderValue::from_str(if self.0 { "on" } else { "off" }) + } +} diff --git a/src/header/x_download_options.rs b/src/header/x_download_options.rs new file mode 100644 index 0000000..f0c845b --- /dev/null +++ b/src/header/x_download_options.rs @@ -0,0 +1,24 @@ +use crate::IntoHeader; +use http::header::{HeaderName, InvalidHeaderValue}; +use http::HeaderValue; + +/// `XDownloadOptions` sets the `X-Download-Options` header, which is specific to Internet Explorer 8. +/// It forces potentially-unsafe downloads to be saved, mitigating execution of HTML in your site's context. +/// For more, see [this old post on MSDN](https://docs.microsoft.com/en-us/archive/blogs/ie/ie8-security-part-v-comprehensive-protection). +pub struct XDownloadOptions; + +impl Default for XDownloadOptions { + fn default() -> Self { + XDownloadOptions + } +} + +impl IntoHeader for XDownloadOptions { + fn header_name(&self) -> HeaderName { + HeaderName::from_static("x-download-options") + } + + fn header_value(&self) -> Result { + HeaderValue::from_str("noopen") + } +} diff --git a/src/header/x_frame_options.rs b/src/header/x_frame_options.rs new file mode 100644 index 0000000..a4e7bc9 --- /dev/null +++ b/src/header/x_frame_options.rs @@ -0,0 +1,41 @@ +use crate::IntoHeader; +use http::header::{HeaderName, InvalidHeaderValue}; +use http::HeaderValue; +use std::fmt::{Display, Formatter}; + +/// `XFrameOptions` sets the `X-Frame-Options` header to help you mitigate [clickjacking attacks](https://en.wikipedia.org/wiki/Clickjacking). +/// This header is superseded by [the `frame-ancestors` Content Security Policy directive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors) but is still useful on old browsers. +/// For more, see `helmet.contentSecurityPolicy`, as well as [the documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options). +/// +/// `DENY` or `SAMEORIGIN`. (A legacy directive, `ALLOW-FROM`, is not supported by this crate. [Read more here.](https://github.com/helmetjs/helmet/wiki/How-to-use-X%E2%80%93Frame%E2%80%93Options's-%60ALLOW%E2%80%93FROM%60-directive)) +pub enum XFrameOptions { + Deny, + SameOrigin, +} + +impl Display for XFrameOptions { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let s = match self { + XFrameOptions::SameOrigin => "SAMEORIGIN", + XFrameOptions::Deny => "DENY", + }; + + write!(f, "{}", s) + } +} + +impl Default for XFrameOptions { + fn default() -> Self { + XFrameOptions::SameOrigin + } +} + +impl IntoHeader for XFrameOptions { + fn header_name(&self) -> HeaderName { + http::header::X_FRAME_OPTIONS + } + + fn header_value(&self) -> Result { + HeaderValue::from_str(self.to_string().as_str()) + } +} diff --git a/src/header/x_permitted_cross_domain_policies.rs b/src/header/x_permitted_cross_domain_policies.rs new file mode 100644 index 0000000..8eb36b7 --- /dev/null +++ b/src/header/x_permitted_cross_domain_policies.rs @@ -0,0 +1,42 @@ +use crate::IntoHeader; +use http::header::{HeaderName, InvalidHeaderValue}; +use http::HeaderValue; +use std::fmt::{Display, Formatter}; + +/// `XPermittedCrossDomainPolicies` sets the `X-Permitted-Cross-Domain-Policies` header, which tells some clients (mostly Adobe products) your domain's policy for loading cross-domain content. +/// See [the description on OWASP](https://owasp.org/www-project-secure-headers/) for more. +pub enum XPermittedCrossDomainPolicies { + None, + MasterOnly, + ByContentType, + All, +} + +impl Display for XPermittedCrossDomainPolicies { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + let s = match self { + XPermittedCrossDomainPolicies::None => "none", + XPermittedCrossDomainPolicies::MasterOnly => "master-only", + XPermittedCrossDomainPolicies::ByContentType => "by-content-type", + XPermittedCrossDomainPolicies::All => "all", + }; + + write!(f, "{}", s) + } +} + +impl Default for XPermittedCrossDomainPolicies { + fn default() -> Self { + XPermittedCrossDomainPolicies::None + } +} + +impl IntoHeader for XPermittedCrossDomainPolicies { + fn header_name(&self) -> HeaderName { + HeaderName::from_static("x-permitted-cross-domain-policies") + } + + fn header_value(&self) -> Result { + HeaderValue::from_str(self.to_string().as_str()) + } +} diff --git a/src/header/x_xss_protection.rs b/src/header/x_xss_protection.rs new file mode 100644 index 0000000..aed0d6d --- /dev/null +++ b/src/header/x_xss_protection.rs @@ -0,0 +1,23 @@ +use crate::IntoHeader; +use http::header::{HeaderName, InvalidHeaderValue}; +use http::HeaderValue; + +/// `XXSSProtection` disables browsers' buggy cross-site scripting filter by setting the `X-XSS-Protection` header to `0`. +/// See [discussion about disabling the header here](https://github.com/helmetjs/helmet/issues/230) and [documentation on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection). +pub struct XXSSProtection; + +impl Default for XXSSProtection { + fn default() -> Self { + XXSSProtection + } +} + +impl IntoHeader for XXSSProtection { + fn header_name(&self) -> HeaderName { + http::header::X_XSS_PROTECTION + } + + fn header_value(&self) -> Result { + HeaderValue::from_str("0") + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..8c5465e --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,364 @@ +//! # Overview +//! +//! `tower-helmet` helps you secure your tower server by setting various HTTP headers. _It's not a silver bullet_, but it can help! +//! +//! You can find a list of all available headers under the [header] module. By default (with [HelmetLayer::default]) **all of them** are enabled. +//! Please take a good look at [ContentSecurityPolicy]. Most of the time you will need to adapt this one to your needs. +//! +//! # Examples +//! +//! ``` +//! use tower_helmet::header::{ContentSecurityPolicy, ExpectCt, XFrameOptions}; +//! use tower_helmet::HelmetLayer; +//! +//! // default layer with all security headers active +//! let layer = HelmetLayer::default(); +//! +//! // default layer with customizations applied +//! let mut directives = HashMap::new(); +//! directives.insert("default-src", vec!["'self'", "https://example.com"]); +//! directives.insert("img-src", vec!["'self'", "data:", "https://example.com"]); +//! directives.insert("script-src", vec!["'self'", "'unsafe-inline'", "https://example.com"]); +//! let csp = ContentSecurityPolicy { +//! directives, +//! ..Default::default() +//! }; +//! +//! let layer = HelmetLayer::default() +//! .disable_strict_transport_security() +//! .disable_cross_origin_embedder_policy() +//! .content_security_policy(csp); +//! +//! // completely blank layer, selectively enable and add headers +//! let layer = HelmetLayer::new() +//! .x_frame_options(XFrameOptions::SameOrigin) +//! .expect_ct(ExpectCt::default()); +//! ``` +pub mod header; + +use futures::ready; +use http::header::{HeaderName, InvalidHeaderValue}; +use http::{HeaderMap, HeaderValue, Request, Response}; +use pin_project_lite::pin_project; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; +use tower_layer::Layer; +use tower_service::Service; + +use header::{ + ContentSecurityPolicy, CrossOriginEmbedderPolicy, CrossOriginOpenerPolicy, + CrossOriginResourcePolicy, ExpectCt, OriginAgentCluster, ReferrerPolicy, + StrictTransportSecurity, XContentTypeOptions, XDnsPrefetchControl, XDownloadOptions, + XFrameOptions, XPermittedCrossDomainPolicies, XXSSProtection, +}; + +trait IntoHeader { + fn header_name(&self) -> HeaderName; + fn header_value(&self) -> Result; +} + +/// HelmetLayer +pub struct HelmetLayer<'a> { + content_security_policy: Option>, + cross_origin_embedder_policy: Option, + cross_origin_opener_policy: Option, + cross_origin_resource_policy: Option, + expect_ct: Option, + origin_agent_cluster: Option, + referrer_policy: Option, + strict_transport_security: Option, + x_content_type_options: Option, + x_dns_prefetch_control: Option, + x_download_options: Option, + x_frame_options: Option, + x_permitted_cross_domain_policies: Option, + x_xss_protection: Option, +} + +impl<'a> Default for HelmetLayer<'a> { + fn default() -> Self { + HelmetLayer { + content_security_policy: Some(ContentSecurityPolicy::default()), + cross_origin_embedder_policy: Some(CrossOriginEmbedderPolicy::default()), + cross_origin_opener_policy: Some(CrossOriginOpenerPolicy::default()), + cross_origin_resource_policy: Some(CrossOriginResourcePolicy::default()), + expect_ct: Some(ExpectCt::default()), + origin_agent_cluster: Some(OriginAgentCluster::default()), + referrer_policy: Some(ReferrerPolicy::default()), + strict_transport_security: Some(StrictTransportSecurity::default()), + x_content_type_options: Some(XContentTypeOptions::default()), + x_dns_prefetch_control: Some(XDnsPrefetchControl::default()), + x_download_options: Some(XDownloadOptions::default()), + x_frame_options: Some(XFrameOptions::default()), + x_permitted_cross_domain_policies: Some(XPermittedCrossDomainPolicies::default()), + x_xss_protection: Some(XXSSProtection::default()), + } + } +} + +impl<'a> HelmetLayer<'a> { + pub fn new() -> Self { + HelmetLayer { + content_security_policy: None, + cross_origin_embedder_policy: None, + cross_origin_opener_policy: None, + cross_origin_resource_policy: None, + expect_ct: None, + origin_agent_cluster: None, + referrer_policy: None, + strict_transport_security: None, + x_content_type_options: None, + x_dns_prefetch_control: None, + x_download_options: None, + x_frame_options: None, + x_permitted_cross_domain_policies: None, + x_xss_protection: None, + } + } + + pub fn recommended_defaults() -> Self { + HelmetLayer::default() + } + + pub fn content_security_policy(&mut self, v: ContentSecurityPolicy<'a>) -> &mut Self { + self.content_security_policy = Some(v); + self + } + pub fn disable_content_security_policy(&mut self) -> &mut Self { + self.content_security_policy = None; + self + } + + pub fn cross_origin_embedder_policy(&mut self, v: CrossOriginEmbedderPolicy) -> &mut Self { + self.cross_origin_embedder_policy = Some(v); + self + } + pub fn disable_cross_origin_embedder_policy(&mut self) -> &mut Self { + self.cross_origin_embedder_policy = None; + self + } + + pub fn cross_origin_opener_policy(&mut self, v: CrossOriginOpenerPolicy) -> &mut Self { + self.cross_origin_opener_policy = Some(v); + self + } + pub fn disable_cross_origin_opener_policy(&mut self) -> &mut Self { + self.cross_origin_opener_policy = None; + self + } + + pub fn cross_origin_resource_policy(&mut self, v: CrossOriginResourcePolicy) -> &mut Self { + self.cross_origin_resource_policy = Some(v); + self + } + pub fn disable_cross_origin_resource_policy(&mut self) -> &mut Self { + self.cross_origin_resource_policy = None; + self + } + + pub fn expect_ct(&mut self, v: ExpectCt) -> &mut Self { + self.expect_ct = Some(v); + self + } + pub fn disable_expect_ct(&mut self) -> &mut Self { + self.expect_ct = None; + self + } + + pub fn origin_agent_cluster(&mut self, v: OriginAgentCluster) -> &mut Self { + self.origin_agent_cluster = Some(v); + self + } + pub fn disable_origin_agent_cluster(&mut self) -> &mut Self { + self.origin_agent_cluster = None; + self + } + + pub fn referrer_policy(&mut self, v: ReferrerPolicy) -> &mut Self { + self.referrer_policy = Some(v); + self + } + pub fn disable_referrer_policy(&mut self) -> &mut Self { + self.referrer_policy = None; + self + } + + pub fn strict_transport_security(&mut self, v: StrictTransportSecurity) -> &mut Self { + self.strict_transport_security = Some(v); + self + } + pub fn disable_strict_transport_security(&mut self) -> &mut Self { + self.strict_transport_security = None; + self + } + + pub fn x_content_type_options(&mut self, v: XContentTypeOptions) -> &mut Self { + self.x_content_type_options = Some(v); + self + } + pub fn disable_x_content_type_options(&mut self) -> &mut Self { + self.x_content_type_options = None; + self + } + + pub fn x_dns_prefetch_control(&mut self, v: XDnsPrefetchControl) -> &mut Self { + self.x_dns_prefetch_control = Some(v); + self + } + pub fn disable_x_dns_prefetch_control(&mut self) -> &mut Self { + self.x_dns_prefetch_control = None; + self + } + + pub fn x_download_options(&mut self, v: XDownloadOptions) -> &mut Self { + self.x_download_options = Some(v); + self + } + pub fn disable_x_download_options(&mut self) -> &mut Self { + self.x_download_options = None; + self + } + + pub fn x_frame_options(&mut self, v: XFrameOptions) -> &mut Self { + self.x_frame_options = Some(v); + self + } + pub fn disable_x_frame_options(&mut self) -> &mut Self { + self.x_frame_options = None; + self + } + + pub fn x_permitted_cross_domain_policies( + &mut self, + v: XPermittedCrossDomainPolicies, + ) -> &mut Self { + self.x_permitted_cross_domain_policies = Some(v); + self + } + pub fn disable_x_permitted_cross_domain_policies(&mut self) -> &mut Self { + self.x_permitted_cross_domain_policies = None; + self + } + + pub fn x_xss_protection(&mut self, v: XXSSProtection) -> &mut Self { + self.x_xss_protection = Some(v); + self + } + pub fn disable_x_xss_protection(&mut self) -> &mut Self { + self.x_xss_protection = None; + self + } +} + +impl<'a, S> Layer for HelmetLayer<'a> { + type Service = HelmetService; + + fn layer(&self, service: S) -> Self::Service { + let mut headers = HeaderMap::new(); + + if let Some(h) = &self.content_security_policy { + headers.insert(h.header_name(), h.header_value().unwrap()); + } + if let Some(h) = &self.cross_origin_embedder_policy { + headers.insert(h.header_name(), h.header_value().unwrap()); + } + if let Some(h) = &self.cross_origin_opener_policy { + headers.insert(h.header_name(), h.header_value().unwrap()); + } + if let Some(h) = &self.cross_origin_resource_policy { + headers.insert(h.header_name(), h.header_value().unwrap()); + } + if let Some(h) = &self.expect_ct { + headers.insert(h.header_name(), h.header_value().unwrap()); + } + if let Some(h) = &self.origin_agent_cluster { + headers.insert(h.header_name(), h.header_value().unwrap()); + } + if let Some(h) = &self.referrer_policy { + headers.insert(h.header_name(), h.header_value().unwrap()); + } + if let Some(h) = &self.strict_transport_security { + headers.insert(h.header_name(), h.header_value().unwrap()); + } + if let Some(h) = &self.x_content_type_options { + headers.insert(h.header_name(), h.header_value().unwrap()); + } + if let Some(h) = &self.x_dns_prefetch_control { + headers.insert(h.header_name(), h.header_value().unwrap()); + } + if let Some(h) = &self.x_download_options { + headers.insert(h.header_name(), h.header_value().unwrap()); + } + if let Some(h) = &self.x_frame_options { + headers.insert(h.header_name(), h.header_value().unwrap()); + } + if let Some(h) = &self.x_permitted_cross_domain_policies { + headers.insert(h.header_name(), h.header_value().unwrap()); + } + if let Some(h) = &self.x_xss_protection { + headers.insert(h.header_name(), h.header_value().unwrap()); + } + + HelmetService { + inner: service, + headers, + } + } +} + +#[derive(Clone)] +pub struct HelmetService { + inner: S, + headers: HeaderMap, +} + +impl Service> for HelmetService +where + S: Service, Response = Response>, +{ + type Response = S::Response; + type Error = S::Error; + type Future = ResponseFuture; + + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + fn call(&mut self, request: Request) -> Self::Future { + ResponseFuture { + future: self.inner.call(request), + headers: self.headers.clone(), + } + } +} + +pin_project! { + /// Response future for [`HelmetService`]. + #[derive(Debug)] + pub struct ResponseFuture { + #[pin] + future: F, + + headers: HeaderMap, + } +} + +impl Future for ResponseFuture +where + F: Future, E>>, +{ + type Output = F::Output; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + let mut res: Response = ready!(this.future.poll(cx)?); + let headers = res.headers_mut(); + + for (name, value) in this.headers.iter() { + headers.insert(name, value.clone()); + } + + Poll::Ready(Ok(res)) + } +}