initial wip

master
Atrox 2021-12-22 19:42:00 +01:00
commit 3c558d9e5b
20 changed files with 1056 additions and 0 deletions

2
.gitignore vendored 100644
View File

@ -0,0 +1,2 @@
/target
Cargo.lock

19
Cargo.toml 100644
View File

@ -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 <hello@atrox.dev>"]
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"

22
LICENSE 100644
View File

@ -0,0 +1,22 @@
The MIT License
Copyright (c) 2021 Atrox <hello@atrox.dev>
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.

40
README.md 100644
View File

@ -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());
```

View File

@ -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<HeaderValue, InvalidHeaderValue> {
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::<Vec<String>>()
.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:");
}
}

View File

@ -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, InvalidHeaderValue> {
HeaderValue::from_str("require-corp")
}
}

View File

@ -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, InvalidHeaderValue> {
HeaderValue::from_str(self.to_string().as_str())
}
}

View File

@ -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, InvalidHeaderValue> {
HeaderValue::from_str(self.to_string().as_str())
}
}

View File

@ -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<String>,
}
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<HeaderValue, InvalidHeaderValue> {
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())
}
}

31
src/header/mod.rs 100644
View File

@ -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,
};

View File

@ -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, InvalidHeaderValue> {
HeaderValue::from_str("?1")
}
}

View File

@ -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<ReferrerPolicyValue>);
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<HeaderValue, InvalidHeaderValue> {
let s: Vec<String> = self.0.iter().map(|v| v.to_string()).collect();
HeaderValue::from_str(s.join(",").as_str())
}
}

View File

@ -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<HeaderValue, InvalidHeaderValue> {
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())
}
}

View File

@ -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, InvalidHeaderValue> {
HeaderValue::from_str("nosniff")
}
}

View File

@ -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, InvalidHeaderValue> {
HeaderValue::from_str(if self.0 { "on" } else { "off" })
}
}

View File

@ -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, InvalidHeaderValue> {
HeaderValue::from_str("noopen")
}
}

View File

@ -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, InvalidHeaderValue> {
HeaderValue::from_str(self.to_string().as_str())
}
}

View File

@ -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, InvalidHeaderValue> {
HeaderValue::from_str(self.to_string().as_str())
}
}

View File

@ -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, InvalidHeaderValue> {
HeaderValue::from_str("0")
}
}

364
src/lib.rs 100644
View File

@ -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<HeaderValue, InvalidHeaderValue>;
}
/// HelmetLayer
pub struct HelmetLayer<'a> {
content_security_policy: Option<ContentSecurityPolicy<'a>>,
cross_origin_embedder_policy: Option<CrossOriginEmbedderPolicy>,
cross_origin_opener_policy: Option<CrossOriginOpenerPolicy>,
cross_origin_resource_policy: Option<CrossOriginResourcePolicy>,
expect_ct: Option<ExpectCt>,
origin_agent_cluster: Option<OriginAgentCluster>,
referrer_policy: Option<ReferrerPolicy>,
strict_transport_security: Option<StrictTransportSecurity>,
x_content_type_options: Option<XContentTypeOptions>,
x_dns_prefetch_control: Option<XDnsPrefetchControl>,
x_download_options: Option<XDownloadOptions>,
x_frame_options: Option<XFrameOptions>,
x_permitted_cross_domain_policies: Option<XPermittedCrossDomainPolicies>,
x_xss_protection: Option<XXSSProtection>,
}
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<S> for HelmetLayer<'a> {
type Service = HelmetService<S>;
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<S> {
inner: S,
headers: HeaderMap,
}
impl<ReqBody, ResBody, S> Service<Request<ReqBody>> for HelmetService<S>
where
S: Service<Request<ReqBody>, Response = Response<ResBody>>,
{
type Response = S::Response;
type Error = S::Error;
type Future = ResponseFuture<S::Future>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}
fn call(&mut self, request: Request<ReqBody>) -> Self::Future {
ResponseFuture {
future: self.inner.call(request),
headers: self.headers.clone(),
}
}
}
pin_project! {
/// Response future for [`HelmetService`].
#[derive(Debug)]
pub struct ResponseFuture<F> {
#[pin]
future: F,
headers: HeaderMap,
}
}
impl<F, ResBody, E> Future for ResponseFuture<F>
where
F: Future<Output = Result<Response<ResBody>, E>>,
{
type Output = F::Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let mut res: Response<ResBody> = 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))
}
}