Graduate 'helmet' as 'shield' into core.

The 'SpaceHelmet' fairing is now called 'Shield'. It features the
following changes and improvements:

  * Headers which are now ignored by browsers are removed.
  * 'XssFilter' is no longer an on-by-default policy.
  * A new 'Permission' policy is introduced.
  * 'Shield' is attached to all 'Rocket' instances by default.
  * Default headers never allocate on 'Clone'.
  * Policy headers are rendered once and cached at start-up.
  * Improved use of typed URIs in policy types.
This commit is contained in:
Sergio Benitez 2021-05-21 22:52:52 -07:00
parent 267cb9396f
commit f6a7087c84
15 changed files with 1437 additions and 879 deletions

View File

@ -23,7 +23,6 @@ databases = [
default = ["serve"]
tera_templates = ["tera", "templates"]
handlebars_templates = ["handlebars", "templates"]
helmet = ["time"]
serve = []
compression = ["brotli_compression", "gzip_compression"]
brotli_compression = ["brotli"]
@ -66,9 +65,6 @@ r2d2_sqlite = { version = "0.17", optional = true }
memcache = { version = "0.15", optional = true }
r2d2-memcache = { version = "0.6", optional = true }
# SpaceHelmet dependencies
time = { version = "0.2.9", optional = true }
# Compression dependencies
brotli = { version = "3.3", optional = true }
flate2 = { version = "1.0", optional = true }

View File

@ -1,215 +0,0 @@
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use rocket::{Rocket, Request, Response, Orbit};
use rocket::http::uncased::UncasedStr;
use rocket::fairing::{Fairing, Info, Kind};
use crate::helmet::*;
/// A [`Fairing`](../../rocket/fairing/trait.Fairing.html) that adds HTTP
/// headers to outgoing responses that control security features on the browser.
///
/// # Usage
///
/// To use `SpaceHelmet`, first construct an instance of it. To use the default
/// set of headers, construct with [`SpaceHelmet::default()`](#method.default).
/// For an instance with no preset headers, use [`SpaceHelmet::new()`]. To
/// enable an additional header, use [`enable()`](SpaceHelmet::enable()), and to
/// disable a header, use [`disable()`](SpaceHelmet::disable()):
///
/// ```rust
/// use rocket_contrib::helmet::SpaceHelmet;
/// use rocket_contrib::helmet::{XssFilter, ExpectCt};
///
/// // A `SpaceHelmet` with the default headers:
/// let helmet = SpaceHelmet::default();
///
/// // A `SpaceHelmet` with the default headers minus `XssFilter`:
/// let helmet = SpaceHelmet::default().disable::<XssFilter>();
///
/// // A `SpaceHelmet` with the default headers plus `ExpectCt`.
/// let helmet = SpaceHelmet::default().enable(ExpectCt::default());
///
/// // A `SpaceHelmet` with only `XssFilter` and `ExpectCt`.
/// let helmet = SpaceHelmet::default()
/// .enable(XssFilter::default())
/// .enable(ExpectCt::default());
/// ```
///
/// Then, attach the instance of `SpaceHelmet` to your application's instance of
/// `Rocket`:
///
/// ```rust
/// # extern crate rocket;
/// # extern crate rocket_contrib;
/// # use rocket_contrib::helmet::SpaceHelmet;
/// # let helmet = SpaceHelmet::default();
/// rocket::build()
/// // ...
/// .attach(helmet)
/// # ;
/// ```
///
/// The fairing will inject all enabled headers into all outgoing responses
/// _unless_ the response already contains a header with the same name. If it
/// does contain the header, a warning is emitted, and the header is not
/// overwritten.
///
/// # TLS and HSTS
///
/// If TLS is configured and enabled when the application is launched in a
/// non-development environment (e.g., staging or production), HSTS is
/// automatically enabled with its default policy and a warning is issued.
///
/// To get rid of this warning, explicitly [`enable()`](SpaceHelmet::enable())
/// an [`Hsts`] policy.
pub struct SpaceHelmet {
policies: HashMap<&'static UncasedStr, Box<dyn SubPolicy>>,
force_hsts: AtomicBool,
}
impl Default for SpaceHelmet {
/// Returns a new `SpaceHelmet` instance. See the [table] for a description
/// of the policies used by default.
///
/// [table]: ./#supported-headers
///
/// # Example
///
/// ```rust
/// # extern crate rocket;
/// # extern crate rocket_contrib;
/// use rocket_contrib::helmet::SpaceHelmet;
///
/// let helmet = SpaceHelmet::default();
/// ```
fn default() -> Self {
SpaceHelmet::new()
.enable(NoSniff::default())
.enable(Frame::default())
.enable(XssFilter::default())
}
}
impl SpaceHelmet {
/// Returns an instance of `SpaceHelmet` with no headers enabled.
///
/// # Example
///
/// ```rust
/// use rocket_contrib::helmet::SpaceHelmet;
///
/// let helmet = SpaceHelmet::new();
/// ```
pub fn new() -> Self {
SpaceHelmet {
policies: HashMap::new(),
force_hsts: AtomicBool::new(false),
}
}
/// Enables the policy header `policy`.
///
/// If the poliicy was previously enabled, the configuration is replaced
/// with that of `policy`.
///
/// # Example
///
/// ```rust
/// use rocket_contrib::helmet::SpaceHelmet;
/// use rocket_contrib::helmet::NoSniff;
///
/// let helmet = SpaceHelmet::new().enable(NoSniff::default());
/// ```
pub fn enable<P: Policy>(mut self, policy: P) -> Self {
self.policies.insert(P::NAME.into(), Box::new(policy));
self
}
/// Disables the policy header `policy`.
///
/// # Example
///
/// ```rust
/// use rocket_contrib::helmet::SpaceHelmet;
/// use rocket_contrib::helmet::NoSniff;
///
/// let helmet = SpaceHelmet::default().disable::<NoSniff>();
/// ```
pub fn disable<P: Policy>(mut self) -> Self {
self.policies.remove(UncasedStr::new(P::NAME));
self
}
/// Returns `true` if the policy `P` is enabled.
///
/// # Example
///
/// ```rust
/// use rocket_contrib::helmet::SpaceHelmet;
/// use rocket_contrib::helmet::{XssFilter, NoSniff, Frame};
/// use rocket_contrib::helmet::{Hsts, ExpectCt, Referrer};
///
/// let helmet = SpaceHelmet::default();
///
/// assert!(helmet.is_enabled::<XssFilter>());
/// assert!(helmet.is_enabled::<NoSniff>());
/// assert!(helmet.is_enabled::<Frame>());
///
/// assert!(!helmet.is_enabled::<Hsts>());
/// assert!(!helmet.is_enabled::<ExpectCt>());
/// assert!(!helmet.is_enabled::<Referrer>());
/// ```
pub fn is_enabled<P: Policy>(&self) -> bool {
self.policies.contains_key(UncasedStr::new(P::NAME))
}
/// Sets all of the headers in `self.policies` in `response` as long as the
/// header is not already in the response.
fn apply(&self, response: &mut Response<'_>) {
for policy in self.policies.values() {
let name = policy.name();
if response.headers().contains(name.as_str()) {
warn!("Space Helmet: response contains a '{}' header.", name);
warn_!("Refusing to overwrite existing header.");
continue
}
// FIXME: Cache the rendered header.
response.set_header(policy.header());
}
if self.force_hsts.load(Ordering::Relaxed) {
if !response.headers().contains(Hsts::NAME) {
response.set_header(&Hsts::default());
}
}
}
}
#[rocket::async_trait]
impl Fairing for SpaceHelmet {
fn info(&self) -> Info {
Info {
name: "Space Helmet",
kind: Kind::Liftoff | Kind::Response,
}
}
async fn on_liftoff(&self, rocket: &Rocket<Orbit>) {
if rocket.config().tls_enabled()
&& rocket.figment().profile() != rocket::Config::DEBUG_PROFILE
&& !self.is_enabled::<Hsts>()
{
warn_!("Space Helmet: deploying with TLS without enabling HSTS.");
warn_!("Enabling default HSTS policy.");
info_!("To disable this warning, configure an HSTS policy.");
self.force_hsts.store(true, Ordering::Relaxed);
}
}
async fn on_response<'r>(&self, _: &'r Request<'_>, res: &mut Response<'r>) {
self.apply(res);
}
}

View File

