mirror of
https://github.com/lexmount/moli.git
synced 2026-10-30 16:07:24 +00:00
fix(forms): enforce sandbox checks for ancestor navigation
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
use crate::blob;
|
||||
use crate::context_bootstrap::sandbox_blocks_ancestor_or_top_navigation_from_source;
|
||||
use crate::native_bridge::context_host::ChildBrowsingContextNavigationRequest;
|
||||
use crate::native_bridge::element::activation::{
|
||||
SpecialBrowsingContextTarget, browsing_context_window_for_dispatch_scope,
|
||||
@@ -621,11 +622,22 @@ pub(in crate::native_bridge) fn submit_form_default_action(
|
||||
let source_popup = source_document.and_then(|document| {
|
||||
unsafe { &*runtime_ptr }.lightweight_popup_id_for_document_handle(document)
|
||||
});
|
||||
// Capture the form Document's authority before formdata can replace its
|
||||
// frame. Both methods must stop before navigation events or seed updates.
|
||||
let sandbox_blocks_navigation = form_sandbox_blocks_target_navigation(
|
||||
scope,
|
||||
runtime_ptr,
|
||||
source_document,
|
||||
target_name.as_deref(),
|
||||
);
|
||||
let Some(request) =
|
||||
build_form_submission_request(scope, runtime_ptr, form_handle, submitter, action)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if sandbox_blocks_navigation {
|
||||
return true;
|
||||
}
|
||||
|
||||
let special_target = target_name
|
||||
.as_deref()
|
||||
@@ -785,6 +797,35 @@ pub(in crate::native_bridge) fn submit_form_default_action(
|
||||
}
|
||||
}
|
||||
|
||||
fn form_sandbox_blocks_target_navigation(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
runtime_ptr: *mut JsContextHost,
|
||||
source_document: Option<DomHandle>,
|
||||
target_name: Option<&str>,
|
||||
) -> bool {
|
||||
let target = source_document.and_then(|document| {
|
||||
let runtime = unsafe { &*runtime_ptr };
|
||||
let source_frame = runtime.child_browsing_context_host_for_document_handle(document)?;
|
||||
let target_document = form_navigation_target_document(runtime, document, target_name)?;
|
||||
let target_scope = if target_document == runtime.document_handle() {
|
||||
crate::native_bridge::OwnerDispatchScope::Top
|
||||
} else {
|
||||
crate::native_bridge::OwnerDispatchScope::Child(
|
||||
runtime.child_browsing_context_host_for_document_handle(target_document)?,
|
||||
)
|
||||
};
|
||||
Some((source_frame, target_scope))
|
||||
});
|
||||
let Some((source_frame, target_scope)) = target else {
|
||||
return false;
|
||||
};
|
||||
let Some(window) = browsing_context_window_for_dispatch_scope(scope, runtime_ptr, target_scope)
|
||||
else {
|
||||
return true;
|
||||
};
|
||||
sandbox_blocks_ancestor_or_top_navigation_from_source(scope, source_frame, window)
|
||||
}
|
||||
|
||||
fn submit_dialog_form(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
runtime_ptr: *mut JsContextHost,
|
||||
|
||||
@@ -355,6 +355,269 @@ fn hyperlink_top_navigation_checks_source_document_sandbox_across_realms() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_top_navigation_checks_active_source_document_sandbox_across_realms() {
|
||||
for (depth, target) in [(1, "_top"), (1, "_PARENT"), (2, "_ToP")] {
|
||||
for method in ["get", "post"] {
|
||||
for api in ["submit", "requestSubmit", "click"] {
|
||||
for use_child_realm in [true, false] {
|
||||
for intercept in [false, true] {
|
||||
for (source_is_child, initially_allowed, mutation) in [
|
||||
(true, false, "none"),
|
||||
(true, true, "none"),
|
||||
(false, false, "none"),
|
||||
(true, false, "remove"),
|
||||
(true, true, "tighten"),
|
||||
(true, false, "reload"),
|
||||
] {
|
||||
let description = format!(
|
||||
"depth={depth} target={target} method={method} api={api} \
|
||||
child_realm={use_child_realm} intercept={intercept} \
|
||||
source_is_child={source_is_child} initially_allowed={initially_allowed} \
|
||||
mutation={mutation}"
|
||||
);
|
||||
let mut vm = new_parsed_test_vm(
|
||||
"https://form-navigation-sandbox.test/source",
|
||||
"<!doctype html><body></body>",
|
||||
);
|
||||
vm.eval(&format!(
|
||||
r#"
|
||||
(() => {{
|
||||
const log = [];
|
||||
let childDocument = document;
|
||||
const frames = [];
|
||||
for (let i = 0; i < {depth}; ++i) {{
|
||||
const frame = childDocument.createElement('iframe');
|
||||
frame.sandbox = 'allow-scripts allow-same-origin allow-forms' +
|
||||
({initially_allowed} ? ' allow-top-navigation' : '');
|
||||
frame.srcdoc = '<body></body>';
|
||||
childDocument.body.appendChild(frame);
|
||||
frames.push(frame);
|
||||
childDocument = frame.contentDocument;
|
||||
}}
|
||||
for (const frame of frames) {{
|
||||
if ({mutation:?} === 'remove') frame.removeAttribute('sandbox');
|
||||
if ({mutation:?} === 'tighten') frame.sandbox = 'allow-scripts allow-same-origin allow-forms';
|
||||
}}
|
||||
const source = {source_is_child} ? childDocument : document;
|
||||
const form = source.createElement('form');
|
||||
form.method = {method:?};
|
||||
form.action = 'https://form-navigation-sandbox.test/submitted';
|
||||
form.innerHTML = '<input name="value" value="b"><button>submit</button>';
|
||||
const button = form.querySelector('button');
|
||||
if ({api:?} === 'click') button.formTarget = {target:?};
|
||||
else form.target = {target:?};
|
||||
source.body.appendChild(form);
|
||||
let formDataEvents = 0;
|
||||
form.onformdata = () => {{
|
||||
++formDataEvents;
|
||||
if ({mutation:?} === 'reload') {{
|
||||
for (const frame of frames) {{
|
||||
frame.sandbox = 'allow-scripts allow-same-origin allow-forms allow-top-navigation';
|
||||
frame.srcdoc = '<body>replacement</body>';
|
||||
}}
|
||||
}}
|
||||
}};
|
||||
const from = navigation.currentEntry;
|
||||
navigation.onnavigate = event => {{
|
||||
log.push('navigate');
|
||||
if ({intercept}) {{
|
||||
if (event.canIntercept) event.intercept({{
|
||||
handler() {{ log.push('intercept'); }}
|
||||
}});
|
||||
else event.preventDefault();
|
||||
}}
|
||||
}};
|
||||
globalThis.formSandboxSnapshot = () => ({{
|
||||
log,
|
||||
formDataEvents,
|
||||
href: location.href,
|
||||
entryUnchanged: navigation.currentEntry === from
|
||||
}});
|
||||
const realm = {use_child_realm} ? childDocument.defaultView : window;
|
||||
if ({api:?} === 'click') realm.HTMLElement.prototype.click.call(button);
|
||||
else realm.HTMLFormElement.prototype[{api:?}].call(form);
|
||||
}})()
|
||||
"#,
|
||||
))
|
||||
.expect(&description);
|
||||
let allowed = !source_is_child || initially_allowed;
|
||||
let expected_url = if method == "get" {
|
||||
"https://form-navigation-sandbox.test/submitted?value=b"
|
||||
} else {
|
||||
"https://form-navigation-sandbox.test/submitted"
|
||||
};
|
||||
let committed = allowed && intercept && method == "get";
|
||||
let expected_log = if committed {
|
||||
vec!["navigate", "intercept"]
|
||||
} else if allowed {
|
||||
vec!["navigate"]
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let snapshot = vm
|
||||
.eval("JSON.stringify(formSandboxSnapshot())")
|
||||
.expect(&description);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<serde_json::Value>(&snapshot).unwrap(),
|
||||
serde_json::json!({
|
||||
"log": expected_log,
|
||||
"formDataEvents": 1,
|
||||
"href": if committed { expected_url } else {
|
||||
"https://form-navigation-sandbox.test/source"
|
||||
},
|
||||
"entryUnchanged": !committed
|
||||
}),
|
||||
"{description}"
|
||||
);
|
||||
let pending = vm.take_pending_location_navigation_with_seed();
|
||||
if allowed && !intercept {
|
||||
let pending = pending.expect(&description);
|
||||
assert_eq!(pending.url.as_str(), expected_url, "{description}");
|
||||
assert_eq!(
|
||||
pending.request_method,
|
||||
method.to_ascii_uppercase(),
|
||||
"{description}"
|
||||
);
|
||||
assert_eq!(
|
||||
pending.request_body.as_deref(),
|
||||
(method == "post").then_some(b"value=b".as_slice()),
|
||||
"{description}"
|
||||
);
|
||||
} else {
|
||||
assert!(pending.is_none(), "{description}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sandboxed_form_parent_navigation_does_not_change_ancestor_seed() {
|
||||
for method in ["get", "post"] {
|
||||
for api in ["submit", "requestSubmit", "click"] {
|
||||
for use_child_realm in [true, false] {
|
||||
let description =
|
||||
format!("method={method} api={api} child_realm={use_child_realm}");
|
||||
let mut vm = new_parsed_test_vm(
|
||||
"https://form-navigation-sandbox.test/source",
|
||||
"<!doctype html><body></body>",
|
||||
);
|
||||
let result = vm
|
||||
.eval(&format!(
|
||||
r#"(() => {{
|
||||
const outer = document.createElement('iframe');
|
||||
outer.srcdoc = '<body>parent</body>';
|
||||
document.body.appendChild(outer);
|
||||
const parentWindow = outer.contentWindow;
|
||||
const parentDocument = outer.contentDocument;
|
||||
const inner = parentDocument.createElement('iframe');
|
||||
inner.sandbox = 'allow-scripts allow-same-origin allow-forms';
|
||||
inner.srcdoc = '<body>child</body>';
|
||||
parentDocument.body.appendChild(inner);
|
||||
const source = inner.contentDocument;
|
||||
const form = source.createElement('form');
|
||||
form.method = {method:?};
|
||||
form.action = 'https://form-navigation-sandbox.test/submitted';
|
||||
form.innerHTML = '<input name="value" value="b"><button>submit</button>';
|
||||
const button = form.querySelector('button');
|
||||
if ({api:?} === 'click') button.formTarget = '_PaReNt';
|
||||
else form.target = '_PaReNt';
|
||||
source.body.appendChild(form);
|
||||
const before = parentWindow.location.href;
|
||||
const entry = parentWindow.navigation.currentEntry;
|
||||
const events = [];
|
||||
parentWindow.navigation.onnavigate = () => events.push('parent');
|
||||
navigation.onnavigate = () => events.push('top');
|
||||
const realm = {use_child_realm} ? source.defaultView : window;
|
||||
if ({api:?} === 'click') realm.HTMLElement.prototype.click.call(button);
|
||||
else realm.HTMLFormElement.prototype[{api:?}].call(form);
|
||||
return JSON.stringify([
|
||||
events, parentWindow.location.href === before,
|
||||
parentWindow.navigation.currentEntry === entry,
|
||||
parentWindow.document === parentDocument
|
||||
]);
|
||||
}})()"#,
|
||||
))
|
||||
.expect(&description);
|
||||
assert_eq!(result, "[[],true,true,true]", "{description}");
|
||||
assert!(
|
||||
vm.take_pending_location_navigation_with_seed().is_none(),
|
||||
"{description}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opaque_sandbox_forms_cannot_navigate_top_or_parent() {
|
||||
for sandbox in ["allow-scripts", "allow-scripts allow-forms"] {
|
||||
for method in ["get", "post"] {
|
||||
for target in ["_TOP", "_parent"] {
|
||||
for api in ["submit", "requestSubmit", "click"] {
|
||||
let description =
|
||||
format!("sandbox={sandbox} method={method} target={target} api={api}");
|
||||
let mut vm = new_parsed_test_vm(
|
||||
"https://form-navigation-sandbox.test/source",
|
||||
"<!doctype html><body></body>",
|
||||
);
|
||||
vm.eval(&format!(
|
||||
r#"globalThis.formNavigationEvents = [];
|
||||
navigation.onnavigate = event => {{
|
||||
formNavigationEvents.push('navigate');
|
||||
event.preventDefault();
|
||||
}};
|
||||
const frame = document.createElement('iframe');
|
||||
frame.sandbox = {sandbox:?};
|
||||
frame.srcdoc = '<body>opaque child</body>';
|
||||
document.body.appendChild(frame);"#,
|
||||
))
|
||||
.expect(&description);
|
||||
vm.drain_pending_child_frame_work_for_test();
|
||||
assert_eq!(
|
||||
vm.eval("frame.contentDocument === null")
|
||||
.expect(&description),
|
||||
"true",
|
||||
"{description}"
|
||||
);
|
||||
let context_id = vm
|
||||
.live_child_default_runtime_realm_inventory()
|
||||
.into_iter()
|
||||
.next()
|
||||
.expect(&description)
|
||||
.context_id;
|
||||
vm.eval_in_child_default_context(context_id, &format!(
|
||||
r#"(() => {{
|
||||
const form = document.createElement('form');
|
||||
form.method = {method:?};
|
||||
form.target = {target:?};
|
||||
form.action = 'https://form-navigation-sandbox.test/submitted';
|
||||
form.innerHTML = '<input name="value" value="b"><button>submit</button>';
|
||||
document.body.appendChild(form);
|
||||
if ({api:?} === 'click') form.querySelector('button').click();
|
||||
else form[{api:?}]();
|
||||
}})()"#,
|
||||
)).expect(&description);
|
||||
assert_eq!(
|
||||
vm.eval("JSON.stringify([formNavigationEvents, location.href])")
|
||||
.expect(&description),
|
||||
r#"[[],"https://form-navigation-sandbox.test/source"]"#,
|
||||
"{description}"
|
||||
);
|
||||
assert!(
|
||||
vm.take_pending_location_navigation_with_seed().is_none(),
|
||||
"{description}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_navigation_events_use_resolved_top_window_across_realms() {
|
||||
for (depth, target) in [(1, "_top"), (1, "_PARENT"), (2, "_ToP")] {
|
||||
|
||||
Reference in New Issue
Block a user