use crate::net::mixed_content::is_potentially_trustworthy;
use crate::net::referrer::ReferrerPolicy;
use http::{header, HeaderMap, HeaderValue, Method};
use url::{Host, Origin, Url};
static SEC_FETCH_DEST: header::HeaderName = header::HeaderName::from_static("sec-fetch-dest");
static SEC_FETCH_MODE: header::HeaderName = header::HeaderName::from_static("sec-fetch-mode");
static SEC_FETCH_SITE: header::HeaderName = header::HeaderName::from_static("sec-fetch-site");
static SEC_FETCH_USER: header::HeaderName = header::HeaderName::from_static("sec-fetch-user");
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
pub enum RequestDestination {
#[default]
Empty,
Audio,
AudioWorklet,
Document,
Embed,
Font,
Frame,
Iframe,
Image,
Json,
Manifest,
Object,
PaintWorklet,
Report,
Script,
ServiceWorker,
SharedWorker,
Style,
Track,
Video,
Worker,
Xslt,
}
impl RequestDestination {
pub fn as_str(self) -> &'static str {
match self {
Self::Empty => "empty",
Self::Audio => "audio",
Self::AudioWorklet => "audioworklet",
Self::Document => "document",
Self::Embed => "embed",
Self::Font => "font",
Self::Frame => "frame",
Self::Iframe => "iframe",
Self::Image => "image",
Self::Json => "json",
Self::Manifest => "manifest",
Self::Object => "object",
Self::PaintWorklet => "paintworklet",
Self::Report => "report",
Self::Script => "script",
Self::ServiceWorker => "serviceworker",
Self::SharedWorker => "sharedworker",
Self::Style => "style",
Self::Track => "track",
Self::Video => "video",
Self::Worker => "worker",
Self::Xslt => "xslt",
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
pub enum RequestMode {
#[default]
NoCors,
Cors,
SameOrigin,
Navigate,
Websocket,
}
impl RequestMode {
pub fn as_str(self) -> &'static str {
match self {
Self::NoCors => "no-cors",
Self::Cors => "cors",
Self::SameOrigin => "same-origin",
Self::Navigate => "navigate",
Self::Websocket => "websocket",
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, PartialOrd, Ord)]
pub enum SecFetchSite {
None,
CrossSite,
SameSite,
SameOrigin,
}
impl SecFetchSite {
pub fn as_str(self) -> &'static str {
match self {
Self::None => "none",
Self::CrossSite => "cross-site",
Self::SameSite => "same-site",
Self::SameOrigin => "same-origin",
}
}
}
pub(crate) fn classify_site(initiator: &Origin, target: &Url) -> SecFetchSite {
let target_origin = target.origin();
if *initiator == target_origin {
return SecFetchSite::SameOrigin;
}
match (initiator, &target_origin) {
(Origin::Tuple(s1, h1, _), Origin::Tuple(s2, h2, _))
if s1 == s2 && same_site_host(h1, h2) =>
{
SecFetchSite::SameSite
}
_ => SecFetchSite::CrossSite,
}
}
fn same_site_host(a: &Host, b: &Host) -> bool {
if a == b {
return true;
}
match (a, b) {
(Host::Domain(a), Host::Domain(b)) => {
match (registrable_domain(a), registrable_domain(b)) {
(Some(a), Some(b)) => a == b,
_ => false,
}
}
_ => false,
}
}
#[cfg(not(target_arch = "wasm32"))]
fn registrable_domain(host: &str) -> Option<&str> {
psl::domain_str(host)
}
#[cfg(target_arch = "wasm32")]
fn registrable_domain(_host: &str) -> Option<&str> {
None
}
pub(crate) fn apply_sec_fetch_headers(
headers: &mut HeaderMap,
target: &Url,
destination: RequestDestination,
mode: RequestMode,
site: SecFetchSite,
user_activated: bool,
) {
if !is_potentially_trustworthy(target) {
headers.remove(&SEC_FETCH_DEST);
headers.remove(&SEC_FETCH_MODE);
headers.remove(&SEC_FETCH_SITE);
headers.remove(&SEC_FETCH_USER);
return;
}
headers.insert(
&SEC_FETCH_DEST,
HeaderValue::from_static(destination.as_str()),
);
headers.insert(&SEC_FETCH_MODE, HeaderValue::from_static(mode.as_str()));
headers.insert(&SEC_FETCH_SITE, HeaderValue::from_static(site.as_str()));
if mode == RequestMode::Navigate && user_activated {
headers.insert(&SEC_FETCH_USER, HeaderValue::from_static("?1"));
} else {
headers.remove(&SEC_FETCH_USER);
}
}
pub(crate) fn origin_header_value(
initiator: &Origin,
tainted: bool,
method: &Method,
mode: RequestMode,
referrer_policy: ReferrerPolicy,
target: &Url,
) -> Option<String> {
let cors_like = matches!(mode, RequestMode::Cors | RequestMode::Websocket);
let needed = !matches!(*method, Method::GET | Method::HEAD)
|| (cors_like && (tainted || *initiator != target.origin()));
if !needed {
return None;
}
if tainted {
return Some("null".to_string());
}
let Origin::Tuple(scheme, _, _) = initiator else {
return Some("null".to_string());
};
if !cors_like {
let cloaked = match referrer_policy {
ReferrerPolicy::NoReferrer => true,
ReferrerPolicy::NoReferrerWhenDowngrade
| ReferrerPolicy::StrictOrigin
| ReferrerPolicy::StrictOriginWhenCrossOrigin => {
scheme == "https" && target.scheme() != "https"
}
ReferrerPolicy::SameOrigin => *initiator != target.origin(),
ReferrerPolicy::Origin
| ReferrerPolicy::OriginWhenCrossOrigin
| ReferrerPolicy::UnsafeUrl => false,
};
if cloaked {
return Some("null".to_string());
}
}
Some(initiator.ascii_serialization())
}
#[cfg(test)]
mod tests {
use super::*;
fn u(s: &str) -> Url {
Url::parse(s).unwrap()
}
fn o(s: &str) -> Origin {
u(s).origin()
}
#[test]
fn site_classification() {
let init = o("https://example.com");
assert_eq!(
classify_site(&init, &u("https://example.com/a")),
SecFetchSite::SameOrigin
);
assert_eq!(
classify_site(&init, &u("https://example.com:8443/a")),
SecFetchSite::SameSite
);
assert_eq!(
classify_site(&init, &u("http://example.com/a")),
SecFetchSite::CrossSite
);
assert_eq!(
classify_site(&init, &u("https://other.com/a")),
SecFetchSite::CrossSite
);
}
#[test]
fn site_classification_uses_registrable_domain() {
let init = o("https://a.example.com");
assert_eq!(
classify_site(&init, &u("https://b.example.com/")),
SecFetchSite::SameSite
);
assert_eq!(
classify_site(&init, &u("https://example.com/")),
SecFetchSite::SameSite
);
assert_eq!(
classify_site(&init, &u("https://deep.b.example.com:8443/")),
SecFetchSite::SameSite
);
assert_eq!(
classify_site(&init, &u("https://example.org/")),
SecFetchSite::CrossSite
);
assert_eq!(
classify_site(&init, &u("https://notexample.com/")),
SecFetchSite::CrossSite
);
assert_eq!(
classify_site(
&o("https://x.example.co.uk"),
&u("https://y.example.co.uk/")
),
SecFetchSite::SameSite
);
assert_eq!(
classify_site(&o("https://a.co.uk"), &u("https://b.co.uk/")),
SecFetchSite::CrossSite
);
assert_eq!(
classify_site(&o("https://a.github.io"), &u("https://b.github.io/")),
SecFetchSite::CrossSite
);
assert_eq!(
classify_site(&o("http://localhost:3000"), &u("http://localhost:4000/")),
SecFetchSite::SameSite
);
assert_eq!(
classify_site(&o("http://a.localhost"), &u("http://b.localhost/")),
SecFetchSite::CrossSite
);
assert_eq!(
classify_site(&o("http://127.0.0.1"), &u("http://127.0.0.1:8080/")),
SecFetchSite::SameSite
);
assert_eq!(
classify_site(&o("http://127.0.0.1"), &u("http://127.0.0.2/")),
SecFetchSite::CrossSite
);
}
#[test]
fn site_ordering_degrades() {
assert_eq!(
SecFetchSite::SameOrigin.min(SecFetchSite::CrossSite),
SecFetchSite::CrossSite
);
assert_eq!(
SecFetchSite::SameSite.min(SecFetchSite::SameOrigin),
SecFetchSite::SameSite
);
}
#[test]
fn sec_fetch_headers_are_only_sent_to_trustworthy_targets() {
let mut headers = HeaderMap::new();
apply_sec_fetch_headers(
&mut headers,
&u("https://example.com/a"),
RequestDestination::Image,
RequestMode::NoCors,
SecFetchSite::SameOrigin,
false,
);
assert_eq!(headers.get("sec-fetch-dest").unwrap(), "image");
assert_eq!(headers.get("sec-fetch-mode").unwrap(), "no-cors");
assert_eq!(headers.get("sec-fetch-site").unwrap(), "same-origin");
assert!(headers.get("sec-fetch-user").is_none());
apply_sec_fetch_headers(
&mut headers,
&u("http://example.com/a"),
RequestDestination::Image,
RequestMode::NoCors,
SecFetchSite::SameOrigin,
false,
);
assert!(headers.get("sec-fetch-dest").is_none());
assert!(headers.get("sec-fetch-mode").is_none());
assert!(headers.get("sec-fetch-site").is_none());
}
#[test]
fn sec_fetch_user_requires_a_user_activated_navigation() {
let target = u("https://example.com/a");
let cases = [
(RequestMode::Navigate, true, Some("?1")),
(RequestMode::Navigate, false, None),
(RequestMode::NoCors, true, None),
];
for (mode, activated, expected) in cases {
let mut headers = HeaderMap::new();
apply_sec_fetch_headers(
&mut headers,
&target,
RequestDestination::Document,
mode,
SecFetchSite::None,
activated,
);
assert_eq!(
headers.get("sec-fetch-user").map(|v| v.to_str().unwrap()),
expected,
"{mode:?} activated={activated}"
);
}
}
fn origin_for(
initiator: &str,
tainted: bool,
method: Method,
mode: RequestMode,
policy: ReferrerPolicy,
target: &str,
) -> Option<String> {
origin_header_value(&o(initiator), tainted, &method, mode, policy, &u(target))
}
#[test]
fn origin_is_sent_for_side_effect_methods_and_cors() {
let policy = ReferrerPolicy::default();
assert_eq!(
origin_for(
"https://example.com",
false,
Method::GET,
RequestMode::NoCors,
policy,
"https://other.com/a"
),
None
);
assert_eq!(
origin_for(
"https://example.com",
false,
Method::POST,
RequestMode::NoCors,
policy,
"https://other.com/a"
)
.as_deref(),
Some("https://example.com")
);
assert_eq!(
origin_for(
"https://example.com",
false,
Method::GET,
RequestMode::Cors,
policy,
"https://example.com/a"
),
None
);
assert_eq!(
origin_for(
"https://example.com",
false,
Method::GET,
RequestMode::Cors,
policy,
"https://other.com/a"
)
.as_deref(),
Some("https://example.com")
);
assert_eq!(
origin_for(
"https://example.com",
false,
Method::GET,
RequestMode::Websocket,
policy,
"wss://other.com/a"
)
.as_deref(),
Some("https://example.com")
);
}
#[test]
fn origin_is_cloaked_by_the_referrer_policy() {
assert_eq!(
origin_for(
"https://example.com",
false,
Method::POST,
RequestMode::NoCors,
ReferrerPolicy::NoReferrer,
"https://example.com/a"
)
.as_deref(),
Some("null")
);
assert_eq!(
origin_for(
"https://example.com",
false,
Method::POST,
RequestMode::NoCors,
ReferrerPolicy::default(),
"http://other.com/a"
)
.as_deref(),
Some("null")
);
assert_eq!(
origin_for(
"https://example.com",
false,
Method::POST,
RequestMode::NoCors,
ReferrerPolicy::SameOrigin,
"https://other.com/a"
)
.as_deref(),
Some("null")
);
assert_eq!(
origin_for(
"https://example.com",
false,
Method::POST,
RequestMode::Cors,
ReferrerPolicy::NoReferrer,
"https://other.com/a"
)
.as_deref(),
Some("https://example.com")
);
}
#[test]
fn tainted_and_opaque_origins_serialise_as_null() {
assert_eq!(
origin_for(
"https://example.com",
true,
Method::POST,
RequestMode::NoCors,
ReferrerPolicy::default(),
"https://example.com/a"
)
.as_deref(),
Some("null")
);
let opaque = u("data:text/html,hi").origin();
assert_eq!(
origin_header_value(
&opaque,
false,
&Method::POST,
RequestMode::NoCors,
ReferrerPolicy::default(),
&u("https://example.com/a")
)
.as_deref(),
Some("null")
);
}
}