@ -1,436 +0,0 @@
//! Module containing the [`Policy`] trait and types that implement it.
use std::borrow::Cow;
use rocket::http::{Header, uri::Uri, uncased::UncasedStr};
use time::Duration;
/// Trait implemented by security and privacy policy headers.
///
/// Types that implement this trait can be [`enable()`]d and [`disable()`]d on
/// instances of [`SpaceHelmet`].
///
/// [`SpaceHelmet`]: crate::helmet::SpaceHelmet
/// [`enable()`]: crate::helmet::SpaceHelmet::enable()
/// [`disable()`]: crate::helmet::SpaceHelmet::disable()
pub trait Policy: Default + Send + Sync + 'static {
/// The actual name of the HTTP header.
///
/// This name must uniquely identify the header as it is used to determine
/// whether two implementations of `Policy` are for the same header. Use the
/// real HTTP header's name.
///
/// # Example
///
/// ```rust
/// # extern crate rocket;
/// # extern crate rocket_contrib;
/// # use rocket::http::Header;
/// use rocket_contrib::helmet::Policy;
///
/// #[derive(Default)]
/// struct MyPolicy;
///
/// impl Policy for MyPolicy {
/// const NAME: &'static str = "X-My-Policy";
/// # fn header(&self) -> Header<'static> { unimplemented!() }
/// }
/// ```
const NAME: &'static str;
/// Returns the [`Header`](../../rocket/http/struct.Header.html) to attach
/// to all outgoing responses.
///
/// # Example
///
/// ```rust
/// # extern crate rocket;
/// # extern crate rocket_contrib;
/// use rocket::http::Header;
/// use rocket_contrib::helmet::Policy;
///
/// #[derive(Default)]
/// struct MyPolicy;
///
/// impl Policy for MyPolicy {
/// # const NAME: &'static str = "X-My-Policy";
/// fn header(&self) -> Header<'static> {
/// Header::new(Self::NAME, "value-to-enable")
/// }
/// }
/// ```
fn header(&self) -> Header<'static>;
}
pub(crate) trait SubPolicy: Send + Sync {
fn name(&self) -> &'static UncasedStr;
fn header(&self) -> Header<'static>;
}
impl<P: Policy> SubPolicy for P {
fn name(&self) -> &'static UncasedStr {
P::NAME.into()
}
fn header(&self) -> Header<'static> {
Policy::header(self)
}
}
macro_rules! impl_policy {
($T:ty, $name:expr) => (
impl Policy for $T {
const NAME: &'static str = $name;
fn header(&self) -> Header<'static> {
self.into()
}
}
)
}
// Keep this in-sync with the top-level module docs.
impl_policy!(XssFilter, "X-XSS-Protection");
impl_policy!(NoSniff, "X-Content-Type-Options");
impl_policy!(Frame, "X-Frame-Options");
impl_policy!(Hsts, "Strict-Transport-Security");
impl_policy!(ExpectCt, "Expect-CT");
impl_policy!(Referrer, "Referrer-Policy");
impl_policy!(Prefetch, "X-DNS-Prefetch-Control");
/// The [Referrer-Policy] header: controls the value set by the browser for the
/// [Referer] header.
///
/// Tells the browser if it should send all or part of URL of the current page
/// to the next site the user navigates to via the [Referer] header. This can be
/// important for security as the URL itself might expose sensitive data, such
/// as a hidden file path or personal identifier.
///
/// [Referrer-Policy]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
/// [Referer]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer
pub enum Referrer {
/// Omits the `Referer` header (_SpaceHelmet default_).
NoReferrer,
/// Omits the `Referer` header on connection downgrade i.e. following HTTP
/// link from HTTPS site (_Browser default_).
NoReferrerWhenDowngrade,
/// Only send the origin of part of the URL, e.g. the origin of
/// `https://foo.com/bob.html` is `https://foo.com`.
Origin,
/// Send full URL for same-origin requests, only send origin part when
/// replying to [cross-origin] requests.
///
/// [cross-origin]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
OriginWhenCrossOrigin,
/// Send full URL for same-origin requests only.
SameOrigin,
/// Only send origin part of URL, only send if protocol security level
/// remains the same e.g. HTTPS to HTTPS.
StrictOrigin,
/// Send full URL for same-origin requests. For cross-origin requests, only
/// send origin part of URL if protocol security level remains the same e.g.
/// HTTPS to HTTPS.
StrictOriginWhenCrossOrigin,
/// Send full URL for same-origin or cross-origin requests. _This will leak
/// the full URL of TLS protected resources to insecure origins. Use with
/// caution._
UnsafeUrl,
}
/// Defaults to [`Referrer::NoReferrer`]. Tells the browser to omit the
/// `Referer` header.
impl Default for Referrer {
fn default() -> Referrer {
Referrer::NoReferrer
}
}
impl Into<Header<'static>> for &Referrer {
fn into(self) -> Header<'static> {
let policy_string = match self {
Referrer::NoReferrer => "no-referrer",
Referrer::NoReferrerWhenDowngrade => "no-referrer-when-downgrade",
Referrer::Origin => "origin",
Referrer::OriginWhenCrossOrigin => "origin-when-cross-origin",
Referrer::SameOrigin => "same-origin",
Referrer::StrictOrigin => "strict-origin",
Referrer::StrictOriginWhenCrossOrigin => "strict-origin-when-cross-origin",
Referrer::UnsafeUrl => "unsafe-url",
};
Header::new(Referrer::NAME, policy_string)
}
}
/// The [Expect-CT] header: enables [Certificate Transparency] to detect and
/// prevent misuse of TLS certificates.
///
/// [Certificate Transparency] solves a variety of problems with public TLS/SSL
/// certificate management and is valuable measure for all public applications.
/// If you're just [getting started] with certificate transparency, ensure that
/// your [site is in compliance][getting started] before you enable enforcement
/// with [`ExpectCt::Enforce`] or [`ExpectCt::ReportAndEnforce`]. Failure to do
/// so will result in the browser refusing to communicate with your application.
/// _You have been warned_.
///
/// [Expect-CT]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect-CT
/// [Certificate Transparency]: http://www.certificate-transparency.org/what-is-ct
/// [getting started]: http://www.certificate-transparency.org/getting-started
pub enum ExpectCt {
/// Enforce certificate compliance for the next [`Duration`]. Ensure that
/// your certificates are in compliance before turning on enforcement.
/// (_SpaceHelmet_ default).
Enforce(Duration),
/// Report to `Uri`, but do not enforce, compliance violations for the next
/// [`Duration`]. Doesn't provide any protection but is a good way make sure
/// things are working correctly before turning on enforcement in
/// production.
Report(Duration, Uri<'static>),
/// Enforce compliance and report violations to `Uri` for the next
/// [`Duration`].
ReportAndEnforce(Duration, Uri<'static>),
}
/// Defaults to [`ExpectCt::Enforce(Duration::days(30))`], enforce CT
/// compliance, see [draft] standard for more.
///
/// [draft]: https://tools.ietf.org/html/draft-ietf-httpbis-expect-ct-03#page-15
impl Default for ExpectCt {
fn default() -> ExpectCt {
ExpectCt::Enforce(Duration::days(30))
}
}
impl Into<Header<'static>> for &ExpectCt {
fn into(self) -> Header<'static> {
let policy_string = match self {
ExpectCt::Enforce(age) => format!("max-age={}, enforce", age.whole_seconds()),
ExpectCt::Report(age, uri) => {
format!(r#"max-age={}, report-uri="{}""#, age.whole_seconds(), uri)
}
ExpectCt::ReportAndEnforce(age, uri) => {
format!("max-age={}, enforce, report-uri=\"{}\"", age.whole_seconds(), uri)
}
};
Header::new(ExpectCt::NAME, policy_string)
}
}
/// The [X-Content-Type-Options] header: turns off [mime sniffing] which can
/// prevent certain [attacks].
///
/// [mime sniffing]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#MIME_sniffing
/// [X-Content-Type-Options]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
/// [attacks]: https://helmetjs.github.io/docs/dont-sniff-mimetype/
pub enum NoSniff {
/// Turns off mime sniffing.
Enable,
}
/// Defaults to [`NoSniff::Enable`], turns off mime sniffing.
impl Default for NoSniff {
fn default() -> NoSniff {
NoSniff::Enable
}
}
impl Into<Header<'static>> for &NoSniff {
fn into(self) -> Header<'static> {
Header::new(NoSniff::NAME, "nosniff")
}
}
/// The HTTP [Strict-Transport-Security] (HSTS) header: enforces strict HTTPS
/// usage.
///
/// HSTS tells the browser that the site should only be accessed using HTTPS
/// instead of HTTP. HSTS prevents a variety of downgrading attacks and should
/// always be used when TLS is enabled. `SpaceHelmet` will turn HSTS on and
/// issue a warning if you enable TLS without enabling HSTS when the application
/// is run in the staging or production environments.
///
/// While HSTS is important for HTTPS security, incorrectly configured HSTS can
/// lead to problems as you are disallowing access to non-HTTPS enabled parts of
/// your site. [Yelp engineering] has good discussion of potential challenges
/// that can arise and how to roll this out in a large scale setting. So, if
/// you use TLS, use HSTS, but roll it out with care.
///
/// [TLS]: https://rocket.rs/guide/configuration/#configuring-tls
/// [Strict-Transport-Security]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
/// [default policy]: /rocket_contrib/helmet/enum.Hsts.html#impl-Default
/// [Yelp engineering]: https://engineeringblog.yelp.com/2017/09/the-road-to-hsts.html
/// [Staging]: /rocket/config/enum.Environment.html#variant.Staging
/// [Production]: /rocket/config/enum.Environment.html#variant.Production
pub enum Hsts {
/// Browser should only permit this site to be accesses by HTTPS for the
/// next [`Duration`].
Enable(Duration),
/// Like [`Hsts::Enable`], but also apply to all of the site's subdomains.
IncludeSubDomains(Duration),
/// Google maintains an [HSTS preload service] that can be used to prevent
/// the browser from ever connecting to your site over an insecure
/// connection. Read more [here]. Don't enable this before you have
/// registered your site.
///
/// [HSTS preload service]: https://hstspreload.org/
/// [here]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security#Preloading_Strict_Transport_Security
Preload(Duration),
}
/// Defaults to `Hsts::Enable(Duration::weeks(52))`.
impl Default for Hsts {
fn default() -> Hsts {
Hsts::Enable(Duration::weeks(52))
}
}
impl Into<Header<'static>> for &Hsts {
fn into(self) -> Header<'static> {
let policy_string = match self {
Hsts::Enable(age) => format!("max-age={}", age.whole_seconds()),
Hsts::IncludeSubDomains(age) => {
format!("max-age={}; includeSubDomains", age.whole_seconds())
}
Hsts::Preload(age) => format!("max-age={}; preload", age.whole_seconds()),
};
Header::new(Hsts::NAME, policy_string)
}
}
/// The [X-Frame-Options] header: helps prevent [clickjacking] attacks.
///
/// Controls whether the browser should allow the page to render in a `<frame>`,
/// [`<iframe>`][iframe] or `<object>`. This can be used to prevent
/// [clickjacking] attacks.
///
/// [X-Frame-Options]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
/// [clickjacking]: https://en.wikipedia.org/wiki/Clickjacking
/// [owasp-clickjacking]: https://www.owasp.org/index.php/Clickjacking_Defense_Cheat_Sheet
/// [iframe]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe
pub enum Frame {
/// Page cannot be displayed in a frame.
Deny,
/// Page can only be displayed in a frame if the page trying to render it is
/// in the same origin. Interpretation of same-origin is [browser
/// dependent][X-Frame-Options].
///
/// [X-Frame-Options]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
SameOrigin,
/// Page can only be displayed in a frame if the page trying to render it is
/// in the origin for `Uri`. Interpretation of origin is [browser
/// dependent][X-Frame-Options].
///
/// [X-Frame-Options]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
AllowFrom(Uri<'static>),
}
/// Defaults to [`Frame::SameOrigin`].
impl Default for Frame {
fn default() -> Frame {
Frame::SameOrigin
}
}
impl Into<Header<'static>> for &Frame {
fn into(self) -> Header<'static> {
let policy_string: Cow<'static, str> = match self {
Frame::Deny => "DENY".into(),
Frame::SameOrigin => "SAMEORIGIN".into(),
Frame::AllowFrom(uri) => format!("ALLOW-FROM {}", uri).into(),
};
Header::new(Frame::NAME, policy_string)
}
}
/// The [X-XSS-Protection] header: filters some forms of reflected [XSS]
/// attacks.
///
/// [X-XSS-Protection]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection
/// [XSS]: https://developer.mozilla.org/en-US/docs/Glossary/Cross-site_scripting
pub enum XssFilter {
/// Disables XSS filtering.
Disable,
/// Enables XSS filtering. If XSS is detected, the browser will sanitize
/// before rendering the page (_SpaceHelmet default_).
Enable,
/// Enables XSS filtering. If XSS is detected, the browser will not
/// render the page.
EnableBlock,
/// Enables XSS filtering. If XSS is detected, the browser will sanitize and
/// render the page and report the violation to the given `Uri`. (_Chromium
/// only_)
EnableReport(Uri<'static>),
}
/// Defaults to [`XssFilter::Enable`].
impl Default for XssFilter {
fn default() -> XssFilter {
XssFilter::Enable
}
}
impl Into<Header<'static>> for &XssFilter {
fn into(self) -> Header<'static> {
let policy_string: Cow<'static, str> = match self {
XssFilter::Disable => "0".into(),
XssFilter::Enable => "1".into(),
XssFilter::EnableBlock => "1; mode=block".into(),
XssFilter::EnableReport(u) => format!("{}{}", "1; report=", u).into(),
};
Header::new(XssFilter::NAME, policy_string)
}
}
/// The [X-DNS-Prefetch-Control] header: controls browser DNS prefetching.
///
/// Tells the browser if it should perform domain name resolution on both links
/// that the user may choose to follow as well as URLs for items referenced by
/// the document including images, CSS, JavaScript, and so forth. Disabling
/// prefetching is useful if you don't control the link on the pages, or know
/// that you don't want to leak information to these domains.
///
/// [X-DNS-Prefetch-Control]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-DNS-Prefetch-Control
pub enum Prefetch {
/// Enables DNS prefetching. This is the browser default.
On,
/// Disables DNS prefetching. This is the helmet policy default.
Off,
}
impl Default for Prefetch {
fn default() -> Prefetch {
Prefetch::Off
}
}
impl Into<Header<'static>> for &Prefetch {
fn into(self) -> Header<'static> {
let policy_string = match self {
Prefetch::On => "on",
Prefetch::Off => "off",
};
Header::new(Prefetch::NAME, policy_string)
}
}

