diff options
author | Tim van der Lippe <TimvdLippe@users.noreply.github.com> | 2025-05-02 22:13:31 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-05-02 20:13:31 +0000 |
commit | dd63325f50e21602c79f3a35347efbfedd4d8231 (patch) | |
tree | 62a694210db4a670035d9485497e2b217bdac301 | |
parent | b8971e528f6ed9affb7e8aaaea17b8fc0b929af7 (diff) | |
download | servo-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>
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); } }); }); |