Implement fetch metadata headers (#33830)

* Implement sec-fetch-dest header

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Implement "is same site" algorithm

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Implement remaining sec-fetch-* headers

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Fix casing of header names

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Fix handling Destination::None in sec-fetch-dest

This also removes the comment about wanting to upgrade
to a newer content-security-protocol version because
the csp doesn't implement the "empty" case.

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Update WPT expectations

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Remove colon from spec comment

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Adjust expected default headers

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

* Fix test expectations

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>

---------

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
This commit is contained in:
Simon Wülker 2024-10-16 06:15:56 +02:00 committed by GitHub
parent a2f81d69c1
commit ed959d7a1a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 471 additions and 656 deletions

View file

@ -2,8 +2,19 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use headers::HeaderMap;
use content_security_policy::Destination;
use headers::{Error, Header, HeaderMap};
use http::{HeaderName, HeaderValue};
use net_traits::fetch::headers::get_decode_and_split_header_name;
use net_traits::request::RequestMode;
static SEC_FETCH_DEST: HeaderName = HeaderName::from_static("sec-fetch-dest");
static SEC_FETCH_MODE: HeaderName = HeaderName::from_static("sec-fetch-mode");
static SEC_FETCH_SITE: HeaderName = HeaderName::from_static("sec-fetch-site");
static SEC_FETCH_USER: HeaderName = HeaderName::from_static("sec-fetch-user");
/// <https://fetch.spec.whatwg.org/#determine-nosniff>
pub fn determine_nosniff(headers: &HeaderMap) -> bool {
@ -14,3 +25,187 @@ pub fn determine_nosniff(headers: &HeaderMap) -> bool {
Some(values) => !values.is_empty() && values[0].eq_ignore_ascii_case("nosniff"),
}
}
/// The `sec-fetch-dest` header
pub struct SecFetchDest(pub Destination);
/// The `sec-fetch-mode` header
///
/// This is effectively the same as a [RequestMode], except
/// it doesn't keep track of the websocket protocols
pub enum SecFetchMode {
SameOrigin,
Cors,
NoCors,
Navigate,
WebSocket,
}
/// The `sec-fetch-user` header
pub struct SecFetchUser;
/// The `sec-fetch-site` header
#[derive(Eq, PartialEq)]
pub enum SecFetchSite {
None,
SameOrigin,
SameSite,
CrossSite,
}
impl Header for SecFetchDest {
fn name() -> &'static HeaderName {
&SEC_FETCH_DEST
}
fn decode<'i, I>(_: &mut I) -> Result<Self, Error>
where
Self: Sized,
I: Iterator<Item = &'i HeaderValue>,
{
// TODO
Err(Error::invalid())
}
fn encode<E>(&self, values: &mut E)
where
E: Extend<HeaderValue>,
{
let value = HeaderValue::from_static(destination_as_str(self.0));
values.extend(std::iter::once(value));
}
}
impl From<Destination> for SecFetchDest {
fn from(value: Destination) -> Self {
Self(value)
}
}
impl Header for SecFetchMode {
fn name() -> &'static HeaderName {
&SEC_FETCH_MODE
}
fn decode<'i, I>(_: &mut I) -> Result<Self, Error>
where
Self: Sized,
I: Iterator<Item = &'i HeaderValue>,
{
// TODO
Err(Error::invalid())
}
fn encode<E>(&self, values: &mut E)
where
E: Extend<HeaderValue>,
{
let value = HeaderValue::from_static(self.as_str());
values.extend(std::iter::once(value));
}
}
impl<'a> From<&'a RequestMode> for SecFetchMode {
fn from(value: &'a RequestMode) -> Self {
match value {
RequestMode::SameOrigin => Self::SameOrigin,
RequestMode::CorsMode => Self::Cors,
RequestMode::NoCors => Self::NoCors,
RequestMode::Navigate => Self::Navigate,
RequestMode::WebSocket { .. } => Self::WebSocket,
}
}
}
impl Header for SecFetchSite {
fn name() -> &'static HeaderName {
&SEC_FETCH_SITE
}
fn decode<'i, I>(_: &mut I) -> Result<Self, Error>
where
Self: Sized,
I: Iterator<Item = &'i HeaderValue>,
{
// TODO
Err(Error::invalid())
}
fn encode<E>(&self, values: &mut E)
where
E: Extend<HeaderValue>,
{
let s = match self {
Self::None => "none",
Self::SameSite => "same-site",
Self::CrossSite => "cross-site",
Self::SameOrigin => "same-origin",
};
let value = HeaderValue::from_static(s);
values.extend(std::iter::once(value));
}
}
impl Header for SecFetchUser {
fn name() -> &'static HeaderName {
&SEC_FETCH_USER
}
fn decode<'i, I>(_: &mut I) -> Result<Self, Error>
where
Self: Sized,
I: Iterator<Item = &'i HeaderValue>,
{
// TODO
Err(Error::invalid())
}
fn encode<E>(&self, values: &mut E)
where
E: Extend<HeaderValue>,
{
let value = HeaderValue::from_static("?1");
values.extend(std::iter::once(value));
}
}
const fn destination_as_str(destination: Destination) -> &'static str {
match destination {
Destination::None => "empty",
Destination::Audio => "audio",
Destination::AudioWorklet => "audioworklet",
Destination::Document => "document",
Destination::Embed => "embed",
Destination::Font => "font",
Destination::Frame => "frame",
Destination::IFrame => "iframe",
Destination::Image => "image",
Destination::Json => "json",
Destination::Manifest => "manifest",
Destination::Object => "object",
Destination::PaintWorklet => "paintworklet",
Destination::Report => "report",
Destination::Script => "script",
Destination::ServiceWorker => "serviceworker",
Destination::SharedWorker => "sharedworker",
Destination::Style => "style",
Destination::Track => "track",
Destination::Video => "video",
Destination::WebIdentity => "webidentity",
Destination::Worker => "worker",
Destination::Xslt => "xslt",
}
}
impl SecFetchMode {
/// Converts to the spec representation of a [RequestMode]
fn as_str(&self) -> &'static str {
match self {
Self::SameOrigin => "same-origin",
Self::Cors => "cors",
Self::NoCors => "no-cors",
Self::Navigate => "navigate",
Self::WebSocket => "websocket",
}
}
}