View File

@ -21,7 +21,6 @@
//! * [tera_templates](templates) - Tera Templating
//! * [uuid](uuid) - UUID (de)serialization
//! * [${database}_pool](databases) - Database Configuration and Pooling
//! * [helmet](helmet) - Fairing for Security and Privacy Headers
//!
//! The recommend way to include features from this crate via Rocket in your
//! project is by adding a `[dependencies.rocket_contrib]` section to your
@ -45,7 +44,6 @@
#[cfg(feature="templates")] pub mod templates;
#[cfg(feature="uuid")] pub mod uuid;
#[cfg(feature="databases")] pub mod databases;
#[cfg(feature = "helmet")] pub mod helmet;
// TODO.async: Migrate compression, reenable this, tests, and add to docs.
//#[cfg(any(feature="brotli_compression", feature="gzip_compression"))] pub mod compression;

View File

@ -1,147 +0,0 @@
#[macro_use]
#[cfg(feature = "helmet")]
extern crate rocket;
#[cfg(feature = "helmet")]
mod helmet_tests {
use rocket::http::Status;
use rocket::local::blocking::{Client, LocalResponse};
use rocket_contrib::helmet::*;
use time::Duration;
#[get("/")] fn hello() { }
macro_rules! assert_header {
($response:ident, $name:expr, $value:expr) => {
match $response.headers().get_one($name) {
Some(value) => assert_eq!(value, $value),
None => panic!("missing header '{}' with value '{}'", $name, $value)
}
};
}
macro_rules! assert_no_header {
($response:ident, $name:expr) => {
if let Some(value) = $response.headers().get_one($name) {
panic!("unexpected header: '{}={}", $name, value);
}
};
}
macro_rules! dispatch {
($helmet:expr, $closure:expr) => {{
let rocket = rocket::build().mount("/", routes![hello]).attach($helmet);
let client = Client::debug(rocket).unwrap();
let response = client.get("/").dispatch();
assert_eq!(response.status(), Status::Ok);
$closure(response)
}}
}
#[test]
fn default_headers_test() {
dispatch!(SpaceHelmet::default(), |response: LocalResponse<'_>| {
assert_header!(response, "X-XSS-Protection", "1");
assert_header!(response, "X-Frame-Options", "SAMEORIGIN");
assert_header!(response, "X-Content-Type-Options", "nosniff");
})
}
#[test]
fn disable_headers_test() {
let helmet = SpaceHelmet::default().disable::<XssFilter>();
dispatch!(helmet, |response: LocalResponse<'_>| {
assert_header!(response, "X-Frame-Options", "SAMEORIGIN");
assert_header!(response, "X-Content-Type-Options", "nosniff");
assert_no_header!(response, "X-XSS-Protection");
});
let helmet = SpaceHelmet::default().disable::<Frame>();
dispatch!(helmet, |response: LocalResponse<'_>| {
assert_header!(response, "X-XSS-Protection", "1");
assert_header!(response, "X-Content-Type-Options", "nosniff");
assert_no_header!(response, "X-Frame-Options");
});
let helmet = SpaceHelmet::default()
.disable::<Frame>()
.disable::<XssFilter>()
.disable::<NoSniff>();
dispatch!(helmet, |response: LocalResponse<'_>| {
assert_no_header!(response, "X-Frame-Options");
assert_no_header!(response, "X-XSS-Protection");
assert_no_header!(response, "X-Content-Type-Options");
});
dispatch!(SpaceHelmet::new(), |response: LocalResponse<'_>| {
assert_no_header!(response, "X-Frame-Options");
assert_no_header!(response, "X-XSS-Protection");
assert_no_header!(response, "X-Content-Type-Options");
});
}
#[test]
fn additional_headers_test() {
let helmet = SpaceHelmet::default()
.enable(Hsts::default())
.enable(ExpectCt::default())
.enable(Referrer::default());
dispatch!(helmet, |response: LocalResponse<'_>| {
assert_header!(
response,
"Strict-Transport-Security",
format!("max-age={}", Duration::weeks(52).whole_seconds())
);
assert_header!(
response,
"Expect-CT",
format!("max-age={}, enforce", Duration::days(30).whole_seconds())
);
assert_header!(response, "Referrer-Policy", "no-referrer");
})
}
#[test]
fn uri_test() {
let allow_uri = uri!("https://rocket.rs");
let report_uri = uri!("https://rocket.rs");
let enforce_uri = uri!("https://rocket.rs");
let helmet = SpaceHelmet::default()
.enable(Frame::AllowFrom(allow_uri.into()))
.enable(XssFilter::EnableReport(report_uri.into()))
.enable(ExpectCt::ReportAndEnforce(Duration::seconds(30), enforce_uri.into()));
dispatch!(helmet, |response: LocalResponse<'_>| {
assert_header!(response, "X-Frame-Options", "ALLOW-FROM https://rocket.rs");
assert_header!(response, "X-XSS-Protection", "1; report=https://rocket.rs");
assert_header!(response, "Expect-CT",
"max-age=30, enforce, report-uri=\"https://rocket.rs\"");
});
}
#[test]
fn prefetch_test() {
let helmet = SpaceHelmet::default().enable(Prefetch::default());
dispatch!(helmet, |response: LocalResponse<'_>| {
assert_header!(response, "X-DNS-Prefetch-Control", "off");
});
let helmet = SpaceHelmet::default().enable(Prefetch::Off);
dispatch!(helmet, |response: LocalResponse<'_>| {
assert_header!(response, "X-DNS-Prefetch-Control", "off");
});
let helmet = SpaceHelmet::default().enable(Prefetch::On);
dispatch!(helmet, |response: LocalResponse<'_>| {
assert_header!(response, "X-DNS-Prefetch-Control", "on");
});
}
}

View File

