Initial support for marking custom protocol secure (#36656)

Add initial support for marking custom protocol as secure, this makes it
possible to `fetch` a custom protocol inside secure contexts (e.g.
http://localhost)

Some additional contexts:

- [#embedding > Custom protocol secure
context](https://servo.zulipchat.com/#narrow/channel/437943-embedding/topic/Custom.20protocol.20secure.20context)
-
https://github.com/versotile-org/tauri-runtime-verso/issues/6#issuecomment-2820776128

Testing: use `fetch('urlinfo://abc').then(async (response) =>
console.log(await response.text())).catch(console.log)` in servoshell
with in a secure context (e.g. https://servo.org), and see the response
should not be an error

---------

Signed-off-by: Tony <legendmastertony@gmail.com>
Signed-off-by: Tony <68118705+Legend-Master@users.noreply.github.com>
Co-authored-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Tony 2025-04-25 18:39:33 +08:00 committed by GitHub
parent cf59aa1948
commit 894fbd003d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 56 additions and 16 deletions

View file

@ -42,7 +42,7 @@ use crate::fetch::cors_cache::CorsCache;
use crate::fetch::headers::determine_nosniff; use crate::fetch::headers::determine_nosniff;
use crate::filemanager_thread::FileManager; use crate::filemanager_thread::FileManager;
use crate::http_loader::{HttpState, determine_requests_referrer, http_fetch, set_default_accept}; use crate::http_loader::{HttpState, determine_requests_referrer, http_fetch, set_default_accept};
use crate::protocols::ProtocolRegistry; use crate::protocols::{ProtocolRegistry, is_url_potentially_trustworthy};
use crate::request_interceptor::RequestInterceptor; use crate::request_interceptor::RequestInterceptor;
use crate::subresource_integrity::is_response_integrity_valid; use crate::subresource_integrity::is_response_integrity_valid;
@ -247,7 +247,7 @@ pub async fn main_fetch(
// Step 4. Upgrade request to a potentially trustworthy URL, if appropriate. // Step 4. Upgrade request to a potentially trustworthy URL, if appropriate.
if should_upgrade_request_to_potentially_trustworty(request, context) || if should_upgrade_request_to_potentially_trustworty(request, context) ||
should_upgrade_mixed_content_request(request) should_upgrade_mixed_content_request(request, &context.protocols)
{ {
trace!( trace!(
"upgrading {} targeting {:?}", "upgrading {} targeting {:?}",
@ -294,7 +294,7 @@ pub async fn main_fetch(
"Request attempted on bad port".into(), "Request attempted on bad port".into(),
))); )));
} }
if should_request_be_blocked_as_mixed_content(request) { if should_request_be_blocked_as_mixed_content(request, &context.protocols) {
response = Some(Response::network_error(NetworkError::Internal( response = Some(Response::network_error(NetworkError::Internal(
"Blocked as mixed content".into(), "Blocked as mixed content".into(),
))); )));
@ -359,13 +359,16 @@ pub async fn main_fetch(
if (same_origin && request.response_tainting == ResponseTainting::Basic) || if (same_origin && request.response_tainting == ResponseTainting::Basic) ||
// request's current URL's scheme is "data" // request's current URL's scheme is "data"
current_scheme == "data" || current_scheme == "data" ||
// Note: Although it is not part of the specification, we make an exception here
// for custom protocols that are explicitly marked as active for fetch.
context.protocols.is_fetchable(current_scheme) ||
// request's mode is "navigate" or "websocket" // request's mode is "navigate" or "websocket"
matches!( matches!(
request.mode, request.mode,
RequestMode::Navigate | RequestMode::WebSocket { .. } RequestMode::Navigate | RequestMode::WebSocket { .. }
) )
{ {
// Substep 1. Set requests response tainting to "basic". // Substep 1. Set request's response tainting to "basic".
request.response_tainting = ResponseTainting::Basic; request.response_tainting = ResponseTainting::Basic;
// Substep 2. Return the result of running scheme fetch given fetchParams. // Substep 2. Return the result of running scheme fetch given fetchParams.
@ -373,13 +376,13 @@ pub async fn main_fetch(
} else if request.mode == RequestMode::SameOrigin { } else if request.mode == RequestMode::SameOrigin {
Response::network_error(NetworkError::Internal("Cross-origin response".into())) Response::network_error(NetworkError::Internal("Cross-origin response".into()))
} else if request.mode == RequestMode::NoCors { } else if request.mode == RequestMode::NoCors {
// Substep 1. If requests redirect mode is not "follow", then return a network error. // Substep 1. If request's redirect mode is not "follow", then return a network error.
if request.redirect_mode != RedirectMode::Follow { if request.redirect_mode != RedirectMode::Follow {
Response::network_error(NetworkError::Internal( Response::network_error(NetworkError::Internal(
"NoCors requests must follow redirects".into(), "NoCors requests must follow redirects".into(),
)) ))
} else { } else {
// Substep 2. Set requests response tainting to "opaque". // Substep 2. Set request's response tainting to "opaque".
request.response_tainting = ResponseTainting::Opaque; request.response_tainting = ResponseTainting::Opaque;
// Substep 3. Return the result of running scheme fetch given fetchParams. // Substep 3. Return the result of running scheme fetch given fetchParams.
@ -490,7 +493,7 @@ pub async fn main_fetch(
let should_replace_with_mime_type_error = !response_is_network_error && let should_replace_with_mime_type_error = !response_is_network_error &&
should_be_blocked_due_to_mime_type(request.destination, &response.headers); should_be_blocked_due_to_mime_type(request.destination, &response.headers);
let should_replace_with_mixed_content = !response_is_network_error && let should_replace_with_mixed_content = !response_is_network_error &&
should_response_be_blocked_as_mixed_content(request, &response); should_response_be_blocked_as_mixed_content(request, &response, &context.protocols);
// Step 15. // Step 15.
let mut network_error_response = response let mut network_error_response = response
@ -933,7 +936,10 @@ pub fn should_request_be_blocked_due_to_a_bad_port(url: &ServoUrl) -> bool {
} }
/// <https://w3c.github.io/webappsec-mixed-content/#should-block-fetch> /// <https://w3c.github.io/webappsec-mixed-content/#should-block-fetch>
pub fn should_request_be_blocked_as_mixed_content(request: &Request) -> bool { pub fn should_request_be_blocked_as_mixed_content(
request: &Request,
protocol_registry: &ProtocolRegistry,
) -> bool {
// Step 1. Return allowed if one or more of the following conditions are met: // Step 1. Return allowed if one or more of the following conditions are met:
// 1.1. Does settings prohibit mixed security contexts? // 1.1. Does settings prohibit mixed security contexts?
// returns "Does Not Restrict Mixed Security Contexts" when applied to requests client. // returns "Does Not Restrict Mixed Security Contexts" when applied to requests client.
@ -944,7 +950,7 @@ pub fn should_request_be_blocked_as_mixed_content(request: &Request) -> bool {
} }
// 1.2. requests URL is a potentially trustworthy URL. // 1.2. requests URL is a potentially trustworthy URL.
if request.url().is_potentially_trustworthy() { if is_url_potentially_trustworthy(protocol_registry, &request.url()) {
return false; return false;
} }
@ -961,7 +967,11 @@ pub fn should_request_be_blocked_as_mixed_content(request: &Request) -> bool {
} }
/// <https://w3c.github.io/webappsec-mixed-content/#should-block-response> /// <https://w3c.github.io/webappsec-mixed-content/#should-block-response>
pub fn should_response_be_blocked_as_mixed_content(request: &Request, response: &Response) -> bool { pub fn should_response_be_blocked_as_mixed_content(
request: &Request,
response: &Response,
protocol_registry: &ProtocolRegistry,
) -> bool {
// Step 1. Return allowed if one or more of the following conditions are met: // Step 1. Return allowed if one or more of the following conditions are met:
// 1.1. Does settings prohibit mixed security contexts? returns Does Not Restrict Mixed Content // 1.1. Does settings prohibit mixed security contexts? returns Does Not Restrict Mixed Content
// when applied to requests client. // when applied to requests client.
@ -975,7 +985,7 @@ pub fn should_response_be_blocked_as_mixed_content(request: &Request, response:
if response if response
.actual_response() .actual_response()
.url() .url()
.is_some_and(|response_url| response_url.is_potentially_trustworthy()) .is_some_and(|response_url| is_url_potentially_trustworthy(protocol_registry, response_url))
{ {
return false; return false;
} }
@ -1041,7 +1051,7 @@ fn should_upgrade_request_to_potentially_trustworty(
// requests header list if any of the following criteria are met: // requests header list if any of the following criteria are met:
// * requests URL is not a potentially trustworthy URL // * requests URL is not a potentially trustworthy URL
// * requests URL's host is not a preloadable HSTS host // * requests URL's host is not a preloadable HSTS host
if !request.current_url().is_potentially_trustworthy() || if !is_url_potentially_trustworthy(&context.protocols, &request.current_url()) ||
!request.current_url().host_str().is_some_and(|host| { !request.current_url().host_str().is_some_and(|host| {
!context.state.hsts_list.read().unwrap().is_host_secure(host) !context.state.hsts_list.read().unwrap().is_host_secure(host)
}) })
@ -1094,10 +1104,13 @@ fn do_settings_prohibit_mixed_security_contexts(request: &Request) -> MixedSecur
} }
/// <https://w3c.github.io/webappsec-mixed-content/#upgrade-algorithm> /// <https://w3c.github.io/webappsec-mixed-content/#upgrade-algorithm>
fn should_upgrade_mixed_content_request(request: &Request) -> bool { fn should_upgrade_mixed_content_request(
request: &Request,
protocol_registry: &ProtocolRegistry,
) -> bool {
let url = request.url(); let url = request.url();
// Step 1.1 : requests URL is a potentially trustworthy URL. // Step 1.1 : requests URL is a potentially trustworthy URL.
if url.is_potentially_trustworthy() { if is_url_potentially_trustworthy(protocol_registry, &url) {
return false; return false;
} }

View file

@ -14,6 +14,7 @@ use log::error;
use net_traits::filemanager_thread::RelativePos; use net_traits::filemanager_thread::RelativePos;
use net_traits::request::Request; use net_traits::request::Request;
use net_traits::response::Response; use net_traits::response::Response;
use servo_url::ServoUrl;
use crate::fetch::methods::{DoneChannel, FetchContext, RangeRequestBounds}; use crate::fetch::methods::{DoneChannel, FetchContext, RangeRequestBounds};
@ -47,6 +48,15 @@ pub trait ProtocolHandler: Send + Sync {
fn is_fetchable(&self) -> bool { fn is_fetchable(&self) -> bool {
false false
} }
/// Specify if this custom protocol can be used in a [secure context]
///
/// Note: this only works for bypassing mixed content checks right now
///
/// [secure context]: https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts
fn is_secure(&self) -> bool {
false
}
} }
#[derive(Default)] #[derive(Default)]
@ -114,9 +124,22 @@ impl ProtocolRegistry {
pub fn is_fetchable(&self, scheme: &str) -> bool { pub fn is_fetchable(&self, scheme: &str) -> bool {
self.handlers self.handlers
.get(scheme) .get(scheme)
.map(|handler| handler.is_fetchable()) .is_some_and(|handler| handler.is_fetchable())
.unwrap_or(false)
} }
pub fn is_secure(&self, scheme: &str) -> bool {
self.handlers
.get(scheme)
.is_some_and(|handler| handler.is_secure())
}
}
/// Test if the URL is potentially trustworthy or the custom protocol is registered as secure
pub fn is_url_potentially_trustworthy(
protocol_registry: &ProtocolRegistry,
url: &ServoUrl,
) -> bool {
url.is_potentially_trustworthy() || protocol_registry.is_secure(url.scheme())
} }
pub fn range_not_satisfiable_error(response: &mut Response) { pub fn range_not_satisfiable_error(response: &mut Response) {

View file

@ -46,4 +46,8 @@ impl ProtocolHandler for UrlInfoProtocolHander {
fn is_fetchable(&self) -> bool { fn is_fetchable(&self) -> bool {
true true
} }
fn is_secure(&self) -> bool {
true
}
} }