net: Implement redirect taint for requests (#41824)

This updates the existing algorithm to handle three
cases rather than the previous two. The spec was updated
afterwards and wasn't reflected in the code yet.

The usage of `response.redirect_taint` is in [1],
but we don't implement that quite right either. However,
postponing that to a future PR to keep things manageable.

Part of #41807

[1]: https://fetch.spec.whatwg.org/#fetch-finale

Signed-off-by: Tim van der Lippe <tvanderlippe@gmail.com>
This commit is contained in:
Tim van der Lippe
2026-01-11 11:57:15 +01:00
committed by GitHub
parent 3171a4b095
commit 1442e9fbc5
4 changed files with 71 additions and 42 deletions

View File

@@ -588,7 +588,7 @@ pub async fn main_fetch(
},
};
// Step 13.
// Step 13. If recursive is true, then return response.
if recursive_flag {
return response;
}
@@ -596,7 +596,7 @@ pub async fn main_fetch(
// reborrow request to avoid double mutable borrow
let request = &mut fetch_params.request;
// Step 14.
// Step 14. If response is not a network error and response is not a filtered response, then:
let mut response = if !response.is_network_error() && response.internal_response.is_none() {
// Substep 1.
if request.response_tainting == ResponseTainting::CorsTainting {
@@ -675,6 +675,9 @@ pub async fn main_fetch(
internal_response.url_list.clone_from(&request.url_list)
}
// Step 17. Set internalResponses redirect taint to requests redirect-taint.
internal_response.redirect_taint = request.redirect_taint_for_request();
// Step 19. If response is not a network error and any of the following returns blocked
// * should internalResponse to request be blocked as mixed content
// * should internalResponse to request be blocked by Content Security Policy
@@ -745,7 +748,7 @@ pub async fn main_fetch(
response
};
// Step 19.
// Step 19. If response is not a network error and any of the following returns blocked
let mut response_loaded = false;
let mut response = if !response.is_network_error() && !request.integrity_metadata.is_empty() {
// Step 19.1.

View File

@@ -55,7 +55,9 @@ use net_traits::request::{
is_cors_non_wildcard_request_header_name, is_cors_safelisted_method,
is_cors_safelisted_request_header,
};
use net_traits::response::{CacheState, HttpsState, Response, ResponseBody, ResponseType};
use net_traits::response::{
CacheState, HttpsState, RedirectTaint, Response, ResponseBody, ResponseType,
};
use net_traits::{
CookieSource, DOCUMENT_ACCEPT_HEADER_VALUE, DebugVec, FetchMetadata, NetworkError,
RedirectEndValue, RedirectStartValue, ReferrerPolicy, ResourceAttribute, ResourceFetchTiming,
@@ -2558,9 +2560,8 @@ fn cors_check(request: &Request, response: &Response) -> Result<(), ()> {
}
// Step 4. If the result of byte-serializing a request origin with request is not origin, then return failure.
match request.origin {
Origin::Origin(ref o) if *o.ascii_serialization() == *origin => {},
_ => return Err(()),
if serialize_request_origin(request).to_string() != origin {
return Err(());
}
// Step 5. If requests credentials mode is not "include", then return success.
@@ -2606,38 +2607,6 @@ fn is_redirect_status(status: StatusCode) -> bool {
)
}
/// <https://fetch.spec.whatwg.org/#concept-request-tainted-origin>
fn request_has_redirect_tainted_origin(request: &Request) -> bool {
// Step 1. Assert: requests origin is not "client".
let Origin::Origin(request_origin) = &request.origin else {
panic!("origin cannot be \"client\" at this point in time");
};
// Step 2. Let lastURL be null.
let mut last_url = None;
// Step 3. For each url of requests URL list:
for url in &request.url_list {
// Step 3.1 If lastURL is null, then set lastURL to url and continue.
let Some(last_url) = &mut last_url else {
last_url = Some(url);
continue;
};
// Step 3.2 If urls origin is not same origin with lastURLs origin and
// requests origin is not same origin with lastURLs origin, then return true.
if url.origin() != last_url.origin() && *request_origin != last_url.origin() {
return true;
}
// Step 3.3 Set lastURL to url.
*last_url = url;
}
// Step 4. Return false.
false
}
/// <https://fetch.spec.whatwg.org/#serializing-a-request-origin>
fn serialize_request_origin(request: &Request) -> headers::Origin {
// Step 1. Assert: requests origin is not "client".
@@ -2645,8 +2614,8 @@ fn serialize_request_origin(request: &Request) -> headers::Origin {
panic!("origin cannot be \"client\" at this point in time");
};
// Step 2. If request has a redirect-tainted origin, then return "null".
if request_has_redirect_tainted_origin(request) {
// Step 2. If requests redirect-taint is not "same-origin", then return "null".
if request.redirect_taint_for_request() != RedirectTaint::SameOrigin {
return headers::Origin::NULL;
}

View File

@@ -22,7 +22,8 @@ use url::Position;
use uuid::Uuid;
use crate::policy_container::{PolicyContainer, RequestPolicyContainer};
use crate::response::{HttpsState, Response};
use crate::pub_domains::is_same_site;
use crate::response::{HttpsState, RedirectTaint, Response};
use crate::{ReferrerPolicy, ResourceTimingType};
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, MallocSizeOf, PartialEq, Serialize)]
@@ -1001,6 +1002,49 @@ impl Request {
// Step 5. Return totalRequestLength.
total_request_length
}
/// <https://fetch.spec.whatwg.org/#concept-request-tainted-origin>
pub fn redirect_taint_for_request(&self) -> RedirectTaint {
// Step 1. Assert: requests origin is not "client".
let Origin::Origin(request_origin) = &self.origin else {
unreachable!("origin cannot be \"client\" at this point in time");
};
// Step 2. Let lastURL be null.
let mut last_url = None;
// Step 3. Let taint be "same-origin".
let mut taint = RedirectTaint::SameOrigin;
// Step 4. For each url of requests URL list:
for url in &self.url_list {
// Step 4.1 If lastURL is null, then set lastURL to url and continue.
let Some(last_url) = &mut last_url else {
last_url = Some(url);
continue;
};
// Step 4.2. If urls origin is not same site with lastURLs origin and
// requests origin is not same site with lastURLs origin, then return "cross-site".
if !is_same_site(&url.origin(), &last_url.origin()) &&
!is_same_site(request_origin, &last_url.origin())
{
return RedirectTaint::CrossSite;
}
// Step 4.3. If urls origin is not same origin with lastURLs origin
// and requests origin is not same origin with lastURLs origin, then set taint to "same-site".
if url.origin() != last_url.origin() && *request_origin != last_url.origin() {
taint = RedirectTaint::SameSite;
}
// Step 4.4 Set lastURL to url.
*last_url = url;
}
// Step 5. Return taint.
taint
}
}
impl Referrer {

View File

@@ -58,6 +58,15 @@ impl ResponseBody {
}
}
/// <https://fetch.spec.whatwg.org/#response-redirect-taint>
#[derive(Clone, Copy, Debug, Default, Deserialize, MallocSizeOf, PartialEq, Serialize)]
pub enum RedirectTaint {
#[default]
SameOrigin,
SameSite,
CrossSite,
}
/// [Cache state](https://fetch.spec.whatwg.org/#concept-response-cache-state)
#[derive(Clone, Copy, Debug, Deserialize, MallocSizeOf, Serialize)]
pub enum CacheState {
@@ -109,6 +118,8 @@ pub struct Response {
pub https_state: HttpsState,
pub tls_security_info: Option<TlsSecurityInfo>,
pub referrer: Option<ServoUrl>,
/// <https://fetch.spec.whatwg.org/#response-redirect-taint>
pub redirect_taint: RedirectTaint,
pub referrer_policy: ReferrerPolicy,
/// [CORS-exposed header-name list](https://fetch.spec.whatwg.org/#concept-response-cors-exposed-header-name-list)
pub cors_exposed_header_name_list: Vec<String>,
@@ -152,6 +163,7 @@ impl Response {
aborted: Arc::new(AtomicBool::new(false)),
resource_timing: Arc::new(Mutex::new(resource_timing)),
range_requested: false,
redirect_taint: Default::default(),
}
}
@@ -187,6 +199,7 @@ impl Response {
ResourceTimingType::Error,
))),
range_requested: false,
redirect_taint: Default::default(),
}
}