From f6a7087c849ed1a3bfdec914d64364f7f05a84d5 Mon Sep 17 00:00:00 2001 From: Sergio Benitez Date: Fri, 21 May 2021 22:52:52 -0700 Subject: [PATCH] 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. --- contrib/lib/Cargo.toml | 4 - contrib/lib/src/helmet/helmet.rs | 215 ----- contrib/lib/src/helmet/policy.rs | 436 --------- contrib/lib/src/lib.rs | 2 - contrib/lib/tests/helmet.rs | 147 --- core/http/src/header/header.rs | 4 +- core/lib/Cargo.toml | 2 +- core/lib/src/lib.rs | 10 +- core/lib/src/rocket.rs | 11 +- .../src/helmet => core/lib/src/shield}/mod.rs | 109 +-- core/lib/src/shield/policy.rs | 883 ++++++++++++++++++ core/lib/src/shield/shield.rs | 237 +++++ core/lib/tests/shield.rs | 252 +++++ examples/hello/src/main.rs | 1 + scripts/test.sh | 3 - 15 files changed, 1437 insertions(+), 879 deletions(-) delete mode 100644 contrib/lib/src/helmet/helmet.rs delete mode 100644 contrib/lib/src/helmet/policy.rs delete mode 100644 contrib/lib/tests/helmet.rs rename {contrib/lib/src/helmet => core/lib/src/shield}/mod.rs (54%) create mode 100644 core/lib/src/shield/policy.rs create mode 100644 core/lib/src/shield/shield.rs create mode 100644 core/lib/tests/shield.rs diff --git a/contrib/lib/Cargo.toml b/contrib/lib/Cargo.toml index fd1ebb83..e513e573 100644 --- a/contrib/lib/Cargo.toml +++ b/contrib/lib/Cargo.toml @@ -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 } diff --git a/contrib/lib/src/helmet/helmet.rs b/contrib/lib/src/helmet/helmet.rs deleted file mode 100644 index 311bfe8b..00000000 --- a/contrib/lib/src/helmet/helmet.rs +++ /dev/null @@ -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::(); -/// -/// // 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>, - 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(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::(); - /// ``` - pub fn disable(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::()); - /// assert!(helmet.is_enabled::()); - /// assert!(helmet.is_enabled::()); - /// - /// assert!(!helmet.is_enabled::()); - /// assert!(!helmet.is_enabled::()); - /// assert!(!helmet.is_enabled::()); - /// ``` - pub fn is_enabled(&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) { - if rocket.config().tls_enabled() - && rocket.figment().profile() != rocket::Config::DEBUG_PROFILE - && !self.is_enabled::() - { - 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); - } -} diff --git a/contrib/lib/src/helmet/policy.rs b/contrib/lib/src/helmet/policy.rs deleted file mode 100644 index 9ecb340e..00000000 --- a/contrib/lib/src/helmet/policy.rs +++ /dev/null @@ -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 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> 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> 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> 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> 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 ``, -/// [`