View file

@ -67,6 +67,7 @@ use crate::cookie::ServoCookie;
use crate::cookie_storage::CookieStorage;
use crate::decoder::Decoder;
use crate::fetch::cors_cache::CorsCache;
use crate::fetch::headers::{SecFetchDest, SecFetchMode, SecFetchSite, SecFetchUser};
use crate::fetch::methods::{main_fetch, Data, DoneChannel, FetchContext, Target};
use crate::hsts::HstsList;
use crate::http_cache::{CacheKey, HttpCache};
@ -209,6 +210,35 @@ fn strict_origin_when_cross_origin(
strip_url_for_use_as_referrer(referrer_url, true)
}
/// <https://html.spec.whatwg.org/multipage/#concept-site-same-site>
fn is_same_site(site_a: &ImmutableOrigin, site_b: &ImmutableOrigin) -> bool {
// Step 1. If A and B are the same opaque origin, then return true.
if !site_a.is_tuple() && !site_b.is_tuple() && site_a == site_b {
return true;
}
// Step 2. If A or B is an opaque origin, then return false.
let ImmutableOrigin::Tuple(scheme_a, host_a, _) = site_a else {
return false;
};
let ImmutableOrigin::Tuple(scheme_b, host_b, _) = site_b else {
return false;
};
// Step 3. If A's and B's scheme values are different, then return false.
if scheme_a != scheme_b {
return false;
}
// Step 4. If A's and B's host values are not equal, then return false.
if host_a != host_b {
return false;
}
// Step 5. Return true.
true
}
/// <https://html.spec.whatwg.org/multipage/#schemelessly-same-site>
fn is_schemelessy_same_site(site_a: &ImmutableOrigin, site_b: &ImmutableOrigin) -> bool {
// Step 1
@ -1196,7 +1226,7 @@ async fn http_network_or_cache_fetch(
append_a_request_origin_header(http_request);
// Step 8.13 Append the Fetch metadata headers for httpRequest.
// TODO(#33616) Implement Sec-Fetch-* headers
append_the_fetch_metadata_headers(http_request);
// Step 8.14: If httpRequests initiator is "prefetch", then set a structured field value given
// (`Sec-Purpose`, the token "prefetch") in httpRequests header list.
@ -2356,3 +2386,112 @@ pub fn append_a_request_origin_header(request: &mut Request) {
request.headers.typed_insert(serialized_origin);
}
}
/// <https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-append-the-fetch-metadata-headers-for-a-request>
fn append_the_fetch_metadata_headers(r: &mut Request) {
// Step 1. If rs url is not an potentially trustworthy URL, return.
if !r.url().is_potentially_trustworthy() {
return;
}
// Step 2. Set the Sec-Fetch-Dest header for r.
set_the_sec_fetch_dest_header(r);
// Step 3. Set the Sec-Fetch-Mode header for r.
set_the_sec_fetch_mode_header(r);
// Step 4. Set the Sec-Fetch-Site header for r.
set_the_sec_fetch_site_header(r);
// Step 5. Set the Sec-Fetch-User header for r.
set_the_sec_fetch_user_header(r);
}
/// <https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-set-dest>
fn set_the_sec_fetch_dest_header(r: &mut Request) {
// Step 1. Assert: rs url is a potentially trustworthy URL.
debug_assert!(r.url().is_potentially_trustworthy());
// Step 2. Let header be a Structured Header whose value is a token.
// Step 3. If rs destination is the empty string, set headers value to the string "empty".
// Otherwise, set headers value to rs destination.
let header = r.destination;
// Step 4. Set a structured field value `Sec-Fetch-Dest`/header in rs header list.
r.headers.typed_insert(SecFetchDest(header));
}
/// <https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-set-mode>
fn set_the_sec_fetch_mode_header(r: &mut Request) {
// Step 1. Assert: rs url is a potentially trustworthy URL.
debug_assert!(r.url().is_potentially_trustworthy());
// Step 2. Let header be a Structured Header whose value is a token.
// Step 3. Set headers value to rs mode.
let header = &r.mode;
// Step 4. Set a structured field value `Sec-Fetch-Mode`/header in rs header list.
r.headers.typed_insert(SecFetchMode::from(header));
}
/// <https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-set-site>
fn set_the_sec_fetch_site_header(r: &mut Request) {
// The webappsec spec seems to have a similar issue as
// https://github.com/whatwg/fetch/issues/1773
let Origin::Origin(request_origin) = &r.origin else {
panic!("request origin cannot be \"client\" at this point")
};
// Step 1. Assert: rs url is a potentially trustworthy URL.
debug_assert!(r.url().is_potentially_trustworthy());
// Step 2. Let header be a Structured Header whose value is a token.
// Step 3. Set headers value to same-origin.
let mut header = SecFetchSite::SameOrigin;
// TODO: Step 3. If r is a navigation request that was explicitly caused by a
// users interaction with the user agent, then set headers value to none.
// Step 5. If headers value is not none, then for each url in rs url list:
if header != SecFetchSite::None {
for url in &r.url_list {
// Step 5.1 If url is same origin with rs origin, continue.
if url.origin() == *request_origin {
continue;
}
// Step 5.2 Set headers value to cross-site.
header = SecFetchSite::CrossSite;
// Step 5.3 If rs origin is not same site with urls origin, then break.
if is_same_site(request_origin, &url.origin()) {
break;
}
// Step 5.4 Set headers value to same-site.
header = SecFetchSite::SameSite;
}
}
// Step 6. Set a structured field value `Sec-Fetch-Site`/header in rs header list.
r.headers.typed_insert(header);
}
/// <https://w3c.github.io/webappsec-fetch-metadata/#abstract-opdef-set-user>
fn set_the_sec_fetch_user_header(r: &mut Request) {
// Step 1. Assert: rs url is a potentially trustworthy URL.
debug_assert!(r.url().is_potentially_trustworthy());
// Step 2. If r is not a navigation request, or if rs user-activation is false, return.
// TODO user activation
if !r.is_navigation_request() {
return;
}
// Step 3. Let header be a Structured Header whose value is a token.
// Step 4. Set headers value to true.
let header = SecFetchUser;
// Step 5. Set a structured field value `Sec-Fetch-User`/header in rs header list.
r.headers.typed_insert(header);
}

View file

@ -1368,6 +1368,20 @@ fn test_fetch_with_devtools() {
HeaderValue::from_static("gzip, deflate, br"),
);
// Append fetch metadata headers
headers.insert(
HeaderName::from_static("sec-fetch-dest"),
HeaderValue::from_static("empty"),
);
headers.insert(
HeaderName::from_static("sec-fetch-mode"),
HeaderValue::from_static("no-cors"),
);
headers.insert(
HeaderName::from_static("sec-fetch-site"),
HeaderValue::from_static("same-origin"),
);
let httprequest = DevtoolsHttpRequest {
url: url,
method: Method::GET,

View file

@ -26,7 +26,7 @@ use headers::{
};
use http::header::{self, HeaderMap, HeaderValue};
use http::uri::Authority;
use http::{Method, StatusCode};
use http::{HeaderName, Method, StatusCode};
use hyper::{Body, Request as HyperRequest, Response as HyperResponse};
use ipc_channel::ipc;
use ipc_channel::router::ROUTER;
@ -149,6 +149,24 @@ fn test_check_default_headers_loaded_in_every_request() {
headers.typed_insert::<UserAgent>(crate::DEFAULT_USER_AGENT.parse().unwrap());
// Append fetch metadata headers
headers.insert(
HeaderName::from_static("sec-fetch-dest"),
HeaderValue::from_static("document"),
);
headers.insert(
HeaderName::from_static("sec-fetch-mode"),
HeaderValue::from_static("no-cors"),
);
headers.insert(
HeaderName::from_static("sec-fetch-site"),
HeaderValue::from_static("same-origin"),
);
headers.insert(
HeaderName::from_static("sec-fetch-user"),
HeaderValue::from_static("?1"),
);
*expected_headers.lock().unwrap() = Some(headers.clone());
// Testing for method.GET
@ -159,7 +177,7 @@ fn test_check_default_headers_loaded_in_every_request() {
.pipeline_id(Some(TEST_PIPELINE_ID))
.build();
let response = fetch(&mut request, None);
let response = dbg!(fetch(&mut request, None));
assert!(response
.internal_response
.unwrap()
@ -282,6 +300,24 @@ fn test_request_and_response_data_with_network_messages() {
HeaderValue::from_static("gzip, deflate, br"),
);
// Append fetch metadata headers
headers.insert(
HeaderName::from_static("sec-fetch-dest"),
HeaderValue::from_static("document"),
);
headers.insert(
HeaderName::from_static("sec-fetch-mode"),
HeaderValue::from_static("no-cors"),
);
headers.insert(
HeaderName::from_static("sec-fetch-site"),
HeaderValue::from_static("same-site"),
);
headers.insert(
HeaderName::from_static("sec-fetch-user"),
HeaderValue::from_static("?1"),
);
let httprequest = DevtoolsHttpRequest {
url: url,
method: Method::GET,