@ -155,8 +155,8 @@ impl<'h> HeaderMap<'h> {
/// assert!(!map.contains("Accepts"));
/// ```
#[inline]
pub fn contains(&self, name: &str) -> bool {
self.headers.get(UncasedStr::new(name)).is_some()
pub fn contains<N: AsRef<str>>(&self, name: N) -> bool {
self.headers.get(UncasedStr::new(name.as_ref())).is_some()
}
/// Returns the number of _values_ stored in the map.

View File

@ -48,7 +48,7 @@ figment = { version = "0.10.4", features = ["toml", "env"] }
rand = "0.8"
either = "1"
pin-project-lite = "0.2"
indexmap = { version = "1.0", features = ["serde-1"] }
indexmap = { version = "1.0", features = ["serde-1", "std"] }
tempfile = "3"
async-trait = "0.1.43"
multer = { version = "2", features = ["tokio-io"] }

View File

@ -17,11 +17,6 @@
//! detailed guide]. If you'd like pointers on getting started, see the
//! [quickstart] or [getting started] chapters of the guide.
//!
//! You may also be interested in looking at the
//! [`rocket_contrib`](../rocket_contrib) documentation, which contains
//! automatic JSON (de)serialiazation, templating support, static file serving,
//! and other useful features.
//!
//! [overview]: https://rocket.rs/master/overview
//! [full, detailed guide]: https://rocket.rs/master/guide
//! [quickstart]: https://rocket.rs/master/guide/quickstart
@ -60,8 +55,8 @@
//!
//! ## Features
//!
//! To avoid unused dependencies, Rocket _feaure-gates_ functionalities, all of
//! which are disabled-by-default:
//! To avoid compiling unused dependencies, Rocket gates certain features, all
//! of which are disabled by default:
//!
//! | Feature | Description |
//! |-----------|---------------------------------------------------------|
@ -122,6 +117,7 @@ pub mod error;
pub mod catcher;
pub mod route;
pub mod serde;
pub mod shield;
// Reexport of HTTP everything.
pub mod http {

View File

@ -6,7 +6,7 @@ use yansi::Paint;
use either::Either;
use figment::{Figment, Provider};
use crate::{Catcher, Config, Route, Shutdown, sentinel};
use crate::{Catcher, Config, Route, Shutdown, sentinel, shield::Shield};
use crate::router::Router;
use crate::trip_wire::TripWire;
use crate::fairing::{Fairing, Fairings};
@ -151,10 +151,12 @@ impl Rocket<Build> {
/// }
/// ```
pub fn custom<T: Provider>(provider: T) -> Self {
Rocket(Building {
let rocket: Rocket<Build> = Rocket(Building {
figment: Figment::from(provider),
..Default::default()
})
});
rocket.attach(Shield::default())
}
/// Sets the configuration provider in `self` to `provider`.
@ -398,6 +400,9 @@ impl Rocket<Build> {
/// Attaches a fairing to this instance of Rocket. No fairings are eagerly
/// excuted; fairings are executed at their appropriate time.
///
/// If the attached fairing is _fungible_ and a fairing of the same name
/// already exists, this fairing replaces it.
///
/// # Example
///
/// ```rust,no_run

View File

@ -1,42 +1,28 @@
//! Security and privacy headers for all outgoing responses.
//!
//! [`SpaceHelmet`] provides a typed interface for HTTP security headers. It
//! takes some inspiration from [helmetjs], a similar piece of middleware for
//! [express].
//! The [`Shield`] fairing provides a typed interface for injecting HTTP
//! security and privacy headers into all outgoing responses. It takes some
//! inspiration from [helmetjs], a similar piece of middleware for [express].
//!
//! [fairing]: https://rocket.rs/master/guide/fairings/
//! [helmetjs]: https://helmetjs.github.io/
//! [express]: https://expressjs.com
//! [`SpaceHelmet`]: helmet::SpaceHelmet
//!
//! # Enabling
//!
//! This module is only available when the `helmet` feature is enabled. Enable
//! it in `Cargo.toml` as follows:
//!
//! ```toml
//! [dependencies.rocket_contrib]
//! version = "0.5.0-dev"
//! default-features = false
//! features = ["helmet"]
//! ```
//!
//! # Supported Headers
//!
//! | HTTP Header | Description | Policy | Default? |
//! | --------------------------- | -------------------------------------- | ------------- | -------- |
//! | [X-XSS-Protection] | Prevents some reflected XSS attacks. | [`XssFilter`] | ✔ |
//! | [X-Content-Type-Options] | Prevents client sniffing of MIME type. | [`NoSniff`] | ✔ |
//! | [X-Frame-Options] | Prevents [clickjacking]. | [`Frame`] | ✔ |
//! | [Strict-Transport-Security] | Enforces strict use of HTTPS. | [`Hsts`] | ? |
//! | [Expect-CT] | Enables certificate transparency. | [`ExpectCt`] | ✗ |
//! | [Referrer-Policy] | Enables referrer policy. | [`Referrer`] | ✗ |
//! | [X-DNS-Prefetch-Control] | Controls browser DNS prefetching. | [`Prefetch`] | ✗ |
//! | HTTP Header | Description | Policy | Default? |
//! | --------------------------- | -------------------------------------- | -------------- | -------- |
//! | [X-XSS-Protection] | Prevents some reflected XSS attacks. | [`XssFilter`] | ✗ |
//! | [X-Content-Type-Options] | Prevents client sniffing of MIME type. | [`NoSniff`] | ✔ |
//! | [X-Frame-Options] | Prevents [clickjacking]. | [`Frame`] | ✔ |
//! | [Strict-Transport-Security] | Enforces strict use of HTTPS. | [`Hsts`] | ? |
//! | [Expect-CT] | Enables certificate transparency. | [`ExpectCt`] | ✗ |
//! | [Referrer-Policy] | Enables referrer policy. | [`Referrer`] | ✗ |
//! | [X-DNS-Prefetch-Control] | Controls browser DNS prefetching. | [`Prefetch`] | ✗ |
//! | [Permissions-Policy] | Allows or block browser features. | [`Permission`] | ✔ |
//!
//! <small>? If TLS is enabled when the application is launched, in a
//! non-development environment (e.g., staging or production), HSTS is
//! automatically enabled with its default policy and a warning is
//! issued.</small>
//! <small>? If TLS is enabled in a non-debug profile, HSTS is automatically
//! enabled with its default policy and a warning is logged at liftoff.</small>
//!
//! [X-XSS-Protection]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection
//! [X-Content-Type-Options]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
@ -46,6 +32,7 @@
//! [Referrer-Policy]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
//! [X-DNS-Prefetch-Control]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-DNS-Prefetch-Control
//! [clickjacking]: https://en.wikipedia.org/wiki/Clickjacking
//! [Permissions-Policy]: https://github.com/w3c/webappsec-permissions-policy/blob/a45df7b237e2a85e1909d7f226ca4eb4ce5095ba/permissions-policy-explainer.md
//!
//! [`XssFilter`]: self::XssFilter
//! [`NoSniff`]: self::NoSniff
@ -57,36 +44,40 @@
//!
//! # Usage
//!
//! To apply default headers, simply attach an instance of [`SpaceHelmet`]
//! before launching:
//!
//! ```rust
//! # extern crate rocket;
//! # extern crate rocket_contrib;
//! use rocket_contrib::helmet::SpaceHelmet;
//!
//! let rocket = rocket::build().attach(SpaceHelmet::default());
//! ```
//!
//! Each header can be configured individually. To enable a particular header,
//! call the chainable [`enable()`](helmet::SpaceHelmet::enable()) method
//! on an instance of `SpaceHelmet`, passing in the configured policy type.
//! Similarly, to disable a header, call the chainable
//! [`disable()`](helmet::SpaceHelmet::disable()) method on an instance of
//! `SpaceHelmet`:
//! By default, [`Shield::default()`] is attached to all instances Rocket. To
//! change the default, including removing all `Shield` headers, attach a
//! configured instance of [`Shield`]:
//!
//! ```rust
//! # #[macro_use] extern crate rocket;
//! # extern crate rocket_contrib;
//! use rocket::http::uri::Uri;
//! use rocket_contrib::helmet::{SpaceHelmet, Frame, XssFilter, Hsts, NoSniff};
//! use rocket::shield::Shield;
//!
//! let site_uri = uri!("https://mysite.example.com");
//! let report_uri = uri!("https://report.example.com");
//! let helmet = SpaceHelmet::default()
//! .enable(Hsts::default())
//! .enable(Frame::AllowFrom(site_uri.into()))
//! .enable(XssFilter::EnableReport(report_uri.into()))
//! #[launch]
//! fn rocket() -> _ {
//! // Remove all `Shield` headers.
//! rocket::build().attach(Shield::new())
//! }
//! ```
//!
//! Each header can be configured individually. To enable a particular header,
//! call the chainable [`enable()`](shield::Shield::enable()) method
//! on an instance of `Shield`, passing in the configured policy type.
//! Similarly, to disable a header, call the chainable
//! [`disable()`](shield::Shield::disable()) method on an instance of
//! `Shield`:
//!
//! ```rust
//! # #[macro_use] extern crate rocket;
//! use time::Duration;
//!
//! use rocket::http::uri::Uri;
//! use rocket::shield::{Shield, Referrer, Prefetch, ExpectCt, NoSniff};
//!
//! let report_uri = uri!("https://report.rocket.rs");
//! let shield = Shield::default()
//! .enable(Referrer::NoReferrer)
//! .enable(Prefetch::Off)
//! .enable(ExpectCt::ReportAndEnforce(Duration::days(30), report_uri))
//! .disable::<NoSniff>();
//! ```
//!
@ -98,16 +89,16 @@
//! [helmetjs] docs are also a good resource, and [OWASP] has a collection of
//! references on these headers.
//!
//! * **Do I need any headers beyond what `SpaceHelmet` enables by default?**
//! * **Do I need any headers beyond what `Shield` enables by default?**
//!
//! Maybe! The other headers can protect against many important
//! Maybe! The other headers may protect against many important
//! vulnerabilities. Please consult their documentation and other resources to
//! determine if they are needed for your project.
//!
//! [OWASP]: https://www.owasp.org/index.php/OWASP_Secure_Headers_Project#tab=Headers
mod helmet;
mod shield;
mod policy;
pub use self::helmet::SpaceHelmet;
pub use self::shield::Shield;
pub use self::policy::*;

View File

@ -0,0 +1,883 @@
//! Module containing the [`Policy`] trait and types that implement it.
use std::fmt;
use std::borrow::Cow;
use indexmap::IndexMap;
use rocket_http::{ext::IntoCollection, private::SmallVec};
use time::Duration;
use crate::http::{Header, uri::Absolute, uncased::{UncasedStr, Uncased}};
/// Trait implemented by security and privacy policy headers.
///
/// Types that implement this trait can be [`enable()`]d and [`disable()`]d on
/// instances of [`Shield`].
///
/// [`Shield`]: crate::shield::Shield
/// [`enable()`]: crate::shield::Shield::enable()
/// [`disable()`]: crate::shield::Shield::disable()
pub trait Policy: Default + Send + Sync + 'static {
/// The actual name of the HTTP header.
///
/// This name must uniquely identify the header as it is used to determine
/// whether two implementations of `Policy` are for the same header. Use the
/// real HTTP header's name.
///
/// # Example
///
/// ```rust
/// # extern crate rocket;
/// # use rocket::http::Header;
/// use rocket::shield::Policy;
///
/// #[derive(Default)]
/// struct MyPolicy;
///
/// impl Policy for MyPolicy {
/// const NAME: &'static str = "X-My-Policy";
/// # fn header(&self) -> Header<'static> { unimplemented!() }
/// }
/// ```
const NAME: &'static str;
/// Returns the [`Header`](../../rocket/http/struct.Header.html) to attach
/// to all outgoing responses.
///
/// # Example
///
/// ```rust
/// # extern crate rocket;
/// use rocket::http::Header;
/// use rocket::shield::Policy;
///
/// #[derive(Default)]
/// struct MyPolicy;
///
/// impl Policy for MyPolicy {
/// # const NAME: &'static str = "X-My-Policy";
/// fn header(&self) -> Header<'static> {
/// Header::new(Self::NAME, "value-to-enable")
/// }
/// }
/// ```
fn header(&self) -> Header<'static>;
}
/// Hack to make `Policy` Object-Safe.
pub(crate) trait SubPolicy: Send + Sync {
fn name(&self) -> &'static UncasedStr;
fn header(&self) -> Header<'static>;
}
impl<P: Policy> SubPolicy for P {
fn name(&self) -> &'static UncasedStr {
P::NAME.into()
}
fn header(&self) -> Header<'static> {
Policy::header(self)
}
}
macro_rules! impl_policy {
($T:ty, $name:expr) => (
impl Policy for $T {
const NAME: &'static str = $name;
fn header(&self) -> Header<'static> {
self.into()
}
}
)
}
// Keep this in-sync with the top-level module docs.
impl_policy!(XssFilter, "X-XSS-Protection");
impl_policy!(NoSniff, "X-Content-Type-Options");
impl_policy!(Frame, "X-Frame-Options");
impl_policy!(Hsts, "Strict-Transport-Security");
impl_policy!(ExpectCt, "Expect-CT");
impl_policy!(Referrer, "Referrer-Policy");
impl_policy!(Prefetch, "X-DNS-Prefetch-Control");
impl_policy!(Permission, "Permissions-Policy");
/// The [Referrer-Policy] header: controls the value set by the browser for the
/// [Referer] header.
///
/// Tells the browser if it should send all or part of URL of the current page
/// to the next site the user navigates to via the [Referer] header. This can be
/// important for security as the URL itself might expose sensitive data, such
/// as a hidden file path or personal identifier.
///
/// [Referrer-Policy]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
/// [Referer]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referer
pub enum Referrer {
/// Omits the `Referer` header (returned by [`Referrer::default()`]).
NoReferrer,
/// Omits the `Referer` header on connection downgrade i.e. following HTTP
/// link from HTTPS site (_Browser default_).
NoReferrerWhenDowngrade,
/// Only send the origin of part of the URL, e.g. the origin of
/// `https://foo.com/bob.html` is `https://foo.com`.
Origin,
/// Send full URL for same-origin requests, only send origin part when
/// replying to [cross-origin] requests.
///
/// [cross-origin]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
OriginWhenCrossOrigin,
/// Send full URL for same-origin requests only.
SameOrigin,
/// Only send origin part of URL, only send if protocol security level
/// remains the same e.g. HTTPS to HTTPS.
StrictOrigin,
/// Send full URL for same-origin requests. For cross-origin requests, only
/// send origin part of URL if protocol security level remains the same e.g.
/// HTTPS to HTTPS.
StrictOriginWhenCrossOrigin,
/// Send full URL for same-origin or cross-origin requests. _This will leak
/// the full URL of TLS protected resources to insecure origins. Use with
/// caution._
UnsafeUrl,
}
/// Defaults to [`Referrer::NoReferrer`]. Tells the browser to omit the
/// `Referer` header.
impl Default for Referrer {
fn default() -> Referrer {
Referrer::NoReferrer
}
}
impl Into<Header<'static>> for &Referrer {
fn into(self) -> Header<'static> {
let policy_string = match self {
Referrer::NoReferrer => "no-referrer",
Referrer::NoReferrerWhenDowngrade => "no-referrer-when-downgrade",
Referrer::Origin => "origin",
Referrer::OriginWhenCrossOrigin => "origin-when-cross-origin",
Referrer::SameOrigin => "same-origin",
Referrer::StrictOrigin => "strict-origin",
Referrer::StrictOriginWhenCrossOrigin => "strict-origin-when-cross-origin",
Referrer::UnsafeUrl => "unsafe-url",
};
Header::new(Referrer::NAME, policy_string)
}
}
/// The [Expect-CT] header: enables reporting and/or enforcement of [Certificate
/// Transparency].
///
/// [Certificate Transparency] can detect and prevent the use of misissued,
/// malicious, or revoked TLS certificates. It solves a variety of problems with
/// public TLS/SSL certificate management and is valuable measure for all public
/// TLS applications.
///
/// If you're just [getting started] with certificate transparency, ensure that
/// your [site is in compliance][getting started] before you enable enforcement
/// with [`ExpectCt::Enforce`] or [`ExpectCt::ReportAndEnforce`]. Failure to do
/// so will result in the browser refusing to communicate with your application.
/// _You have been warned_.
///
/// [Expect-CT]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect-CT
/// [Certificate Transparency]: http://www.certificate-transparency.org/what-is-ct
/// [getting started]: http://www.certificate-transparency.org/getting-started
pub enum ExpectCt {
/// Enforce certificate compliance for the next [`Duration`]. Ensure that
/// your certificates are in compliance before turning on enforcement.
/// (_Shield_ default).
Enforce(Duration),
/// Report to `Absolute`, but do not enforce, compliance violations for the
/// next [`Duration`]. Doesn't provide any protection but is a good way make
/// sure things are working correctly before turning on enforcement in
/// production.
Report(Duration, Absolute<'static>),
/// Enforce compliance and report violations to `Absolute` for the next
/// [`Duration`].
ReportAndEnforce(Duration, Absolute<'static>),
}
/// Defaults to [`ExpectCt::Enforce`] with a 30 day duration, enforce CT
/// compliance, see [draft] standard for more.
///
/// [draft]: https://tools.ietf.org/html/draft-ietf-httpbis-expect-ct-03#page-15
impl Default for ExpectCt {
fn default() -> ExpectCt {
ExpectCt::Enforce(Duration::days(30))
}
}
impl Into<Header<'static>> for &ExpectCt {
fn into(self) -> Header<'static> {
let policy_string = match self {
ExpectCt::Enforce(age) => format!("max-age={}, enforce", age.whole_seconds()),
ExpectCt::Report(age, uri) => {
format!(r#"max-age={}, report-uri="{}""#, age.whole_seconds(), uri)
}
ExpectCt::ReportAndEnforce(age, uri) => {
format!("max-age={}, enforce, report-uri=\"{}\"", age.whole_seconds(), uri)
}
};
Header::new(ExpectCt::NAME, policy_string)
}
}
/// The [X-Content-Type-Options] header: turns off [mime sniffing] which can
/// prevent certain [attacks].
///
/// [mime sniffing]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#MIME_sniffing
/// [X-Content-Type-Options]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options
/// [attacks]: https://shieldjs.github.io/docs/dont-sniff-mimetype/
pub enum NoSniff {
/// Turns off mime sniffing.
Enable,
}
/// Defaults to [`NoSniff::Enable`], turns off mime sniffing.
impl Default for NoSniff {
fn default() -> NoSniff {
NoSniff::Enable
}
}
impl Into<Header<'static>> for &NoSniff {
fn into(self) -> Header<'static> {
Header::new(NoSniff::NAME, "nosniff")
}
}
/// The HTTP [Strict-Transport-Security] (HSTS) header: enforces strict HTTPS
/// usage.
///
/// HSTS tells the browser that the site should only be accessed using HTTPS
/// instead of HTTP. HSTS prevents a variety of downgrading attacks and should
/// always be used when [TLS] is enabled. `Shield` will turn HSTS on and issue a
/// warning if you enable TLS without enabling HSTS when the application is run
/// in non-debug profiles.
///
/// While HSTS is important for HTTPS security, incorrectly configured HSTS can
/// lead to problems as you are disallowing access to non-HTTPS enabled parts of
/// your site. [Yelp engineering] has good discussion of potential challenges
/// that can arise and how to roll this out in a large scale setting. So, if
/// you use TLS, use HSTS, but roll it out with care.
///
/// [TLS]: https://rocket.rs/guide/configuration/#configuring-tls
/// [Strict-Transport-Security]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security
/// [Yelp engineering]: https://engineeringblog.yelp.com/2017/09/the-road-to-hsts.html
#[derive(PartialEq, Copy, Clone)]
pub enum Hsts {
/// Browser should only permit this site to be accesses by HTTPS for the
/// next [`Duration`].
Enable(Duration),
/// Like [`Hsts::Enable`], but also apply to all of the site's subdomains.
IncludeSubDomains(Duration),
/// Google maintains an [HSTS preload service] that can be used to prevent
/// the browser from ever connecting to your site over an insecure
/// connection. Read more at [MDN]. Don't enable this before you have
/// registered your site.
///
/// [HSTS preload service]: https://hstspreload.org/
/// [MDN]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security#Preloading_Strict_Transport_Security
Preload(Duration),
}
/// Defaults to `Hsts::Enable(Duration::days(365))`.
impl Default for Hsts {
fn default() -> Hsts {
Hsts::Enable(Duration::days(365))
}
}
impl Into<Header<'static>> for &Hsts {
fn into(self) -> Header<'static> {
if self == &Hsts::default() {
static DEFAULT: Header<'static> = Header {
name: Uncased::from_borrowed(Hsts::NAME),
value: Cow::Borrowed("max-age=31536000")
};
return DEFAULT.clone();
}
let policy_string = match self {
Hsts::Enable(age) => format!("max-age={}", age.whole_seconds()),
Hsts::IncludeSubDomains(age) => {
format!("max-age={}; includeSubDomains", age.whole_seconds())
}
Hsts::Preload(age) => format!("max-age={}; preload", age.whole_seconds()),
};
Header::new(Hsts::NAME, policy_string)
}
}
/// The [X-Frame-Options] header: helps prevent [clickjacking] attacks.
///
/// Controls whether the browser should allow the page to render in a `<frame>`,
/// [`<iframe>`][iframe] or `<object>`. This can be used to prevent
/// [clickjacking] attacks.
///
/// [X-Frame-Options]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
/// [clickjacking]: https://en.wikipedia.org/wiki/Clickjacking
/// [owasp-clickjacking]: https://www.owasp.org/index.php/Clickjacking_Defense_Cheat_Sheet
/// [iframe]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe
pub enum Frame {
/// Page cannot be displayed in a frame.
Deny,
/// Page can only be displayed in a frame if the page trying to render it is
/// in the same origin. Interpretation of same-origin is [browser
/// dependent][X-Frame-Options].
///
/// [X-Frame-Options]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options
SameOrigin,
}
/// Defaults to [`Frame::SameOrigin`].
impl Default for Frame {
fn default() -> Frame {
Frame::SameOrigin
}
}
impl Into<Header<'static>> for &Frame {
fn into(self) -> Header<'static> {
let policy_string: &'static str = match self {
Frame::Deny => "DENY",
Frame::SameOrigin => "SAMEORIGIN",
};
Header::new(Frame::NAME, policy_string)
}
}
/// The [X-XSS-Protection] header: filters some forms of reflected [XSS]
/// attacks. Modern browsers do not support or enorce this header.
///
/// [X-XSS-Protection]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection
/// [XSS]: https://developer.mozilla.org/en-US/docs/Glossary/Cross-site_scripting
pub enum XssFilter {
/// Disables XSS filtering.
Disable,
/// Enables XSS filtering. If XSS is detected, the browser will sanitize
/// before rendering the page (_Shield default_).
Enable,
/// Enables XSS filtering. If XSS is detected, the browser will not
/// render the page.
EnableBlock,
}
/// Defaults to [`XssFilter::Enable`].
impl Default for XssFilter {
fn default() -> XssFilter {
XssFilter::Enable
}
}
impl Into<Header<'static>> for &XssFilter {
fn into(self) -> Header<'static> {
let policy_string: &'static str = match self {
XssFilter::Disable => "0",
XssFilter::Enable => "1",
XssFilter::EnableBlock => "1; mode=block",
};
Header::new(XssFilter::NAME, policy_string)
}
}
/// The [X-DNS-Prefetch-Control] header: controls browser DNS prefetching.
///
/// Tells the browser if it should perform domain name resolution on both links
/// that the user may choose to follow as well as URLs for items referenced by
/// the document including images, CSS, JavaScript, and so forth. Disabling
/// prefetching is useful if you don't control the link on the pages, or know
/// that you don't want to leak information to these domains.
///
/// [X-DNS-Prefetch-Control]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-DNS-Prefetch-Control
pub enum Prefetch {
/// Enables DNS prefetching. This is the browser default.
On,
/// Disables DNS prefetching. This is the shield policy default.
Off,
}
impl Default for Prefetch {
fn default() -> Prefetch {
Prefetch::Off
}
}
impl Into<Header<'static>> for &Prefetch {
fn into(self) -> Header<'static> {
let policy_string = match self {
Prefetch::On => "on",
Prefetch::Off => "off",
};
Header::new(Prefetch::NAME, policy_string)
}
}
/// The [Permissions-Policy] header: allow or block the use of browser features.
///
/// Tells the browser to allow or block the use of a browser feature in the
/// top-level page as well as allow or block _requesting access to_ (via the
/// `allow` `iframe` attribute) features in embedded iframes.
///
/// By default, the top-level page may access ~all features and any embedded
/// iframes may request access to ~any feature. This header allows the server to
/// control exactly _which_ (if any) origins may access or request access to
/// browser features.
///
/// Features are enabled via the [`Permission::allowed()`] contructor and
/// chainable [`allow()`](Self::allow()) build method. Features can be blocked
/// via the [`Permission::blocked()`] and chainable [`block()`](Self::block())
/// builder method.
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// use rocket::shield::{Shield, Permission, Feature, Allow};
///
/// // In addition to defaults, block access to geolocation and USB features.
/// // Enable camera and microphone features only for the serving origin. Enable
/// // payment request access for the current origin and `https://rocket.rs`.
/// let permission = Permission::default()
/// .block(Feature::Geolocation)
/// .block(Feature::Usb)
/// .allow(Feature::Camera, Allow::This)
/// .allow(Feature::Microphone, Allow::This)
/// .allow(Feature::Payment, [Allow::This, Allow::Origin(uri!("https://rocket.rs"))]);
///
/// rocket::build().attach(Shield::default().enable(permission));
/// ```
///
/// # Default
///
/// The default returned via [`Permission::default()`] blocks access to the
/// `interest-cohort` feature, otherwise known as FLoC, which disables using the
/// current site in ad targeting tracking computations.
///
/// [Permissions-Policy]: https://github.com/w3c/webappsec-permissions-policy/blob/a45df7b237e2a85e1909d7f226ca4eb4ce5095ba/permissions-policy-explainer.md
#[derive(PartialEq, Clone)]
pub struct Permission(IndexMap<Feature, Option<SmallVec<[Allow; 1]>>>);
impl Default for Permission {
/// The default `Permission` policy blocks access to the `interest-cohort`
/// feature, otherwise known as FLoC, which disables using the current site
/// in ad targeting tracking computations.
fn default() -> Self {
Permission::blocked(Feature::InterestCohort)
}
}
impl Permission {
/// Constructs a new `Permission` policy with only `feature` allowed for the
/// set of origins in `allow` which may be a single [`Allow`], a slice
/// (`[Allow]` or `&[Allow]`), or a vector (`Vec<Allow>`).
///
/// If `allow` is empty, the use of the feature is blocked unless another
/// call to `allow()` allows it. If `allow` contains [`Allow::Any`], the
/// feature is allowable for all origins. Otherwise, the feature is
/// allowable only for the origin specified in `allow`.
///
/// # Panics
///
/// Panics if an `Absolute` URI in an `Allow::Origin` does not contain a
/// host part.
///
/// # Example
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// use rocket::shield::{Permission, Feature, Allow};
///
/// let rocket = Allow::Origin(uri!("https://rocket.rs"));
///
/// let perm = Permission::allowed(Feature::Usb, Allow::This);
/// let perm = Permission::allowed(Feature::Usb, Allow::Any);
/// let perm = Permission::allowed(Feature::Usb, [Allow::This, rocket]);
/// ```
pub fn allowed<L>(feature: Feature, allow: L) -> Self
where L: IntoCollection<Allow>
{
Permission(IndexMap::new()).allow(feature, allow)
}
/// Constructs a new `Permission` policy with only `feature` blocked.
///
/// # Example
///
/// ```rust
/// use rocket::shield::{Permission, Feature};
///
/// let perm = Permission::blocked(Feature::Usb);
/// let perm = Permission::blocked(Feature::Payment);
/// ```
pub fn blocked(feature: Feature) -> Self {
Permission(IndexMap::new()).block(feature)
}
/// Adds `feature` as allowable for the set of origins in `allow` which may
/// be a single [`Allow`], a slice (`[Allow]` or `&[Allow]`), or a vector
/// (`Vec<Allow>`).
///
/// This policy supercedes any previous policy set for `feature`.
///
/// If `allow` is empty, the use of the feature is blocked unless another
/// call to `allow()` allows it. If `allow` contains [`Allow::Any`], the
/// feature is allowable for all origins. Otherwise, the feature is
/// allowable only for the origin specified in `allow`.
///
/// # Panics
///
/// Panics if an `Absolute` URI in an `Allow::Origin` does not contain a
/// host part.
///
/// # Example
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// use rocket::shield::{Permission, Feature, Allow};
///
/// let rocket = Allow::Origin(uri!("https://rocket.rs"));
/// let perm = Permission::allowed(Feature::Usb, Allow::This)
/// .allow(Feature::Payment, [rocket, Allow::This]);
/// ```
pub fn allow<L>(mut self, feature: Feature, allow: L) -> Self
where L: IntoCollection<Allow>
{
let mut allow = allow.into_collection();
if allow.contains(&Allow::Any) {
allow = Allow::Any.into_collection();
}
for allow in &allow {
if let Allow::Origin(absolute) = allow {
let auth = absolute.authority();
if auth.is_none() || matches!(auth, Some(a) if a.host().is_empty()) {
panic!("...")
}
}
}
self.0.insert(feature, Some(allow));
self
}
/// Blocks `feature`. This policy supercedes any previous policy set for
/// `feature`.
///
/// # Example
///
/// ```rust
/// use rocket::shield::{Permission, Feature};
///
/// let perm = Permission::default()
/// .block(Feature::Usb)
/// .block(Feature::Payment);
/// ```
pub fn block(mut self, feature: Feature) -> Self {
self.0.insert(feature, None);
self
}
/// Returns the allow list (so far) for `feature` if feature is allowed.
///
/// # Example
///
/// ```rust
/// use rocket::shield::{Permission, Feature, Allow};
///
/// let perm = Permission::default();
/// assert!(perm.get(Feature::Usb).is_none());
///
/// let perm = perm.allow(Feature::Usb, Allow::Any);
/// assert_eq!(perm.get(Feature::Usb).unwrap(), &[Allow::Any]);
/// ```
pub fn get(&self, feature: Feature) -> Option<&[Allow]> {
self.0.get(&feature)?.as_deref()
}
/// Returns an iterator over the pairs of features and their allow lists,
/// `None` if the feature is blocked.
///
/// Features are returned in the order in which they were first added.
///
/// # Example
///
/// ```rust
/// # #[macro_use] extern crate rocket;
/// use rocket::shield::{Permission, Feature, Allow};
///
/// let foo = uri!("https://foo.com:1234");
/// let perm = Permission::blocked(Feature::Camera)
/// .allow(Feature::Gyroscope, [Allow::This, Allow::Origin(foo.clone())])
/// .block(Feature::Payment)
/// .allow(Feature::Camera, Allow::Any);
///
/// let perms: Vec<_> = perm.iter().collect();
/// assert_eq!(perms.len(), 3);
/// assert_eq!(perms, vec![
/// (Feature::Camera, Some(&[Allow::Any][..])),
/// (Feature::Gyroscope, Some(&[Allow::This, Allow::Origin(foo)][..])),
/// (Feature::Payment, None),
/// ]);
/// ```
pub fn iter(&self) -> impl Iterator<Item = (Feature, Option<&[Allow]>)> {
self.0.iter().map(|(feature, list)| (*feature, list.as_deref()))
}
}
impl Into<Header<'static>> for &Permission {
fn into(self) -> Header<'static> {
if self == &Permission::default() {
static DEFAULT: Header<'static> = Header {
name: Uncased::from_borrowed(Permission::NAME),
value: Cow::Borrowed("interest-cohort=()")
};
return DEFAULT.clone();
}
let value = self.0.iter()
.map(|(feature, allow)| {
let list = allow.as_ref()
.into_iter()
.flatten()
.map(|origin| origin.rendered())
.collect::<Vec<_>>()
.join(" ");
format!("{}=({})", feature, list)
})
.collect::<Vec<_>>()
.join(", ");
Header::new(Permission::NAME, value)
}
}
/// Specifies the origin(s) allowed to access a browser [`Feature`] via
/// [`Permission`].
#[derive(Debug, PartialEq, Clone)]
pub enum Allow {
/// Allow this specific origin. The feature is allowed only for this
/// specific origin.
///
/// The `user_info`, `path`, and `query` parts of the URI, if any, are
/// ignored.
Origin(Absolute<'static>),
/// Any origin at all.
///
/// The feature will be allowed in all browsing contexts regardless of their
/// origin.
Any,
/// The current origin.
///
/// The feature will be allowed in the immediately returned document and in
/// all nested browsing contexts (iframes) in the same origin.
This,
}
impl Allow {
fn rendered(&self) -> Cow<'static, str> {
match self {
Allow::Origin(uri) => {
let mut string = String::with_capacity(32);
string.push('"');
string.push_str(uri.scheme());
// This should never fail when rendering a header for `Shield`
// due to `panic` in `.allow()`.
if let Some(auth) = uri.authority() {
use std::fmt::Write;
let _ = write!(string, "://{}", auth.host());
if let Some(port) = auth.port() {
let _ = write!(string, ":{}", port);
}
}
string.push('"');
string.into()
}
Allow::Any => "*".into(),
Allow::This => "self".into(),
}
}
}
/// A browser feature that can be enabled or blocked via [`Permission`].
#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
#[non_exhaustive]
pub enum Feature {
// Standardized.
/// The "accelerometer" feature.
Accelerometer,
/// The "ambient-light-sensor" feature.
AmbientLightSensor,
/// The "autoplay" feature.
Autoplay,
/// The "battery" feature.
Battery,
/// The "camera" feature.
Camera,
/// The "cross-origin-isolated" feature.
CrossOriginIsolated,
/// The "display-capture" feature.
Displaycapture,
/// The "document-domain" feature.
DocumentDomain,
/// The "encrypted-media" feature.
EncryptedMedia,
/// The "execution-while-not-rendered" feature.
ExecutionWhileNotRendered,
/// The "execution-while-out-of-viewport" feature.
ExecutionWhileOutOfviewport,
/// The "fullscreen" feature.
Fullscreen,
/// The "geolocation" feature.
Geolocation,
/// The "gyroscope" feature.
Gyroscope,
/// The "magnetometer" feature.
Magnetometer,
/// The "microphone" feature.
Microphone,
/// The "midi" feature.
Midi,
/// The "navigation-override" feature.
NavigationOverride,
/// The "payment" feature.
Payment,
/// The "picture-in-picture" feature.
PictureInPicture,
/// The "publickey-credentials-get" feature.
PublickeyCredentialsGet,
/// The "screen-wake-lock" feature.
ScreenWakeLock,
/// The "sync-xhr" feature.
SyncXhr,
/// The "usb" feature.
Usb,
/// The "web-share" feature.
WebShare,
/// The "xr-spatial-tracking<sup>[2](#fn2)</sup>" feature.
XrSpatialtracking,
// Proposed.
/// The "clipboard-read" feature.
ClipboardRead,
/// The "clipboard-write" feature.
ClipboardWrite,
/// The "gamepad" feature.
Gamepad,
/// The "speaker-selection" feature.
SpeakerSelection,
/// The "interest-cohort" feature.
InterestCohort,
// Expiremental.
/// The "conversion-measurement" feature.
ConversionMeasurement,
/// The "focus-without-user-activation" feature.
FocusWithoutUserActivation,
/// The "hid" feature.
Hid,
/// The "idle-detection" feature.
IdleDetection,
/// The "serial" feature.
Serial,
/// The "sync-script" feature.
SyncScript,
/// The "trust-token-redemption" feature.
TrustTokenRedemption,
/// The "vertical-scroll" feature.
VerticalScroll,
}
impl Feature {
/// Returns the feature string as it appears in the header.
///
/// # Example
///
/// ```rust
/// use rocket::shield::Feature;
///
/// assert_eq!(Feature::Camera.as_str(), "camera");
/// assert_eq!(Feature::SyncScript.as_str(), "sync-script");
/// ```
pub const fn as_str(self) -> &'static str {
use Feature::*;
match self {
Accelerometer => "accelerometer",
AmbientLightSensor => "ambient-light-sensor",
Autoplay => "autoplay",
Battery => "battery",
Camera => "camera",
CrossOriginIsolated => "cross-origin-isolated",
Displaycapture => "display-capture",
DocumentDomain => "document-domain",
EncryptedMedia => "encrypted-media",
ExecutionWhileNotRendered => "execution-while-not-rendered",
ExecutionWhileOutOfviewport => "execution-while-out-of-viewport",
Fullscreen => "fullscreen",
Geolocation => "geolocation",
Gyroscope => "gyroscope",
Magnetometer => "magnetometer",
Microphone => "microphone",
Midi => "midi",
NavigationOverride => "navigation-override",
Payment => "payment",
PictureInPicture => "picture-in-picture",
PublickeyCredentialsGet => "publickey-credentials-get",
ScreenWakeLock => "screen-wake-lock",
SyncXhr => "sync-xhr",
Usb => "usb",
WebShare => "web-share",
XrSpatialtracking => "xr-spatial-tracking<sup>[2](#fn2)</sup>",
ClipboardRead => "clipboard-read",
ClipboardWrite => "clipboard-write",
Gamepad => "gamepad",
SpeakerSelection => "speaker-selection",
InterestCohort => "interest-cohort",
ConversionMeasurement => "conversion-measurement",
FocusWithoutUserActivation => "focus-without-user-activation",
Hid => "hid",
IdleDetection => "idle-detection",
Serial => "serial",
SyncScript => "sync-script",
TrustTokenRedemption => "trust-token-redemption",
VerticalScroll => "vertical-scroll",
}
}
}
impl fmt::Display for Feature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.as_str().fmt(f)
}
}

