api: re-validate intercept URL rewrites against the SSRF gate

A Continue resolution that rewrites the request URL applied the new URL
after validate_fetch_url had already run on the original, so a rewrite
could reach a private or internal address the original request was
blocked from. Run the overridden URL back through validate_fetch_url,
matching how redirects are re-validated, and return a blocked response
if it fails.

Also fix a doc comment that ended up attached to the new fetch_response
helper instead of stealth_fetch_all.
This commit is contained in:
SGavrl committed 2026-06-24 23:06:54 +00:00
1 parent c19a21c142
commit bd39512ffb
1 file changed
+29 -7
+29 -7
View File
@@ -711,7 +711,26 @@ async fn op_fetch_url(
} }
// Apply interception overrides (shadow the params for the rest of the op). // Apply interception overrides (shadow the params for the rest of the op).
let url = override_url.unwrap_or(url); // A Continue rewrite of the URL must pass the same SSRF / private-network
// gate as the original request (checked above) and as redirects (checked
// below). Without this re-validation a rewrite to an internal address would
// bypass validate_fetch_url entirely.
let url = if let Some(new_url) = override_url {
if let Ok(parsed) = url::Url::parse(&new_url) {
if let Err(reason) = validate_fetch_url(&parsed) {
return Ok(serde_json::json!({
"status": 0,
"body": "",
"url": new_url,
"blocked": true,
"error": format!("Intercept rewrite to forbidden URL blocked: {}", reason),
}).to_string());
}
}
new_url
} else {
url
};
let method = override_method.unwrap_or(method); let method = override_method.unwrap_or(method);
let body = override_body.unwrap_or(body); let body = override_body.unwrap_or(body);
@@ -1040,12 +1059,9 @@ async fn op_fetch_url(
.to_string()) .to_string())
} }
/// Stealth-mode scripted fetch()/XHR: mirrors op_fetch_url's redirect, SSRF, /// Assemble a `Response` for the on_response interception callbacks from the
/// and CORS semantics but sends every hop through the wreq stealth client so /// parts op_fetch_url already holds. Navigation gets a Response straight from
/// the request carries the Chrome TLS fingerprint and client hints. Cookie /// the http client, but the JS fetch path builds the pieces itself.
/// handling lives inside StealthHttpClient::send_single, which shares the
/// context jar. Response bodies are not mirrored into the CDP
/// Network.getResponseBody buffer here; that is a follow-up for stealth fetches.
fn fetch_response(url: &str, status: u16, headers: HashMap<String, String>, body: Vec<u8>) -> Response { fn fetch_response(url: &str, status: u16, headers: HashMap<String, String>, body: Vec<u8>) -> Response {
Response { Response {
url: url::Url::parse(url).unwrap_or_else(|_| url::Url::parse("http://0.0.0.0/").unwrap()), url: url::Url::parse(url).unwrap_or_else(|_| url::Url::parse("http://0.0.0.0/").unwrap()),
@@ -1056,6 +1072,12 @@ fn fetch_response(url: &str, status: u16, headers: HashMap<String, String>, body
} }
} }
/// Stealth-mode scripted fetch()/XHR: mirrors op_fetch_url's redirect, SSRF,
/// and CORS semantics but sends every hop through the wreq stealth client so
/// the request carries the Chrome TLS fingerprint and client hints. Cookie
/// handling lives inside StealthHttpClient::send_single, which shares the
/// context jar. Response bodies are not mirrored into the CDP
/// Network.getResponseBody buffer here; that is a follow-up for stealth fetches.
#[cfg(feature = "stealth")] #[cfg(feature = "stealth")]
async fn stealth_fetch_all( async fn stealth_fetch_all(
stealth: Arc<StealthHttpClient>, stealth: Arc<StealthHttpClient>,