script: Implement document's active sandboxing flag set (#39079)

Implements document's active sandboxing flags. These are currently
populated only from CSP-derived sandboxing flags for a new document,
when defined in the CSP.

Testing: 1 new pass, and some new wpt's are added to test points in the
spec where these flags influence behaviour.

Signed-off-by: Shane Handley <shanehandley@fastmail.com>
This commit is contained in:
shanehandley 2025-09-05 15:02:23 +10:00 committed by GitHub
parent f722419861
commit 989c0d8994
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 156 additions and 15 deletions

View file

@ -19,6 +19,7 @@ use canvas_traits::canvas::CanvasId;
use canvas_traits::webgl::{WebGLContextId, WebGLMsg}; use canvas_traits::webgl::{WebGLContextId, WebGLMsg};
use chrono::Local; use chrono::Local;
use constellation_traits::{NavigationHistoryBehavior, ScriptToConstellationMessage}; use constellation_traits::{NavigationHistoryBehavior, ScriptToConstellationMessage};
use content_security_policy::sandboxing_directive::SandboxingFlagSet;
use content_security_policy::{CspList, PolicyDisposition}; use content_security_policy::{CspList, PolicyDisposition};
use cookie::Cookie; use cookie::Cookie;
use cssparser::match_ignore_ascii_case; use cssparser::match_ignore_ascii_case;
@ -569,6 +570,10 @@ pub(crate) struct Document {
/// The global custom element reaction stack for this script thread. /// The global custom element reaction stack for this script thread.
#[conditional_malloc_size_of] #[conditional_malloc_size_of]
custom_element_reaction_stack: Rc<CustomElementReactionStack>, custom_element_reaction_stack: Rc<CustomElementReactionStack>,
#[no_trace]
#[ignore_malloc_size_of = "type from external crate"]
/// <https://html.spec.whatwg.org/multipage/#active-sandboxing-flag-set>,
active_sandboxing_flag_set: Cell<SandboxingFlagSet>,
} }
#[allow(non_snake_case)] #[allow(non_snake_case)]
@ -3439,6 +3444,7 @@ impl Document {
waiting_on_canvas_image_updates: Cell::new(false), waiting_on_canvas_image_updates: Cell::new(false),
current_canvas_epoch: RefCell::new(Epoch(0)), current_canvas_epoch: RefCell::new(Epoch(0)),
custom_element_reaction_stack, custom_element_reaction_stack,
active_sandboxing_flag_set: Cell::new(SandboxingFlagSet::empty()),
} }
} }
@ -4416,6 +4422,14 @@ impl Document {
pub(crate) fn custom_element_reaction_stack(&self) -> Rc<CustomElementReactionStack> { pub(crate) fn custom_element_reaction_stack(&self) -> Rc<CustomElementReactionStack> {
self.custom_element_reaction_stack.clone() self.custom_element_reaction_stack.clone()
} }
pub(crate) fn has_active_sandboxing_flag(&self, flag: SandboxingFlagSet) -> bool {
self.active_sandboxing_flag_set.get().contains(flag)
}
pub(crate) fn set_active_sandboxing_flag_set(&self, flags: SandboxingFlagSet) {
self.active_sandboxing_flag_set.set(flags)
}
} }
#[allow(non_snake_case)] #[allow(non_snake_case)]
@ -4537,16 +4551,20 @@ impl DocumentMethods<crate::DomTypeHolder> for Document {
} }
} }
// https://html.spec.whatwg.org/multipage/#dom-document-domain /// <https://html.spec.whatwg.org/multipage/#dom-document-domain>
fn SetDomain(&self, value: DOMString) -> ErrorResult { fn SetDomain(&self, value: DOMString) -> ErrorResult {
// Step 1. // Step 1.
if !self.has_browsing_context { if !self.has_browsing_context {
return Err(Error::Security); return Err(Error::Security);
} }
// TODO: Step 2. "If this Document object's active sandboxing // Step 2. If this Document object's active sandboxing flag set has its sandboxed
// flag set has its sandboxed document.domain browsing context // document.domain browsing context flag set, then throw a "SecurityError" DOMException.
// flag set, then throw a "SecurityError" DOMException." if self.has_active_sandboxing_flag(
SandboxingFlagSet::SANDBOXED_DOCUMENT_DOMAIN_BROWSING_CONTEXT_FLAG,
) {
return Err(Error::Security);
}
// Steps 3-4. // Steps 3-4.
let effective_domain = match self.origin.effective_domain() { let effective_domain = match self.origin.effective_domain() {

View file

@ -6,6 +6,7 @@ use std::borrow::ToOwned;
use std::cell::Cell; use std::cell::Cell;
use constellation_traits::{LoadData, LoadOrigin, NavigationHistoryBehavior}; use constellation_traits::{LoadData, LoadOrigin, NavigationHistoryBehavior};
use content_security_policy::sandboxing_directive::SandboxingFlagSet;
use dom_struct::dom_struct; use dom_struct::dom_struct;
use encoding_rs::{Encoding, UTF_8}; use encoding_rs::{Encoding, UTF_8};
use headers::{ContentType, HeaderMapExt}; use headers::{ContentType, HeaderMapExt};
@ -739,10 +740,18 @@ impl HTMLFormElement {
if self.constructing_entry_list.get() { if self.constructing_entry_list.get() {
return; return;
} }
// Step 3 // Step 3. Let form document be form's node document.
let doc = self.owner_document(); let doc = self.owner_document();
// Step 4. If form document's active sandboxing flag set has its sandboxed forms browsing
// context flag set, then return.
if doc.has_active_sandboxing_flag(SandboxingFlagSet::SANDBOXED_FORMS_BROWSING_CONTEXT_FLAG)
{
return;
}
let base = doc.base_url(); let base = doc.base_url();
// TODO: Handle browsing contexts (Step 4, 5) // TODO: Handle browsing contexts (Step 5)
// Step 6 // Step 6
if submit_method_flag == SubmittedFrom::NotFromForm { if submit_method_flag == SubmittedFrom::NotFromForm {
// Step 6.1 // Step 6.1

View file

@ -10,6 +10,7 @@ use std::time::{Duration, Instant};
use std::{f64, mem}; use std::{f64, mem};
use compositing_traits::{CrossProcessCompositorApi, ImageUpdate, SerializableImageData}; use compositing_traits::{CrossProcessCompositorApi, ImageUpdate, SerializableImageData};
use content_security_policy::sandboxing_directive::SandboxingFlagSet;
use dom_struct::dom_struct; use dom_struct::dom_struct;
use embedder_traits::{MediaPositionState, MediaSessionEvent, MediaSessionPlaybackState}; use embedder_traits::{MediaPositionState, MediaSessionEvent, MediaSessionPlaybackState};
use euclid::default::Size2D; use euclid::default::Size2D;
@ -717,11 +718,8 @@ impl HTMLMediaElement {
} }
if ready_state == ReadyState::HaveEnoughData { if ready_state == ReadyState::HaveEnoughData {
// TODO: Check sandboxed automatic features browsing context flag.
// FIXME(nox): I have no idea what this TODO is about.
// FIXME(nox): Review this block. // FIXME(nox): Review this block.
if self.autoplaying.get() && self.Paused() && self.Autoplay() { if self.eligible_for_autoplay() {
// Step 1 // Step 1
self.paused.set(false); self.paused.set(false);
// Step 2 // Step 2
@ -968,6 +966,31 @@ impl HTMLMediaElement {
} }
} }
/// <https://html.spec.whatwg.org/multipage/#eligible-for-autoplay>
fn eligible_for_autoplay(&self) -> bool {
// its can autoplay flag is true;
self.autoplaying.get() &&
// its paused attribute is true;
self.Paused() &&
// it has an autoplay attribute specified;
self.Autoplay() &&
// its node document's active sandboxing flag set does not have the sandboxed automatic
// features browsing context flag set; and
{
let document = self.owner_document();
!document.has_active_sandboxing_flag(
SandboxingFlagSet::SANDBOXED_AUTOMATIC_FEATURES_BROWSING_CONTEXT_FLAG,
)
}
// its node document is allowed to use the "autoplay" feature.
// TODO: Feature policy: https://html.spec.whatwg.org/iframe-embed-object.html#allowed-to-use
}
// https://html.spec.whatwg.org/multipage/#concept-media-load-resource // https://html.spec.whatwg.org/multipage/#concept-media-load-resource
fn resource_fetch_algorithm(&self, resource: Resource) { fn resource_fetch_algorithm(&self, resource: Resource) {
if let Err(e) = self.setup_media_player(&resource) { if let Err(e) = self.setup_media_player(&resource) {

View file

@ -10,6 +10,7 @@ use base::cross_process_instant::CrossProcessInstant;
use base::id::PipelineId; use base::id::PipelineId;
use base64::Engine as _; use base64::Engine as _;
use base64::engine::general_purpose; use base64::engine::general_purpose;
use content_security_policy::sandboxing_directive::SandboxingFlagSet;
use devtools_traits::ScriptToDevtoolsControlMsg; use devtools_traits::ScriptToDevtoolsControlMsg;
use dom_struct::dom_struct; use dom_struct::dom_struct;
use embedder_traits::resources::{self, Resource}; use embedder_traits::resources::{self, Resource};
@ -940,6 +941,15 @@ impl FetchResponseListener for ParserContext {
let _realm = enter_realm(&*parser.document); let _realm = enter_realm(&*parser.document);
// From Step 23.8.3 of https://html.spec.whatwg.org/multipage/#navigate
// Let finalSandboxFlags be the union of targetSnapshotParams's sandboxing flags and
// policyContainer's CSP list's CSP-derived sandboxing flags.
// TODO: implement targetSnapshotParam's sandboxing flags
let csp_derived_sandboxing_flag_set = csp_list
.as_ref()
.and_then(|csp| csp.get_sandboxing_flag_set_for_document())
.unwrap_or(SandboxingFlagSet::empty());
if let Some(endpoints) = endpoints_list { if let Some(endpoints) = endpoints_list {
parser.document.window().set_endpoints_list(endpoints); parser.document.window().set_endpoints_list(endpoints);
} }
@ -961,6 +971,15 @@ impl FetchResponseListener for ParserContext {
let Some(media_type) = MimeClassifier::get_media_type(&mime_type) else { let Some(media_type) = MimeClassifier::get_media_type(&mime_type) else {
return; return;
}; };
// CSP/Sandboxing is applied conditionally based on the content type
let apply_csp_and_sandboxing_flags = || {
parser
.document
.set_active_sandboxing_flag_set(csp_derived_sandboxing_flag_set);
parser.document.set_csp_list(csp_list);
};
match media_type { match media_type {
// Return the result of loading a media document given navigationParams and type. // Return the result of loading a media document given navigationParams and type.
MediaType::Image | MediaType::AudioVideo => { MediaType::Image | MediaType::AudioVideo => {
@ -1037,10 +1056,10 @@ impl FetchResponseListener for ParserContext {
}, },
Some(_) => {}, Some(_) => {},
// Return the result of loading an HTML document, given navigationParams. // Return the result of loading an HTML document, given navigationParams.
None => parser.document.set_csp_list(csp_list), None => apply_csp_and_sandboxing_flags(),
}, },
// Return the result of loading an XML document given navigationParams and type. // Return the result of loading an XML document given navigationParams and type.
MediaType::Xml => parser.document.set_csp_list(csp_list), MediaType::Xml => apply_csp_and_sandboxing_flags(),
_ => { _ => {
// Show warning page for unknown mime types. // Show warning page for unknown mime types.
let page = format!( let page = format!(

View file

@ -403717,6 +403717,14 @@
] ]
}, },
"sandbox": { "sandbox": {
"autoplay-disabled-by-csp.html.headers": [
"32518e57d4584de71845a9260b093c3535fc3074",
[]
],
"form-submission-blocked-by-sandboxing.html.headers": [
"1efcf8c226fac074c98d0a5a747856f532e5d84e",
[]
],
"support": { "support": {
"empty.html": [ "empty.html": [
"e69de29bb2d1d6434b8b29ae775ad8c2e48c5391", "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391",
@ -581697,6 +581705,20 @@
] ]
}, },
"sandbox": { "sandbox": {
"autoplay-disabled-by-csp.html": [
"d7bd453a34c0e75c98c837f853c0cf492359625a",
[
null,
{}
]
],
"form-submission-blocked-by-sandboxing.html": [
"4c717a18fd8bfa9d5cb4bc5449b0f25498ccb754",
[
null,
{}
]
],
"iframe-inside-csp.sub.html": [ "iframe-inside-csp.sub.html": [
"cd402bdba0198bf763e1733004c2005614b9a542", "cd402bdba0198bf763e1733004c2005614b9a542",
[ [

View file

@ -1,3 +0,0 @@
[sandboxed-document_domain.html]
[Sandboxed document.domain]
expected: FAIL

View file

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<link rel="help" href="https://html.spec.whatwg.org/multipage/#eligible-for-autoplay" />
<title>Test that autoplay is blocked by a document's active sandboxing flags</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/common/media.js"></script>
</head>
<body>
<video id="v" autoplay></video>
<script>
async_test((t) => {
var v = document.getElementById('v')
v.addEventListener('playing', t.unreached_func(
'video should not autoplay due to sandboxing flags'
));
v.src = getVideoURI('/media/movie_5') + '?' + new Date() + Math.random()
t.step_timeout(() => t.done(), 500);
}, 'csp-derived sandboxing flags prevent autoplay.')
</script>
</body>
</html>

View file

@ -0,0 +1 @@
Content-Security-Policy: sandbox allow-forms

View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>
<head>
<link rel="help" href="https://html.spec.whatwg.org/multipage/#concept-form-submit">
<title>Test that form submission is blocked by a document's active sandboxing flags</title>
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
</head>
<body>
<form id="f">
<input type="hidden" value="test" />
</form>
<script>
async_test((t) => {
var f = document.getElementById('f')
f.addEventListener('submit', t.unreached_func(
'form should not be submitted due to sandboxing flags'
));
f.submit();
t.step_timeout(() => t.done(), 500);
}, 'csp-derived sandboxing flags prevent form submission.')
</script>
</body>
</html>

View file

@ -0,0 +1 @@
Content-Security-Policy: sandbox allow-scripts