View File

@ -0,0 +1,237 @@
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use state::Storage;
use yansi::Paint;
use crate::{Rocket, Request, Response, Orbit, Config};
use crate::fairing::{Fairing, Info, Kind};
use crate::http::{Header, uncased::UncasedStr};
use crate::log::PaintExt;
use crate::shield::*;
/// A [`Fairing`] that injects browser security and privacy headers into all
/// outgoing responses.
///
/// # Usage
///
/// To use `Shield`, first construct an instance of it. To use the default
/// set of headers, construct with [`Shield::default()`](#method.default).
/// For an instance with no preset headers, use [`Shield::new()`]. To
/// enable an additional header, use [`enable()`](Shield::enable()), and to
/// disable a header, use [`disable()`](Shield::disable()):
///
/// ```rust
/// use rocket::shield::Shield;
/// use rocket::shield::{XssFilter, ExpectCt};
///
/// // A `Shield` with the default headers:
/// let shield = Shield::default();
///
/// // A `Shield` with the default headers minus `XssFilter`:
/// let shield = Shield::default().disable::<XssFilter>();
///
/// // A `Shield` with the default headers plus `ExpectCt`.
/// let shield = Shield::default().enable(ExpectCt::default());
///
/// // A `Shield` with only `XssFilter` and `ExpectCt`.
/// let shield = Shield::default()
/// .enable(XssFilter::default())
/// .enable(ExpectCt::default());
/// ```
///
/// Then, attach the instance of `Shield` to your application's instance of
/// `Rocket`:
///
/// ```rust
/// # extern crate rocket;
/// # use rocket::shield::Shield;
/// # let shield = Shield::default();
/// rocket::build()
/// // ...
/// .attach(shield)
/// # ;
/// ```
///
/// The fairing will inject all enabled headers into all outgoing responses
/// _unless_ the response already contains a header with the same name. If it
/// does contain the header, a warning is emitted, and the header is not
/// overwritten.
///
/// # TLS and HSTS
///
/// If TLS is configured and enabled when the application is launched in a
/// non-debug profile, HSTS is automatically enabled with its default policy and
/// a warning is logged.
///
/// To get rid of this warning, explicitly [`Shield::enable()`] an [`Hsts`]
/// policy.
pub struct Shield {
/// Enabled policies where the key is the header name.
policies: HashMap<&'static UncasedStr, Box<dyn SubPolicy>>,
/// Whether to enforce HSTS even though the user didn't enable it.
force_hsts: AtomicBool,
/// Headers pre-rendered at liftoff from the configured policies.
rendered: Storage<Vec<Header<'static>>>,
}
impl Default for Shield {
/// Returns a new `Shield` instance. See the [table] for a description
/// of the policies used by default.
///
/// [table]: ./#supported-headers
///
/// # Example
///
/// ```rust
/// # extern crate rocket;
/// use rocket::shield::Shield;
///
/// let shield = Shield::default();
/// ```
fn default() -> Self {
Shield::new()
.enable(NoSniff::default())
.enable(Frame::default())
.enable(Permission::default())
}
}
impl Shield {
/// Returns an instance of `Shield` with no headers enabled.
///
/// # Example
///
/// ```rust
/// use rocket::shield::Shield;
///
/// let shield = Shield::new();
/// ```
pub fn new() -> Self {
Shield {
policies: HashMap::new(),
force_hsts: AtomicBool::new(false),
rendered: Storage::new(),
}
}
/// Enables the policy header `policy`.
///
/// If the poliicy was previously enabled, the configuration is replaced
/// with that of `policy`.
///
/// # Example
///
/// ```rust
/// use rocket::shield::Shield;
/// use rocket::shield::NoSniff;
///
/// let shield = Shield::new().enable(NoSniff::default());
/// ```
pub fn enable<P: Policy>(mut self, policy: P) -> Self {
self.rendered = Storage::new();
self.policies.insert(P::NAME.into(), Box::new(policy));
self
}
/// Disables the policy header `policy`.
///
/// # Example
///
/// ```rust
/// use rocket::shield::Shield;
/// use rocket::shield::NoSniff;
///
/// let shield = Shield::default().disable::<NoSniff>();
/// ```
pub fn disable<P: Policy>(mut self) -> Self {
self.rendered = Storage::new();
self.policies.remove(UncasedStr::new(P::NAME));
self
}
/// Returns `true` if the policy `P` is enabled.
///
/// # Example
///
/// ```rust
/// use rocket::shield::Shield;
/// use rocket::shield::{Permission, NoSniff, Frame};
/// use rocket::shield::{Prefetch, ExpectCt, Referrer};
///
/// let shield = Shield::default();
///
/// assert!(shield.is_enabled::<NoSniff>());
/// assert!(shield.is_enabled::<Frame>());
/// assert!(shield.is_enabled::<Permission>());
///
/// assert!(!shield.is_enabled::<Prefetch>());
/// assert!(!shield.is_enabled::<ExpectCt>());
/// assert!(!shield.is_enabled::<Referrer>());
/// ```
pub fn is_enabled<P: Policy>(&self) -> bool {
self.policies.contains_key(UncasedStr::new(P::NAME))
}
fn headers(&self) -> &[Header<'static>] {
self.rendered.get_or_set(|| {
let mut headers: Vec<_> = self.policies.values()
.map(|p| p.header())
.collect();
if self.force_hsts.load(Ordering::Acquire) {
headers.push(Policy::header(&Hsts::default()));
}
headers
})
}
}
#[crate::async_trait]
impl Fairing for Shield {
fn info(&self) -> Info {
Info {
name: "Shield",
kind: Kind::Liftoff | Kind::Response | Kind::Singleton,
}
}
async fn on_liftoff(&self, rocket: &Rocket<Orbit>) {
let force_hsts = rocket.config().tls_enabled()
&& rocket.figment().profile() != Config::DEBUG_PROFILE
&& !self.is_enabled::<Hsts>();
if force_hsts {
self.force_hsts.store(true, Ordering::Release);
}
if !self.headers().is_empty() {
info!("{}{}:", Paint::emoji("🛡️ "), Paint::magenta("Shield"));
for header in self.headers() {
info_!("{}: {}", header.name(), Paint::default(header.value()));
}
if force_hsts {
warn_!("Detected TLS-enabled liftoff without enabling HSTS.");
warn_!("Shield has enabled a default HSTS policy.");
info_!("To remove this warning, configure an HSTS policy.");
}
}
}
async fn on_response<'r>(&self, _: &'r Request<'_>, response: &mut Response<'r>) {
// Set all of the headers in `self.policies` in `response` as long as
// the header is not already in the response.
for header in self.headers() {
if response.headers().contains(header.name()) {
warn!("Shield: response contains a '{}' header.", header.name());
warn_!("Refusing to overwrite existing header.");
continue
}
response.set_header(header.clone());
}
}
}

252
core/lib/tests/shield.rs Normal file
View File

@ -0,0 +1,252 @@
#[macro_use] extern crate rocket;
use rocket::Config;
use rocket::http::Status;
use rocket::local::blocking::{Client, LocalResponse};
use rocket::shield::*;
use time::Duration;
#[get("/")] fn hello() { }
macro_rules! assert_header {
($response:ident, $name:expr, $value:expr) => {
match $response.headers().get_one($name) {
Some(value) => assert_eq!(value, $value),
None => panic!("missing header '{}' with value '{}'", $name, $value)
}
};
}
macro_rules! assert_no_header {
($response:ident, $name:expr) => {
if let Some(value) = $response.headers().get_one($name) {
panic!("unexpected header: '{}={}", $name, value);
}
};
}
macro_rules! dispatch {
($shield:expr, $closure:expr) => {{
let rocket = rocket::build().mount("/", routes![hello]).attach($shield);
let client = Client::debug(rocket).unwrap();
let response = client.get("/").dispatch();
assert_eq!(response.status(), Status::Ok);
$closure(response)
}}
}
#[test]
fn default_shield() {
let client = Client::debug(rocket::build()).unwrap();
let response = client.get("/").dispatch();
assert_header!(response, "Permissions-Policy", "interest-cohort=()");
assert_header!(response, "X-Frame-Options", "SAMEORIGIN");
assert_header!(response, "X-Content-Type-Options", "nosniff");
let client = Client::debug(rocket::custom(Config::debug_default())).unwrap();
let response = client.get("/").dispatch();
assert_header!(response, "Permissions-Policy", "interest-cohort=()");
assert_header!(response, "X-Frame-Options", "SAMEORIGIN");
assert_header!(response, "X-Content-Type-Options", "nosniff");
}
#[test]
fn shield_singleton() {
let rocket = rocket::build().attach(Shield::new());
let client = Client::debug(rocket).unwrap();
let response = client.get("/").dispatch();
assert_no_header!(response, "Permissions-Policy");
assert_no_header!(response, "X-Frame-Options");
assert_no_header!(response, "X-Content-Type-Options");
let rocket = rocket::custom(Config::debug_default()).attach(Shield::new());
let client = Client::debug(rocket).unwrap();
let response = client.get("/").dispatch();
assert_no_header!(response, "Permissions-Policy");
assert_no_header!(response, "X-Frame-Options");
assert_no_header!(response, "X-Content-Type-Options");
}
#[test]
fn default_headers_test() {
dispatch!(Shield::default(), |response: LocalResponse<'_>| {
assert_header!(response, "Permissions-Policy", "interest-cohort=()");
assert_header!(response, "X-Frame-Options", "SAMEORIGIN");
assert_header!(response, "X-Content-Type-Options", "nosniff");
})
}
#[test]
fn disable_headers_test() {
let shield = Shield::default().disable::<Permission>();
dispatch!(shield, |response: LocalResponse<'_>| {
assert_header!(response, "X-Frame-Options", "SAMEORIGIN");
assert_header!(response, "X-Content-Type-Options", "nosniff");
assert_no_header!(response, "Permissions-Policy");
});
let shield = Shield::default().disable::<Frame>();
dispatch!(shield, |response: LocalResponse<'_>| {
assert_header!(response, "Permissions-Policy", "interest-cohort=()");
assert_header!(response, "X-Content-Type-Options", "nosniff");
assert_no_header!(response, "X-Frame-Options");
});
let shield = Shield::default()
.disable::<Frame>()
.disable::<Permission>()
.disable::<NoSniff>();
dispatch!(shield, |response: LocalResponse<'_>| {
assert_no_header!(response, "X-Frame-Options");
assert_no_header!(response, "Permissions-Policy");
assert_no_header!(response, "X-Content-Type-Options");
});
dispatch!(Shield::new(), |response: LocalResponse<'_>| {
assert_no_header!(response, "X-Frame-Options");
assert_no_header!(response, "Permissions-Policy");
assert_no_header!(response, "X-Content-Type-Options");
});
}
#[test]
fn additional_headers_test() {
let shield = Shield::default()
.enable(Hsts::default())
.enable(ExpectCt::default())
.enable(Referrer::default());
dispatch!(shield, |response: LocalResponse<'_>| {
assert_header!(
response,
"Strict-Transport-Security",
format!("max-age={}", Duration::days(365).whole_seconds())
);
assert_header!(
response,
"Expect-CT",
format!("max-age={}, enforce", Duration::days(30).whole_seconds())
);
assert_header!(response, "Referrer-Policy", "no-referrer");
})
}
#[test]
fn uri_test() {
let enforce_uri = uri!("https://rocket.rs");
let shield = Shield::default()
.enable(ExpectCt::ReportAndEnforce(Duration::seconds(30), enforce_uri));
dispatch!(shield, |response: LocalResponse<'_>| {
assert_header!(response, "Expect-CT",
"max-age=30, enforce, report-uri=\"https://rocket.rs\"");
});
}
#[test]
fn prefetch_test() {
let shield = Shield::default().enable(Prefetch::default());
dispatch!(shield, |response: LocalResponse<'_>| {
assert_header!(response, "X-DNS-Prefetch-Control", "off");
});
let shield = Shield::default().enable(Prefetch::Off);
dispatch!(shield, |response: LocalResponse<'_>| {
assert_header!(response, "X-DNS-Prefetch-Control", "off");
});
let shield = Shield::default().enable(Prefetch::On);
dispatch!(shield, |response: LocalResponse<'_>| {
assert_header!(response, "X-DNS-Prefetch-Control", "on");
});
}
#[test]
#[should_panic]
fn bad_uri_permission_test() {
let uri = uri!("http://:200");
Permission::allowed(Feature::Usb, Allow::Origin(uri));
}
#[test]
#[should_panic]
fn bad_uri_permission_test2() {
let uri = uri!("http://:200");
Permission::default().allow(Feature::Camera, Allow::Origin(uri));
}
#[test]
fn permission_test() {
let shield = Shield::default().enable(Permission::default());
dispatch!(shield, |response: LocalResponse<'_>| {
assert_header!(response, "Permissions-Policy", "interest-cohort=()");
});
let shield = Shield::default().enable(Permission::blocked(Feature::Usb));
dispatch!(shield, |response: LocalResponse<'_>| {
assert_header!(response, "Permissions-Policy", "usb=()");
});
let permission = Permission::blocked(Feature::Usb)
.block(Feature::Camera)
.block(Feature::WebShare);
let shield = Shield::default().enable(permission);
dispatch!(shield, |r: LocalResponse<'_>| {
assert_header!(r, "Permissions-Policy", "usb=(), camera=(), web-share=()");
});
let permission = Permission::blocked(Feature::Usb)
.allow(Feature::Camera, [Allow::Any, Allow::This])
.block(Feature::WebShare);
let shield = Shield::default().enable(permission);
dispatch!(shield, |r: LocalResponse<'_>| {
assert_header!(r, "Permissions-Policy", "usb=(), camera=(*), web-share=()");
});
let permission = Permission::blocked(Feature::Usb)
.allow(Feature::Camera, [Allow::This])
.block(Feature::WebShare);
let shield = Shield::default().enable(permission);
dispatch!(shield, |r: LocalResponse<'_>| {
assert_header!(r, "Permissions-Policy", "usb=(), camera=(self), web-share=()");
});
let uri = uri!("http://rocket.rs");
let permission = Permission::allowed(Feature::Usb, Allow::Origin(uri))
.allow(Feature::Camera, [Allow::This])
.block(Feature::WebShare);
let shield = Shield::default().enable(permission);
dispatch!(shield, |r: LocalResponse<'_>| {
assert_header!(r, "Permissions-Policy",
"usb=(\"http://rocket.rs\"), camera=(self), web-share=()");
});
let origin1 = Allow::Origin(uri!("http://rocket.rs"));
let origin2 = Allow::Origin(uri!("https://rocket.rs"));
let shield = Shield::default()
.enable(Permission::allowed(Feature::Camera, [origin1, origin2]));
dispatch!(shield, |r: LocalResponse<'_>| {
assert_header!(r, "Permissions-Policy",
"camera=(\"http://rocket.rs\" \"https://rocket.rs\")");
});
let origin1 = Allow::Origin(uri!("http://rocket.rs"));
let origin2 = Allow::Origin(uri!("https://rocket.rs"));
let perm = Permission::allowed(Feature::Accelerometer, [origin1, origin2])
.block(Feature::Usb);
let shield = Shield::default().enable(perm);
dispatch!(shield, |r: LocalResponse<'_>| {
assert_header!(r, "Permissions-Policy",
"accelerometer=(\"http://rocket.rs\" \"https://rocket.rs\"), usb=()");
});
}

View File

@ -61,4 +61,5 @@ fn rocket() -> _ {
.mount("/", routes![hello])
.mount("/hello", routes![world, mir])
.mount("/wave", routes![wave])
.attach(rocket::shield::Shield::new())
}

View File

@ -63,15 +63,12 @@ function test_contrib() {
tera_templates
handlebars_templates
serve
helmet
diesel_postgres_pool
diesel_sqlite_pool
diesel_mysql_pool
postgres_pool
sqlite_pool
memcache_pool
brotli_compression
gzip_compression
)
echo ":: Building and testing contrib [default]..."