mirror of
https://github.com/servo/servo.git
synced 2025-06-06 16:45:39 +00:00
net: Perform CSP checks on fetch responses. (#37154)
Also add clarifying comments to the SRI WPT tests with regards to the `www.` domain and how that interacts with the integrity checks. Lastly, adjust the casing for `Strict-Dynamic`, as in the post-request check that should also be case-insensitive. Closes servo/servo#37200 Closes servo/servo#36760 Fixes servo/servo#36499 Part of w3c/webappsec-csp#727 Fixes w3c/webappsec-csp#728 Part of servo/servo#4577 Signed-off-by: Josh Matthews <josh@joshmatthews.net> Signed-off-by: Tim van der Lippe <tvanderlippe@gmail.com> Co-authored-by: Josh Matthews <josh@joshmatthews.net>
This commit is contained in:
parent
ed888e284b
commit
f710e2cab4
18 changed files with 104 additions and 88 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1228,7 +1228,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "content-security-policy"
|
||||
version = "0.5.4"
|
||||
source = "git+https://github.com/servo/rust-content-security-policy/?branch=servo-csp#58a09ee320fd6fbb828748ae04255e4c8d3f9c9e"
|
||||
source = "git+https://github.com/servo/rust-content-security-policy/?branch=servo-csp#dc1fd32b2b32b704a43f4ae170bb2cbb80a7cf59"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bitflags 2.9.1",
|
||||
|
|
|
@ -170,8 +170,12 @@ pub async fn fetch_with_cors_cache(
|
|||
// TODO: We don't implement fetchParams as defined in the spec
|
||||
}
|
||||
|
||||
fn convert_request_to_csp_request(request: &Request, origin: &ImmutableOrigin) -> csp::Request {
|
||||
csp::Request {
|
||||
pub(crate) fn convert_request_to_csp_request(request: &Request) -> Option<csp::Request> {
|
||||
let origin = match &request.origin {
|
||||
Origin::Client => return None,
|
||||
Origin::Origin(origin) => origin,
|
||||
};
|
||||
let csp_request = csp::Request {
|
||||
url: request.url().into_url(),
|
||||
origin: origin.clone().into_url_origin(),
|
||||
redirect_count: request.redirect_count,
|
||||
|
@ -190,45 +194,58 @@ fn convert_request_to_csp_request(request: &Request, origin: &ImmutableOrigin) -
|
|||
ParserMetadata::NotParserInserted => csp::ParserMetadata::NotParserInserted,
|
||||
ParserMetadata::Default => csp::ParserMetadata::None,
|
||||
},
|
||||
}
|
||||
};
|
||||
Some(csp_request)
|
||||
}
|
||||
|
||||
/// <https://www.w3.org/TR/CSP/#should-block-request>
|
||||
pub fn should_request_be_blocked_by_csp(
|
||||
request: &Request,
|
||||
csp_request: &csp::Request,
|
||||
policy_container: &PolicyContainer,
|
||||
) -> (csp::CheckResult, Vec<csp::Violation>) {
|
||||
let origin = match &request.origin {
|
||||
Origin::Client => return (csp::CheckResult::Allowed, Vec::new()),
|
||||
Origin::Origin(origin) => origin,
|
||||
};
|
||||
let csp_request = convert_request_to_csp_request(request, origin);
|
||||
|
||||
policy_container
|
||||
.csp_list
|
||||
.as_ref()
|
||||
.map(|c| c.should_request_be_blocked(&csp_request))
|
||||
.map(|c| c.should_request_be_blocked(csp_request))
|
||||
.unwrap_or((csp::CheckResult::Allowed, Vec::new()))
|
||||
}
|
||||
|
||||
/// <https://www.w3.org/TR/CSP/#report-for-request>
|
||||
pub fn report_violations_for_request_by_csp(
|
||||
request: &Request,
|
||||
csp_request: &csp::Request,
|
||||
policy_container: &PolicyContainer,
|
||||
) -> Vec<csp::Violation> {
|
||||
let origin = match &request.origin {
|
||||
Origin::Client => return Vec::new(),
|
||||
Origin::Origin(origin) => origin,
|
||||
};
|
||||
let csp_request = convert_request_to_csp_request(request, origin);
|
||||
|
||||
policy_container
|
||||
.csp_list
|
||||
.as_ref()
|
||||
.map(|c| c.report_violations_for_request(&csp_request))
|
||||
.map(|c| c.report_violations_for_request(csp_request))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn should_response_be_blocked_by_csp(
|
||||
csp_request: &csp::Request,
|
||||
response: &Response,
|
||||
policy_container: &PolicyContainer,
|
||||
) -> (csp::CheckResult, Vec<csp::Violation>) {
|
||||
if response.is_network_error() {
|
||||
return (csp::CheckResult::Allowed, Vec::new());
|
||||
}
|
||||
let csp_response = csp::Response {
|
||||
url: response
|
||||
.actual_response()
|
||||
.url()
|
||||
.cloned()
|
||||
.expect("response must have a url")
|
||||
.into_url(),
|
||||
redirect_count: csp_request.redirect_count,
|
||||
};
|
||||
policy_container
|
||||
.csp_list
|
||||
.as_ref()
|
||||
.map(|c| c.should_response_to_request_be_blocked(csp_request, &csp_response))
|
||||
.unwrap_or((csp::CheckResult::Allowed, Vec::new()))
|
||||
}
|
||||
|
||||
/// [Main fetch](https://fetch.spec.whatwg.org/#concept-main-fetch)
|
||||
pub async fn main_fetch(
|
||||
fetch_params: &mut FetchParams,
|
||||
|
@ -270,13 +287,15 @@ pub async fn main_fetch(
|
|||
RequestPolicyContainer::Client => PolicyContainer::default(),
|
||||
RequestPolicyContainer::PolicyContainer(container) => container.to_owned(),
|
||||
};
|
||||
let csp_request = convert_request_to_csp_request(request);
|
||||
if let Some(csp_request) = csp_request.as_ref() {
|
||||
// Step 2.2.
|
||||
let violations = report_violations_for_request_by_csp(csp_request, &policy_container);
|
||||
|
||||
// Step 2.2.
|
||||
let violations = report_violations_for_request_by_csp(request, &policy_container);
|
||||
|
||||
if !violations.is_empty() {
|
||||
target.process_csp_violations(request, violations);
|
||||
}
|
||||
if !violations.is_empty() {
|
||||
target.process_csp_violations(request, violations);
|
||||
}
|
||||
};
|
||||
|
||||
// Step 3.
|
||||
// TODO: handle request abort.
|
||||
|
@ -309,22 +328,24 @@ pub async fn main_fetch(
|
|||
request.insecure_requests_policy
|
||||
);
|
||||
}
|
||||
if let Some(csp_request) = csp_request.as_ref() {
|
||||
// Step 7. If should request be blocked due to a bad port, should fetching request be blocked
|
||||
// as mixed content, or should request be blocked by Content Security Policy returns blocked,
|
||||
// then set response to a network error.
|
||||
let (check_result, violations) =
|
||||
should_request_be_blocked_by_csp(csp_request, &policy_container);
|
||||
|
||||
// Step 7. If should request be blocked due to a bad port, should fetching request be blocked
|
||||
// as mixed content, or should request be blocked by Content Security Policy returns blocked,
|
||||
// then set response to a network error.
|
||||
let (check_result, violations) = should_request_be_blocked_by_csp(request, &policy_container);
|
||||
if !violations.is_empty() {
|
||||
target.process_csp_violations(request, violations);
|
||||
}
|
||||
|
||||
if !violations.is_empty() {
|
||||
target.process_csp_violations(request, violations);
|
||||
}
|
||||
|
||||
if check_result == csp::CheckResult::Blocked {
|
||||
warn!("Request blocked by CSP");
|
||||
response = Some(Response::network_error(NetworkError::Internal(
|
||||
"Blocked by Content-Security-Policy".into(),
|
||||
)))
|
||||
}
|
||||
if check_result == csp::CheckResult::Blocked {
|
||||
warn!("Request blocked by CSP");
|
||||
response = Some(Response::network_error(NetworkError::Internal(
|
||||
"Blocked by Content-Security-Policy".into(),
|
||||
)))
|
||||
}
|
||||
};
|
||||
if should_request_be_blocked_due_to_a_bad_port(&request.current_url()) {
|
||||
response = Some(Response::network_error(NetworkError::Internal(
|
||||
"Request attempted on bad port".into(),
|
||||
|
@ -530,6 +551,14 @@ pub async fn main_fetch(
|
|||
should_be_blocked_due_to_mime_type(request.destination, &response.headers);
|
||||
let should_replace_with_mixed_content = !response_is_network_error &&
|
||||
should_response_be_blocked_as_mixed_content(request, &response, &context.protocols);
|
||||
let should_replace_with_csp_error = csp_request.is_some_and(|csp_request| {
|
||||
let (check_result, violations) =
|
||||
should_response_be_blocked_by_csp(&csp_request, &response, &policy_container);
|
||||
if !violations.is_empty() {
|
||||
target.process_csp_violations(request, violations);
|
||||
}
|
||||
check_result == csp::CheckResult::Blocked
|
||||
});
|
||||
|
||||
// Step 15.
|
||||
let mut network_error_response = response
|
||||
|
@ -553,7 +582,7 @@ pub async fn main_fetch(
|
|||
|
||||
// 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
|
||||
// TODO: * should internalResponse to request be blocked by Content Security Policy
|
||||
// * should internalResponse to request be blocked by Content Security Policy
|
||||
// * should internalResponse to request be blocked due to its MIME type
|
||||
// * should internalResponse to request be blocked due to nosniff
|
||||
let mut blocked_error_response;
|
||||
|
@ -572,6 +601,10 @@ pub async fn main_fetch(
|
|||
blocked_error_response =
|
||||
Response::network_error(NetworkError::Internal("Blocked as mixed content".into()));
|
||||
&blocked_error_response
|
||||
} else if should_replace_with_csp_error {
|
||||
blocked_error_response =
|
||||
Response::network_error(NetworkError::Internal("Blocked due to CSP".into()));
|
||||
&blocked_error_response
|
||||
} else {
|
||||
internal_response
|
||||
};
|
||||
|
|
|
@ -43,7 +43,8 @@ use crate::async_runtime::HANDLE;
|
|||
use crate::connector::{CACertificates, TlsConfig, create_tls_config};
|
||||
use crate::cookie::ServoCookie;
|
||||
use crate::fetch::methods::{
|
||||
should_request_be_blocked_by_csp, should_request_be_blocked_due_to_a_bad_port,
|
||||
convert_request_to_csp_request, should_request_be_blocked_by_csp,
|
||||
should_request_be_blocked_due_to_a_bad_port,
|
||||
};
|
||||
use crate::hosts::replace_host;
|
||||
use crate::http_loader::HttpState;
|
||||
|
@ -390,14 +391,18 @@ fn connect(
|
|||
RequestPolicyContainer::PolicyContainer(container) => container.to_owned(),
|
||||
};
|
||||
|
||||
let (check_result, violations) = should_request_be_blocked_by_csp(&request, &policy_container);
|
||||
if let Some(csp_request) = convert_request_to_csp_request(&request) {
|
||||
let (check_result, violations) =
|
||||
should_request_be_blocked_by_csp(&csp_request, &policy_container);
|
||||
|
||||
if !violations.is_empty() {
|
||||
let _ = resource_event_sender.send(WebSocketNetworkEvent::ReportCSPViolations(violations));
|
||||
}
|
||||
if !violations.is_empty() {
|
||||
let _ =
|
||||
resource_event_sender.send(WebSocketNetworkEvent::ReportCSPViolations(violations));
|
||||
}
|
||||
|
||||
if check_result == csp::CheckResult::Blocked {
|
||||
return Err("Blocked by Content-Security-Policy".to_string());
|
||||
if check_result == csp::CheckResult::Blocked {
|
||||
return Err("Blocked by Content-Security-Policy".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let client = match create_request(
|
||||
|
|
8
tests/wpt/meta/MANIFEST.json
vendored
8
tests/wpt/meta/MANIFEST.json
vendored
|
@ -397312,7 +397312,7 @@
|
|||
[]
|
||||
],
|
||||
"script-src-strict_dynamic_parser_inserted.html.headers": [
|
||||
"b7918c93323eff9db66ad26a73b78798d35e5f7b",
|
||||
"9d0b3b93d44db43be7d19c34483bc1e63ef777a0",
|
||||
[]
|
||||
],
|
||||
"script-src-strict_dynamic_parser_inserted_correct_nonce.html.headers": [
|
||||
|
@ -568648,7 +568648,7 @@
|
|||
]
|
||||
],
|
||||
"default-src-sri_hash.sub.html": [
|
||||
"87fce5961fd1854303377ee939b21b6275b312cf",
|
||||
"87389c306a53fdffa9806ba05f08a097713bcc37",
|
||||
[
|
||||
null,
|
||||
{}
|
||||
|
@ -573246,7 +573246,7 @@
|
|||
]
|
||||
],
|
||||
"script-src-sri_hash.sub.html": [
|
||||
"9216e2b0d4971fc46d0010e8dfa7375845187a8d",
|
||||
"e290911183d0b9a5dccf4a6a2eaa3b12ee25c682",
|
||||
[
|
||||
null,
|
||||
{}
|
||||
|
@ -573351,7 +573351,7 @@
|
|||
]
|
||||
],
|
||||
"script-src-strict_dynamic_parser_inserted.html": [
|
||||
"c5e33dc4253dbf3ce2b0c6cb2fca4b0306d68244",
|
||||
"9a8ad7a4ef2b5592af70d4dcc56f291e75da8e1b",
|
||||
[
|
||||
null,
|
||||
{}
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
[connect-src-syncxmlhttprequest-redirect-to-blocked.sub.html]
|
||||
[Expecting logs: ["PASS Sync XMLHttpRequest.send() did not follow the disallowed redirect.","TEST COMPLETE","violated-directive=connect-src"\]]
|
||||
expected: FAIL
|
|
@ -1,3 +0,0 @@
|
|||
[connect-src-xmlhttprequest-redirect-to-blocked.sub.html]
|
||||
[Expecting logs: ["PASS XMLHttpRequest.send() did not follow the disallowed redirect.","TEST COMPLETE","violated-directive=connect-src"\]]
|
||||
expected: FAIL
|
|
@ -1,6 +1,3 @@
|
|||
[script-tag.http.html]
|
||||
[Content Security Policy: Expects blocked for script-tag to same-http origin and swap-origin redirection from http context.]
|
||||
expected: FAIL
|
||||
|
||||
[Content Security Policy: Expects blocked for script-tag to same-http origin and swap-origin redirection from http context.: securitypolicyviolation]
|
||||
expected: FAIL
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
[script-tag.https.html]
|
||||
[Content Security Policy: Expects blocked for script-tag to same-https origin and swap-origin redirection from https context.]
|
||||
expected: FAIL
|
||||
|
||||
[Content Security Policy: Expects blocked for script-tag to same-https origin and swap-origin redirection from https context.: securitypolicyviolation]
|
||||
expected: FAIL
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
[script-tag.http.html]
|
||||
[Content Security Policy: Expects blocked for script-tag to same-http origin and swap-origin redirection from http context.]
|
||||
expected: FAIL
|
||||
|
||||
[Content Security Policy: Expects blocked for script-tag to same-http origin and swap-origin redirection from http context.: securitypolicyviolation]
|
||||
expected: FAIL
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
[script-tag.https.html]
|
||||
[Content Security Policy: Expects blocked for script-tag to same-https origin and swap-origin redirection from https context.]
|
||||
expected: FAIL
|
||||
|
||||
[Content Security Policy: Expects blocked for script-tag to same-https origin and swap-origin redirection from https context.: securitypolicyviolation]
|
||||
expected: FAIL
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
[wildcard-host-part.sub.window.html]
|
||||
expected: CRASH
|
|
@ -1,7 +1,4 @@
|
|||
[dedicatedworker-connect-src.html]
|
||||
[Same-origin => cross-origin 'fetch()' in http: with connect-src 'self']
|
||||
expected: FAIL
|
||||
|
||||
[Reports match in http: with connect-src 'self']
|
||||
expected: FAIL
|
||||
|
||||
|
|
|
@ -1,10 +1,3 @@
|
|||
[report-original-url.sub.html]
|
||||
expected: TIMEOUT
|
||||
[Block after redirect, same-origin = original URL in report]
|
||||
expected: TIMEOUT
|
||||
|
||||
[Block after redirect, cross-origin = original URL in report]
|
||||
expected: TIMEOUT
|
||||
|
||||
[Violation report status OK.]
|
||||
expected: FAIL
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
[img-src-redirect.sub.html]
|
||||
[The blocked URI in the security policy violation event should be the original URI before redirects.]
|
||||
expected: FAIL
|
|
@ -7,6 +7,9 @@
|
|||
<script src='/resources/testharnessreport.js' nonce='dummy'></script>
|
||||
|
||||
<!-- CSP served: default-src {{domains[www]}}:* 'nonce-dummy' 'sha256-wIc3KtqOuTFEu6t17sIBuOswgkV406VJvhSk79Gw6U0=' 'ShA256-L7/UQ9VWpyG7C9RDEC4ctS5hI3Zcw+ta+haPGlByG9c=' 'sha512-rYCVMxWV5nq8IsMo+UZNObWtEiWGok/vDN8BMoEQi41s0znSes6E1Q2aag3Lw3u2J1w2rqH7uF2ws6FpQhfSOA=='; style-src 'unsafe-inline' -->
|
||||
<!-- The domain here is intentionally served with `www`. In the event that the integrity check fails,
|
||||
the request should be disallowed by the source list. If we were to use {{domains[]}},
|
||||
then we would not be able to observe the difference with regards to the integrity check -->
|
||||
<!-- ShA256 is intentionally mixed case -->
|
||||
</head>
|
||||
|
||||
|
@ -18,6 +21,8 @@
|
|||
var port = "{{ports[http][0]}}";
|
||||
if (location.protocol === "https:")
|
||||
port = "{{ports[https][0]}}";
|
||||
// Since {{domains[www]}} is allowed by the CSP policy, regardless of the integrity check
|
||||
// the request would be allowed.
|
||||
var crossorigin_base = location.protocol + "//{{domains[www]}}:" + port;
|
||||
|
||||
// Test name, src, integrity, expected to run.
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
<script src='/resources/testharnessreport.js' nonce='dummy'></script>
|
||||
|
||||
<!-- CSP served: script-src {{domains[www]}}:* 'nonce-dummy' 'sha256-wIc3KtqOuTFEu6t17sIBuOswgkV406VJvhSk79Gw6U0=' 'ShA256-L7/UQ9VWpyG7C9RDEC4ctS5hI3Zcw+ta+haPGlByG9c=' 'sha512-rYCVMxWV5nq8IsMo+UZNObWtEiWGok/vDN8BMoEQi41s0znSes6E1Q2aag3Lw3u2J1w2rqH7uF2ws6FpQhfSOA==' -->
|
||||
<!-- The domain here is intentionally served with `www`. In the event that the integrity check fails,
|
||||
the request should be disallowed by the source list. If we were to use {{domains[]}},
|
||||
then we would not be able to observe the difference with regards to the integrity check -->
|
||||
<!-- ShA256 is intentionally mixed case -->
|
||||
</head>
|
||||
|
||||
|
@ -18,6 +21,8 @@
|
|||
var port = "{{ports[http][0]}}";
|
||||
if (location.protocol === "https:")
|
||||
port = "{{ports[https][0]}}";
|
||||
// Since {{domains[www]}} is allowed by the CSP policy, regardless of the integrity check
|
||||
// the request would be allowed.
|
||||
var crossorigin_base = location.protocol + "//{{domains[www]}}:" + port;
|
||||
|
||||
// Test name, src, integrity, expected to run.
|
||||
|
|
|
@ -2,11 +2,12 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<title>Parser-inserted scripts without a correct nonce are not allowed with `strict-dynamic` in the script-src directive.</title>
|
||||
<title>Parser-inserted scripts without a correct nonce are not allowed with `Strict-Dynamic` in the script-src directive.</title>
|
||||
<script src='/resources/testharness.js' nonce='dummy'></script>
|
||||
<script src='/resources/testharnessreport.js' nonce='dummy'></script>
|
||||
|
||||
<!-- CSP served: script-src 'strict-dynamic' 'nonce-dummy' -->
|
||||
<!-- CSP served: script-src 'Strict-Dynamic' 'nonce-dummy' -->
|
||||
<!-- Strict-Dynamic is intentionally mixed case -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
|
@ -2,4 +2,4 @@ Expires: Mon, 26 Jul 1997 05:00:00 GMT
|
|||
Cache-Control: no-store, no-cache, must-revalidate
|
||||
Cache-Control: post-check=0, pre-check=0, false
|
||||
Pragma: no-cache
|
||||
Content-Security-Policy: script-src 'strict-dynamic' 'nonce-dummy'
|
||||
Content-Security-Policy: script-src 'Strict-Dynamic' 'nonce-dummy'
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue