aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorTim van der Lippe <TimvdLippe@users.noreply.github.com>2025-05-02 22:13:31 +0200
committerGitHub <noreply@github.com>2025-05-02 20:13:31 +0000
commitdd63325f50e21602c79f3a35347efbfedd4d8231 (patch)
tree62a694210db4a670035d9485497e2b217bdac301
parentb8971e528f6ed9affb7e8aaaea17b8fc0b929af7 (diff)
downloadservo-dd63325f50e21602c79f3a35347efbfedd4d8231.tar.gz
servo-dd63325f50e21602c79f3a35347efbfedd4d8231.zip
Check CSP for `javascript:` URLs (#36709)
Also update a WPT test to fail-fast if the iframe incorrectly evaluates the `eval`. Before, it would run into a timeout if the implementation is correct. Now we reject the promise when an exception is thrown. Requires servo/rust-content-security-policy#6 Signed-off-by: Tim van der Lippe <tvanderlippe@gmail.com>
-rw-r--r--Cargo.lock2
-rw-r--r--components/script/dom/globalscope.rs35
-rw-r--r--components/script/dom/htmliframeelement.rs7
-rw-r--r--components/script/script_thread.rs9
-rw-r--r--tests/wpt/meta/MANIFEST.json2
-rw-r--r--tests/wpt/meta/content-security-policy/navigation/to-javascript-parent-initiated-child-csp.html.ini5
-rw-r--r--tests/wpt/meta/content-security-policy/navigation/to-javascript-parent-initiated-parent-csp.html.ini3
-rw-r--r--tests/wpt/meta/content-security-policy/script-src/script-src-strict_dynamic_javascript_uri.html.ini4
-rw-r--r--tests/wpt/meta/content-security-policy/script-src/script-src-trusted_types_eval_with_require_trusted_types_eval.html.ini13
-rw-r--r--tests/wpt/meta/content-security-policy/securitypolicyviolation/linenumber.tentative.html.ini3
-rw-r--r--tests/wpt/meta/content-security-policy/unsafe-eval/eval-blocked-in-about-blank-iframe.html.ini3
-rw-r--r--tests/wpt/meta/content-security-policy/unsafe-hashes/javascript_src_denied_missing_unsafe_hashes-href.html.ini3
-rw-r--r--tests/wpt/meta/content-security-policy/unsafe-hashes/javascript_src_denied_missing_unsafe_hashes-window_location.html.ini3
-rw-r--r--tests/wpt/meta/content-security-policy/unsafe-hashes/javascript_src_denied_wrong_hash-href.html.ini3
-rw-r--r--tests/wpt/meta/content-security-policy/unsafe-hashes/javascript_src_denied_wrong_hash-window_location.html.ini3
-rw-r--r--tests/wpt/tests/content-security-policy/unsafe-eval/eval-blocked-in-about-blank-iframe.html29
16 files changed, 70 insertions, 57 deletions
diff --git a/Cargo.lock b/Cargo.lock
index cecdada6176..b8ac73f4f73 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1223,7 +1223,7 @@ dependencies = [
[[package]]
name = "content-security-policy"
version = "0.5.4"
-source = "git+https://github.com/servo/rust-content-security-policy/?branch=servo-csp#81f95254fbfe98dd6e130260fd872cf950de9fcd"
+source = "git+https://github.com/servo/rust-content-security-policy/?branch=servo-csp#fcd91e99139ca96629e04e1a8010f96374f0370f"
dependencies = [
"base64 0.22.1",
"bitflags 2.9.0",
diff --git a/components/script/dom/globalscope.rs b/components/script/dom/globalscope.rs
index f097642f424..527d03eed4e 100644
--- a/components/script/dom/globalscope.rs
+++ b/components/script/dom/globalscope.rs
@@ -18,11 +18,12 @@ use base::id::{
ServiceWorkerId, ServiceWorkerRegistrationId, WebViewId,
};
use constellation_traits::{
- BlobData, BlobImpl, BroadcastMsg, FileBlob, MessagePortImpl, MessagePortMsg, PortMessageTask,
- ScriptToConstellationChan, ScriptToConstellationMessage,
+ BlobData, BlobImpl, BroadcastMsg, FileBlob, LoadData, LoadOrigin, MessagePortImpl,
+ MessagePortMsg, PortMessageTask, ScriptToConstellationChan, ScriptToConstellationMessage,
};
use content_security_policy::{
- CheckResult, CspList, PolicyDisposition, PolicySource, Violation, ViolationResource,
+ CheckResult, CspList, Destination, Initiator, NavigationCheckType, ParserMetadata,
+ PolicyDisposition, PolicySource, Request, Violation, ViolationResource,
};
use crossbeam_channel::Sender;
use devtools_traits::{PageError, ScriptToDevtoolsControlMsg};
@@ -62,6 +63,7 @@ use profile_traits::{ipc as profile_ipc, mem as profile_mem, time as profile_tim
use script_bindings::interfaces::GlobalScopeHelpers;
use servo_url::{ImmutableOrigin, MutableOrigin, ServoUrl};
use timers::{TimerEventId, TimerEventRequest, TimerSource};
+use url::Origin;
use uuid::Uuid;
#[cfg(feature = "webgpu")]
use webgpu_traits::{DeviceLostReason, WebGPUDevice};
@@ -2951,6 +2953,33 @@ impl GlobalScope {
is_js_evaluation_allowed == CheckResult::Allowed
}
+ pub(crate) fn should_navigation_request_be_blocked(&self, load_data: &LoadData) -> bool {
+ let Some(csp_list) = self.get_csp_list() else {
+ return false;
+ };
+ let request = Request {
+ url: load_data.url.clone().into_url(),
+ origin: match &load_data.load_origin {
+ LoadOrigin::Script(immutable_origin) => immutable_origin.clone().into_url_origin(),
+ _ => Origin::new_opaque(),
+ },
+ // TODO: populate this field correctly
+ redirect_count: 0,
+ destination: Destination::None,
+ initiator: Initiator::None,
+ nonce: "".to_owned(),
+ integrity_metadata: "".to_owned(),
+ parser_metadata: ParserMetadata::None,
+ };
+ // TODO: set correct navigation check type for form submission if applicable
+ let (result, violations) =
+ csp_list.should_navigation_request_be_blocked(&request, NavigationCheckType::Other);
+
+ self.report_csp_violations(violations);
+
+ result == CheckResult::Blocked
+ }
+
pub(crate) fn create_image_bitmap(
&self,
image: ImageBitmapSource,
diff --git a/components/script/dom/htmliframeelement.rs b/components/script/dom/htmliframeelement.rs
index c5194c4527f..da7a53bbf0b 100644
--- a/components/script/dom/htmliframeelement.rs
+++ b/components/script/dom/htmliframeelement.rs
@@ -162,8 +162,13 @@ impl HTMLIFrameElement {
if load_data.url.scheme() == "javascript" {
let window_proxy = self.GetContentWindow();
if let Some(window_proxy) = window_proxy {
+ if document
+ .global()
+ .should_navigation_request_be_blocked(&load_data)
+ {
+ return;
+ }
// Important re security. See https://github.com/servo/servo/issues/23373
- // TODO: check according to https://w3c.github.io/webappsec-csp/#should-block-navigation-request
if ScriptThread::check_load_origin(&load_data.load_origin, &document.url().origin())
{
ScriptThread::eval_js_url(&window_proxy.global(), &mut load_data, can_gc);
diff --git a/components/script/script_thread.rs b/components/script/script_thread.rs
index 382ab94d893..bd4de9d893b 100644
--- a/components/script/script_thread.rs
+++ b/components/script/script_thread.rs
@@ -598,7 +598,7 @@ impl ScriptThread {
with_script_thread(|script_thread| {
let is_javascript = load_data.url.scheme() == "javascript";
// If resource is a request whose url's scheme is "javascript"
- // https://html.spec.whatwg.org/multipage/#javascript-protocol
+ // https://html.spec.whatwg.org/multipage/#navigate-to-a-javascript:-url
if is_javascript {
let window = match script_thread.documents.borrow().find_window(pipeline_id) {
None => return,
@@ -612,8 +612,12 @@ impl ScriptThread {
.clone();
let task = task!(navigate_javascript: move || {
// Important re security. See https://github.com/servo/servo/issues/23373
- // TODO: check according to https://w3c.github.io/webappsec-csp/#should-block-navigation-request
if let Some(window) = trusted_global.root().downcast::<Window>() {
+ // Step 5: If the result of should navigation request of type be blocked by
+ // Content Security Policy? given request and cspNavigationType is "Blocked", then return. [CSP]
+ if trusted_global.root().should_navigation_request_be_blocked(&load_data) {
+ return;
+ }
if ScriptThread::check_load_origin(&load_data.load_origin, &window.get_url().origin()) {
ScriptThread::eval_js_url(&trusted_global.root(), &mut load_data, CanGc::note());
sender
@@ -622,6 +626,7 @@ impl ScriptThread {
}
}
});
+ // Step 19 of <https://html.spec.whatwg.org/multipage/#navigate>
global
.task_manager()
.dom_manipulation_task_source()
diff --git a/tests/wpt/meta/MANIFEST.json b/tests/wpt/meta/MANIFEST.json
index c74943bb031..e786bb85e80 100644
--- a/tests/wpt/meta/MANIFEST.json
+++ b/tests/wpt/meta/MANIFEST.json
@@ -571755,7 +571755,7 @@
]
],
"eval-blocked-in-about-blank-iframe.html": [
- "054e75b52749b37530a02bc5ee0119ca5e76a474",
+ "b2286f56a234a515cddec0e02439f9768e3a2905",
[
null,
{}
diff --git a/tests/wpt/meta/content-security-policy/navigation/to-javascript-parent-initiated-child-csp.html.ini b/tests/wpt/meta/content-security-policy/navigation/to-javascript-parent-initiated-child-csp.html.ini
index e09851b2685..3eb78514061 100644
--- a/tests/wpt/meta/content-security-policy/navigation/to-javascript-parent-initiated-child-csp.html.ini
+++ b/tests/wpt/meta/content-security-policy/navigation/to-javascript-parent-initiated-child-csp.html.ini
@@ -1,10 +1,7 @@
[to-javascript-parent-initiated-child-csp.html]
expected: TIMEOUT
- [Should not have executed the javascript URL for\n iframe.contentWindow.location.href with child's CSP "script-src 'none'"]
- expected: TIMEOUT
-
[Should not have executed the javascript URL for\n iframe.src with child's CSP "script-src 'none'"]
- expected: NOTRUN
+ expected: TIMEOUT
[Should not have executed the javascript URL for\n otherTabWithScriptSrcNone.location.href with child's CSP "script-src 'none'"]
expected: NOTRUN
diff --git a/tests/wpt/meta/content-security-policy/navigation/to-javascript-parent-initiated-parent-csp.html.ini b/tests/wpt/meta/content-security-policy/navigation/to-javascript-parent-initiated-parent-csp.html.ini
index bf7d79650ae..09a7c444ac5 100644
--- a/tests/wpt/meta/content-security-policy/navigation/to-javascript-parent-initiated-parent-csp.html.ini
+++ b/tests/wpt/meta/content-security-policy/navigation/to-javascript-parent-initiated-parent-csp.html.ini
@@ -3,9 +3,6 @@
[Should not have executed the javascript url for\n iframe.contentWindow.location.href]
expected: FAIL
- [Should not have executed the javascript url for\n iframe.src]
- expected: FAIL
-
[Should not have executed the javascript url for\n otherTab.location.href]
expected: TIMEOUT
diff --git a/tests/wpt/meta/content-security-policy/script-src/script-src-strict_dynamic_javascript_uri.html.ini b/tests/wpt/meta/content-security-policy/script-src/script-src-strict_dynamic_javascript_uri.html.ini
deleted file mode 100644
index b16c3876c44..00000000000
--- a/tests/wpt/meta/content-security-policy/script-src/script-src-strict_dynamic_javascript_uri.html.ini
+++ /dev/null
@@ -1,4 +0,0 @@
-[script-src-strict_dynamic_javascript_uri.html]
- expected: TIMEOUT
- [Script injected via `javascript:` URIs are not allowed with `strict-dynamic`.]
- expected: TIMEOUT
diff --git a/tests/wpt/meta/content-security-policy/script-src/script-src-trusted_types_eval_with_require_trusted_types_eval.html.ini b/tests/wpt/meta/content-security-policy/script-src/script-src-trusted_types_eval_with_require_trusted_types_eval.html.ini
deleted file mode 100644
index 63f89f3c8fd..00000000000
--- a/tests/wpt/meta/content-security-policy/script-src/script-src-trusted_types_eval_with_require_trusted_types_eval.html.ini
+++ /dev/null
@@ -1,13 +0,0 @@
-[script-src-trusted_types_eval_with_require_trusted_types_eval.html]
- expected: ERROR
- [Script injected via direct `eval` is allowed with `trusted-types-eval` and `require-trusted-types-for 'script'`.]
- expected: FAIL
-
- [Script injected via indirect `eval` is allowed with `trusted-types-eval` and `require-trusted-types-for 'script'`.]
- expected: FAIL
-
- [Script injected via `new Function` is allowed with `trusted-types-eval` and `require-trusted-types-for 'script'`.]
- expected: FAIL
-
- [Script injected via `setTimeout` is allowed with `trusted-types-eval` and `require-trusted-types-for 'script'`.]
- expected: FAIL
diff --git a/tests/wpt/meta/content-security-policy/securitypolicyviolation/linenumber.tentative.html.ini b/tests/wpt/meta/content-security-policy/securitypolicyviolation/linenumber.tentative.html.ini
index e8114229ab9..fed78a0aa49 100644
--- a/tests/wpt/meta/content-security-policy/securitypolicyviolation/linenumber.tentative.html.ini
+++ b/tests/wpt/meta/content-security-policy/securitypolicyviolation/linenumber.tentative.html.ini
@@ -1,3 +1,4 @@
[linenumber.tentative.html]
+ expected: TIMEOUT
[linenumber]
- expected: FAIL
+ expected: NOTRUN
diff --git a/tests/wpt/meta/content-security-policy/unsafe-eval/eval-blocked-in-about-blank-iframe.html.ini b/tests/wpt/meta/content-security-policy/unsafe-eval/eval-blocked-in-about-blank-iframe.html.ini
index d47972783e3..3a9bd2e206d 100644
--- a/tests/wpt/meta/content-security-policy/unsafe-eval/eval-blocked-in-about-blank-iframe.html.ini
+++ b/tests/wpt/meta/content-security-policy/unsafe-eval/eval-blocked-in-about-blank-iframe.html.ini
@@ -1,4 +1,3 @@
[eval-blocked-in-about-blank-iframe.html]
- expected: ERROR
[eval-blocked-in-about-blank-iframe]
- expected: TIMEOUT
+ expected: FAIL
diff --git a/tests/wpt/meta/content-security-policy/unsafe-hashes/javascript_src_denied_missing_unsafe_hashes-href.html.ini b/tests/wpt/meta/content-security-policy/unsafe-hashes/javascript_src_denied_missing_unsafe_hashes-href.html.ini
deleted file mode 100644
index 4496339f476..00000000000
--- a/tests/wpt/meta/content-security-policy/unsafe-hashes/javascript_src_denied_missing_unsafe_hashes-href.html.ini
+++ /dev/null
@@ -1,3 +0,0 @@
-[javascript_src_denied_missing_unsafe_hashes-href.html]
- [javascript: navigation using <a href> should be refused due to missing unsafe-hashes]
- expected: FAIL
diff --git a/tests/wpt/meta/content-security-policy/unsafe-hashes/javascript_src_denied_missing_unsafe_hashes-window_location.html.ini b/tests/wpt/meta/content-security-policy/unsafe-hashes/javascript_src_denied_missing_unsafe_hashes-window_location.html.ini
deleted file mode 100644
index 5d8a7e6683b..00000000000
--- a/tests/wpt/meta/content-security-policy/unsafe-hashes/javascript_src_denied_missing_unsafe_hashes-window_location.html.ini
+++ /dev/null
@@ -1,3 +0,0 @@
-[javascript_src_denied_missing_unsafe_hashes-window_location.html]
- [Test that the javascript: src is not allowed to run]
- expected: FAIL
diff --git a/tests/wpt/meta/content-security-policy/unsafe-hashes/javascript_src_denied_wrong_hash-href.html.ini b/tests/wpt/meta/content-security-policy/unsafe-hashes/javascript_src_denied_wrong_hash-href.html.ini
deleted file mode 100644
index 6f785e1732f..00000000000
--- a/tests/wpt/meta/content-security-policy/unsafe-hashes/javascript_src_denied_wrong_hash-href.html.ini
+++ /dev/null
@@ -1,3 +0,0 @@
-[javascript_src_denied_wrong_hash-href.html]
- [javascript: navigation using <a href> should be refused due to wrong hash]
- expected: FAIL
diff --git a/tests/wpt/meta/content-security-policy/unsafe-hashes/javascript_src_denied_wrong_hash-window_location.html.ini b/tests/wpt/meta/content-security-policy/unsafe-hashes/javascript_src_denied_wrong_hash-window_location.html.ini
deleted file mode 100644
index 413b1198c61..00000000000
--- a/tests/wpt/meta/content-security-policy/unsafe-hashes/javascript_src_denied_wrong_hash-window_location.html.ini
+++ /dev/null
@@ -1,3 +0,0 @@
-[javascript_src_denied_wrong_hash-window_location.html]
- [Test that the javascript: src is not allowed to run]
- expected: FAIL
diff --git a/tests/wpt/tests/content-security-policy/unsafe-eval/eval-blocked-in-about-blank-iframe.html b/tests/wpt/tests/content-security-policy/unsafe-eval/eval-blocked-in-about-blank-iframe.html
index 054e75b5274..b2286f56a23 100644
--- a/tests/wpt/tests/content-security-policy/unsafe-eval/eval-blocked-in-about-blank-iframe.html
+++ b/tests/wpt/tests/content-security-policy/unsafe-eval/eval-blocked-in-about-blank-iframe.html
@@ -19,18 +19,27 @@
const document_loaded = new Promise(resolve => window.onload = resolve);
await document_loaded;
- const eval_error = new Promise(resolve => {
- window.addEventListener('message', function(e) {
- assert_not_equals(e.data, 'FAIL', 'eval was executed in the frame');
- if (e.data === 'PASS')
- resolve();
+ const eval_error = new Promise((resolve, reject) => {
+ window.addEventListener('message', function(event) {
+ try {
+ assert_not_equals(event.data, 'FAIL', 'eval was executed in the frame');
+ if (event.data === 'PASS') {
+ resolve();
+ }
+ } catch (e) {
+ reject(e);
+ }
});
});
- const csp_violation_report = new Promise(resolve => {
- window.addEventListener('message', function(e) {
- if (e.data["violated-directive"]) {
- assert_equals(e.data["violated-directive"], "script-src");
- resolve();
+ const csp_violation_report = new Promise((resolve, reject) => {
+ window.addEventListener('message', function(event) {
+ try {
+ if (event.data["violated-directive"]) {
+ assert_equals(event.data["violated-directive"], "script-src");
+ resolve();
+ }
+ } catch (e) {
+ reject(e);
}
});
});