mirror of https://github.com/rwf2/Rocket.git
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:
parent
267cb9396f
commit
f6a7087c84
|
@ -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 }
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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::*;
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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=()");
|
||||
});
|
||||
}
|
|
@ -61,4 +61,5 @@ fn rocket() -> _ {
|
|||
.mount("/", routes![hello])
|
||||
.mount("/hello", routes![world, mir])
|
||||
.mount("/wave", routes![wave])
|
||||
.attach(rocket::shield::Shield::new())
|
||||
}
|
||||
|
|
|
@ -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]..."
|
||||
|
|
Loading…
Reference in New Issue