mirror of
https://github.com/lexmount/moli.git
synced 2026-10-09 08:01:05 +00:00
fix(media): block dangling markup source URLs
This commit is contained in:
@@ -2906,7 +2906,6 @@ fetch/api/policies/referrer-origin-when-cross-origin-worker.html
|
||||
fetch/api/policies/referrer-origin-worker.html
|
||||
fetch/api/policies/referrer-unsafe-url-worker.html
|
||||
fetch/nosniff/importscripts.html
|
||||
fetch/security/dangling-markup/media.html
|
||||
fetch/security/dangling-markup/option.html
|
||||
fetch/security/dangling-markup/textarea.html
|
||||
focus/activeelement-after-focusing-different-site-iframe-contentwindow.html
|
||||
|
||||
@@ -4906,6 +4906,7 @@ fetch/api/response/multi-globals/url-parsing.html
|
||||
fetch/api/response/response-body-read-task-handling.html
|
||||
fetch/api/response/response-form-data.html
|
||||
fetch/content-length/content-length.html
|
||||
fetch/security/dangling-markup/media.html
|
||||
focus/activeelement-after-focusing-different-site-iframe-then-immediately-focusing-back.html
|
||||
focus/anchor-remove-href.html
|
||||
focus/focus-already-focused-iframe-deep-different-site.html
|
||||
|
||||
@@ -652,7 +652,7 @@ use url_attributes::{
|
||||
iframe_has_inactive_child_context, iframe_is_in_own_child_document,
|
||||
iframe_is_inside_its_own_child_context_document, iframe_uses_detached_content_cache,
|
||||
normalize_url_default_port, parsed_url_like_attribute, resolve_url_like_attribute,
|
||||
set_resolved_url_attribute,
|
||||
set_resolved_url_attribute, should_block_dangling_markup_subresource,
|
||||
};
|
||||
|
||||
#[derive(WebApiFunctionTemplate)]
|
||||
|
||||
@@ -6,6 +6,7 @@ use super::super::{
|
||||
html_media_element_setter_receiver, parsed_url_like_attribute, property_dom_string_value,
|
||||
property_usv_string_value, remove_reflected_attribute, resolve_url_like_attribute,
|
||||
set_reflected_attribute, set_reflected_boolean_attribute,
|
||||
should_block_dangling_markup_subresource,
|
||||
};
|
||||
use crate::document_runtime::DocumentSubresourceCspKind;
|
||||
use crate::native_bridge::{
|
||||
@@ -444,7 +445,7 @@ fn start_media_load(
|
||||
scope: &mut v8::PinScope<'_, '_>,
|
||||
runtime_ptr: *mut JsContextHost,
|
||||
handle: crate::document_runtime::DomHandle,
|
||||
selected_source: Option<url::Url>,
|
||||
selected_source: Option<SelectedMediaSource>,
|
||||
) {
|
||||
let pending = {
|
||||
let runtime = unsafe { &mut *runtime_ptr };
|
||||
@@ -472,7 +473,8 @@ fn start_media_load(
|
||||
}
|
||||
let start = selected_source
|
||||
.ok_or_else(|| "media resource selection found no supported URL".to_owned())
|
||||
.and_then(|request_url| {
|
||||
.and_then(|selected_source| {
|
||||
let request_url = selected_source.request_url;
|
||||
if unsafe { &mut *runtime_ptr }
|
||||
.check_top_document_subresource_csp(
|
||||
scope,
|
||||
@@ -485,6 +487,14 @@ fn start_media_load(
|
||||
successful: false,
|
||||
});
|
||||
}
|
||||
if should_block_dangling_markup_subresource(
|
||||
&request_url,
|
||||
&selected_source.original_input,
|
||||
) {
|
||||
return Ok(crate::network_host::MediaElementResourceFetchStart::Local {
|
||||
successful: false,
|
||||
});
|
||||
}
|
||||
crate::network_host::start_media_element_resource_fetch(
|
||||
scope,
|
||||
unsafe { &mut *runtime_ptr },
|
||||
@@ -760,10 +770,15 @@ fn media_load_target_for_source_change(
|
||||
.map(|_| parent)
|
||||
}
|
||||
|
||||
struct SelectedMediaSource {
|
||||
request_url: url::Url,
|
||||
original_input: String,
|
||||
}
|
||||
|
||||
fn selected_media_source(
|
||||
runtime: &JsContextHost,
|
||||
handle: crate::document_runtime::DomHandle,
|
||||
) -> Result<Option<url::Url>, ()> {
|
||||
) -> Result<Option<SelectedMediaSource>, ()> {
|
||||
let element = runtime
|
||||
.dom_host()
|
||||
.node(handle)
|
||||
@@ -774,7 +789,12 @@ fn selected_media_source(
|
||||
return Err(());
|
||||
}
|
||||
return parsed_url_like_attribute(runtime, handle, "src")
|
||||
.map(Some)
|
||||
.map(|request_url| {
|
||||
Some(SelectedMediaSource {
|
||||
request_url,
|
||||
original_input: src.to_owned(),
|
||||
})
|
||||
})
|
||||
.ok_or(());
|
||||
}
|
||||
for child in runtime.dom_host().child_handles(handle) {
|
||||
@@ -793,7 +813,12 @@ fn selected_media_source(
|
||||
continue;
|
||||
}
|
||||
return parsed_url_like_attribute(runtime, child, "src")
|
||||
.map(Some)
|
||||
.map(|request_url| {
|
||||
Some(SelectedMediaSource {
|
||||
request_url,
|
||||
original_input: src.to_owned(),
|
||||
})
|
||||
})
|
||||
.ok_or(());
|
||||
}
|
||||
Ok(None)
|
||||
|
||||
@@ -4,6 +4,7 @@ mod iframe;
|
||||
pub(super) use self::helpers::{
|
||||
default_port_for_scheme, normalize_url_default_port, parsed_url_like_attribute,
|
||||
resolve_url_like_attribute, set_resolved_url_attribute,
|
||||
should_block_dangling_markup_subresource,
|
||||
};
|
||||
pub(in crate::native_bridge) use self::iframe::update_iframe_snapshot_navigation;
|
||||
pub(super) use self::iframe::{
|
||||
|
||||
@@ -47,6 +47,20 @@ pub(in crate::native_bridge::element) fn parsed_url_like_attribute(
|
||||
parse_url_with_document_query_encoding(runtime, handle, &base, &value).ok()
|
||||
}
|
||||
|
||||
/// Reconstructs Chromium's URL-parser flag after `url::Url` has removed raw
|
||||
/// URL whitespace and percent-encoded the `<` in its serialized result.
|
||||
pub(in crate::native_bridge::element) fn should_block_dangling_markup_subresource(
|
||||
request_url: &Url,
|
||||
original_input: &str,
|
||||
) -> bool {
|
||||
matches!(request_url.scheme(), "http" | "https")
|
||||
&& original_input.as_bytes().contains(&b'<')
|
||||
&& original_input
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.any(|byte| matches!(byte, b'\r' | b'\n' | b'\t'))
|
||||
}
|
||||
|
||||
fn parse_url_with_document_query_encoding(
|
||||
runtime: &JsContextHost,
|
||||
handle: DomHandle,
|
||||
@@ -130,3 +144,35 @@ pub(in crate::native_bridge::element) fn set_resolved_url_attribute(
|
||||
) {
|
||||
set_reflected_attribute(scope, runtime_ptr, handle, name, url.as_ref());
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn dangling_markup_subresource_gate_preserves_url_parser_flag_semantics() {
|
||||
let http = Url::parse("https://example.test/resource").unwrap();
|
||||
assert!(should_block_dangling_markup_subresource(
|
||||
&http,
|
||||
"resource?\n<"
|
||||
));
|
||||
assert!(should_block_dangling_markup_subresource(
|
||||
&http,
|
||||
"resource?<\tkey"
|
||||
));
|
||||
assert!(!should_block_dangling_markup_subresource(
|
||||
&http,
|
||||
"resource?<"
|
||||
));
|
||||
assert!(!should_block_dangling_markup_subresource(
|
||||
&http,
|
||||
"resource?\r%3C"
|
||||
));
|
||||
|
||||
let data = Url::parse("data:text/plain,resource").unwrap();
|
||||
assert!(!should_block_dangling_markup_subresource(
|
||||
&data,
|
||||
"data:text/plain,resource\n<"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,6 +209,108 @@ async fn media_invalid_request_url_fails_before_load() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn media_blocks_http_dangling_markup_without_blocking_safe_url_inputs() {
|
||||
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
|
||||
let mut vm = new_storage_page_task_executor_test_vm_with_loader(
|
||||
"https://media-dangling-markup.test/page.html",
|
||||
&loader,
|
||||
);
|
||||
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
globalThis.__lmMediaDanglingMarkupEvents = [];
|
||||
const probes = [
|
||||
["audio", "blocked", "asset.mp3?\n<", false],
|
||||
["video", "blocked", "asset.mp4?<\tkey", false],
|
||||
["video", "child-source", "asset.mp4?\n<", true],
|
||||
["audio", "data", "data:audio/mp3,bytes\n<", false]
|
||||
];
|
||||
for (const [localName, name, source, useChildSource] of probes) {
|
||||
const media = document.createElement(localName);
|
||||
media.onerror = () => __lmMediaDanglingMarkupEvents.push(`${localName}:${name}:error`);
|
||||
media.oncanplay = () => __lmMediaDanglingMarkupEvents.push(`${localName}:${name}:canplay`);
|
||||
if (useChildSource) {
|
||||
const sourceElement = document.createElement("source");
|
||||
sourceElement.src = source;
|
||||
media.appendChild(sourceElement);
|
||||
} else {
|
||||
media.src = source;
|
||||
}
|
||||
(document.body || document.documentElement || document).appendChild(media);
|
||||
}
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("media dangling markup setup should evaluate");
|
||||
|
||||
for turn in 0..10 {
|
||||
run_next_page_media_element_event_for_test(
|
||||
&mut vm,
|
||||
&loader,
|
||||
&format!("dangling-markup media event turn {turn}"),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
vm.eval(r#"__lmMediaDanglingMarkupEvents.sort().join("|")"#)
|
||||
.expect("media dangling markup events should evaluate"),
|
||||
"audio:blocked:error|audio:data:canplay|video:blocked:error|video:child-source:error"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn media_dangling_markup_still_dispatches_report_only_csp() {
|
||||
let loader = ResourceRequestClient::new(&moli_fetch::FetchConfig::default()).expect("loader");
|
||||
let mut vm = new_storage_page_task_executor_test_vm_with_loader(
|
||||
"https://media-dangling-csp.test/page.html",
|
||||
&loader,
|
||||
);
|
||||
vm.set_response_content_security_report_only_policies(&["media-src 'none'".to_owned()]);
|
||||
|
||||
vm.eval(
|
||||
r#"
|
||||
(() => {
|
||||
globalThis.__lmMediaDanglingCspEvents = [];
|
||||
document.addEventListener("securitypolicyviolation", event => {
|
||||
__lmMediaDanglingCspEvents.push(`csp:${event.disposition}:${event.effectiveDirective}`);
|
||||
});
|
||||
const video = document.createElement("video");
|
||||
video.onerror = () => __lmMediaDanglingCspEvents.push("error");
|
||||
video.oncanplay = () => __lmMediaDanglingCspEvents.push("canplay");
|
||||
video.src = "asset.mp4?\n<";
|
||||
(document.body || document.documentElement || document).appendChild(video);
|
||||
})()
|
||||
"#,
|
||||
)
|
||||
.expect("media dangling markup CSP setup should evaluate");
|
||||
|
||||
assert_eq!(
|
||||
drain_pre_domcontentloaded_non_script_page_tasks_for_test(&mut vm),
|
||||
1
|
||||
);
|
||||
run_next_page_media_element_event_for_test(
|
||||
&mut vm,
|
||||
&loader,
|
||||
"report-only dangling-markup media loadstart turn",
|
||||
)
|
||||
.await;
|
||||
run_next_page_media_element_event_for_test(
|
||||
&mut vm,
|
||||
&loader,
|
||||
"report-only dangling-markup media error turn",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
vm.eval(r#"__lmMediaDanglingCspEvents.join("|")"#)
|
||||
.expect("media dangling markup CSP events should evaluate"),
|
||||
"csp:report:media-src|error"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn media_play_returns_a_fulfilled_promise() {
|
||||
let mut vm = new_storage_test_vm("https://media-play-promise.test/");
|
||||
|
||||
Reference in New Issue
Block a user