Implement trusted types url setter (#36596)

We now check the sink of script.src for trusted types. This is the first
attribute that we check, other sinks will be implemented in follow-up
changes.

The algorithms currently hardcode various parts. That's because I need
to refactor a couple of algorithms already present in TrustedTypePolicy.
They use callbacks at the moment, which made sense for their initial
use. However, for these new algorithms they don't work. Therefore, I
will align them with the specification by taking in an enum. However,
since that's a bigger refactoring, I left that out of this PR (which is
already quite big).

The other trusted types support (createScript and createHTML) will also
be implemented separately.

Part of #36258

---------

Signed-off-by: Tim van der Lippe <tvanderlippe@gmail.com>
Signed-off-by: Tim van der Lippe <TimvdLippe@users.noreply.github.com>
Co-authored-by: Josh Matthews <josh@joshmatthews.net>
This commit is contained in:
Tim van der Lippe 2025-04-21 08:56:40 +02:00 committed by GitHub
parent fee2ea34af
commit 6bb087e381
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 233 additions and 74 deletions

2
Cargo.lock generated
View file

@ -1230,7 +1230,7 @@ dependencies = [
[[package]]
name = "content-security-policy"
version = "0.5.4"
source = "git+https://github.com/servo/rust-content-security-policy/?branch=servo-csp#be68d50b793c31403d858ecdfc6eb245085e7e7c"
source = "git+https://github.com/servo/rust-content-security-policy/?branch=servo-csp#827eea44ec0f3d91457d1c0467881cb4f9752520"
dependencies = [
"base64 0.22.1",
"bitflags 2.9.0",

View file

@ -78,7 +78,7 @@ use crate::dom::bindings::codegen::Bindings::ShadowRootBinding::{
use crate::dom::bindings::codegen::Bindings::WindowBinding::{
ScrollBehavior, ScrollToOptions, WindowMethods,
};
use crate::dom::bindings::codegen::UnionTypes::NodeOrString;
use crate::dom::bindings::codegen::UnionTypes::{NodeOrString, TrustedScriptURLOrUSVString};
use crate::dom::bindings::conversions::DerivedFrom;
use crate::dom::bindings::error::{Error, ErrorResult, Fallible};
use crate::dom::bindings::inheritance::{Castable, ElementTypeId, HTMLElementTypeId, NodeTypeId};
@ -149,6 +149,7 @@ use crate::dom::raredata::ElementRareData;
use crate::dom::servoparser::ServoParser;
use crate::dom::shadowroot::{IsUserAgentWidget, ShadowRoot};
use crate::dom::text::Text;
use crate::dom::types::TrustedTypePolicyFactory;
use crate::dom::validation::Validatable;
use crate::dom::validitystate::ValidationFlags;
use crate::dom::virtualmethods::{VirtualMethods, vtable_for};
@ -1928,6 +1929,53 @@ impl Element {
self.set_attribute(local_name, AttrValue::String(value.to_string()), can_gc);
}
pub(crate) fn get_trusted_type_url_attribute(
&self,
local_name: &LocalName,
) -> TrustedScriptURLOrUSVString {
assert_eq!(*local_name, local_name.to_ascii_lowercase());
let attr = match self.get_attribute(&ns!(), local_name) {
Some(attr) => attr,
None => return TrustedScriptURLOrUSVString::USVString(USVString::default()),
};
let value = &**attr.value();
// XXXManishearth this doesn't handle `javascript:` urls properly
self.owner_document()
.base_url()
.join(value)
.map(|parsed| TrustedScriptURLOrUSVString::USVString(USVString(parsed.into_string())))
.unwrap_or_else(|_| TrustedScriptURLOrUSVString::USVString(USVString(value.to_owned())))
}
pub(crate) fn set_trusted_type_url_attribute(
&self,
local_name: &LocalName,
value: TrustedScriptURLOrUSVString,
can_gc: CanGc,
) -> Fallible<()> {
assert_eq!(*local_name, local_name.to_ascii_lowercase());
let value = match value {
TrustedScriptURLOrUSVString::USVString(url) => {
let global = self.owner_global();
// TODO(36258): Reflectively get the name of the class
let sink = format!("{} {}", "HTMLScriptElement", &local_name);
let result = TrustedTypePolicyFactory::get_trusted_type_compliant_string(
&global,
url.to_string(),
&sink,
"'script'",
can_gc,
);
result?
},
// This partially implements <https://w3c.github.io/trusted-types/dist/spec/#get-trusted-type-compliant-string-algorithm>
// Step 1: If input is an instance of expectedType, return stringified input and abort these steps.
TrustedScriptURLOrUSVString::TrustedScriptURL(script_url) => script_url.to_string(),
};
self.set_attribute(local_name, AttrValue::String(value), can_gc);
Ok(())
}
pub(crate) fn get_string_attribute(&self, local_name: &LocalName) -> DOMString {
match self.get_attribute(&ns!(), local_name) {
Some(x) => x.Value(),

View file

@ -3456,11 +3456,16 @@ impl GlobalScope {
ViolationResource::TrustedTypePolicy { sample } => {
(Some(sample), "trusted-types-policy".to_owned())
},
ViolationResource::TrustedTypeSink { sample } => {
(Some(sample), "trusted-types-sink".to_owned())
},
};
let report = CSPViolationReportBuilder::default()
.resource(resource)
.sample(sample)
.effective_directive(violation.directive.name)
.original_policy(violation.policy.to_string())
.report_only(violation.policy.disposition == PolicyDisposition::Report)
.build(self);
let task = CSPViolationReportTask::new(self, report);
self.task_manager()

View file

@ -44,6 +44,8 @@ use crate::dom::bindings::codegen::Bindings::DocumentBinding::DocumentMethods;
use crate::dom::bindings::codegen::Bindings::HTMLScriptElementBinding::HTMLScriptElementMethods;
use crate::dom::bindings::codegen::Bindings::NodeBinding::NodeMethods;
use crate::dom::bindings::codegen::GenericBindings::HTMLElementBinding::HTMLElement_Binding::HTMLElementMethods;
use crate::dom::bindings::codegen::UnionTypes::TrustedScriptURLOrUSVString;
use crate::dom::bindings::error::Fallible;
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::refcounted::Trusted;
use crate::dom::bindings::reflector::DomGlobal;
@ -1342,10 +1344,10 @@ impl VirtualMethods for HTMLScriptElement {
impl HTMLScriptElementMethods<crate::DomTypeHolder> for HTMLScriptElement {
// https://html.spec.whatwg.org/multipage/#dom-script-src
make_url_getter!(Src, "src");
make_trusted_type_url_getter!(Src, "src");
// https://html.spec.whatwg.org/multipage/#dom-script-src
make_url_setter!(SetSrc, "src");
make_trusted_type_url_setter!(SetSrc, "src");
// https://html.spec.whatwg.org/multipage/#dom-script-type
make_getter!(Type, "type");

View file

@ -121,6 +121,32 @@ macro_rules! make_url_setter(
);
);
#[macro_export]
macro_rules! make_trusted_type_url_getter(
( $attr:ident, $htmlname:tt ) => (
fn $attr(&self) -> TrustedScriptURLOrUSVString {
use $crate::dom::bindings::inheritance::Castable;
use $crate::dom::element::Element;
let element = self.upcast::<Element>();
element.get_trusted_type_url_attribute(&html5ever::local_name!($htmlname))
}
);
);
#[macro_export]
macro_rules! make_trusted_type_url_setter(
( $attr:ident, $htmlname:tt ) => (
fn $attr(&self, value: TrustedScriptURLOrUSVString, can_gc: CanGc) -> Fallible<()> {
use $crate::dom::bindings::inheritance::Castable;
use $crate::dom::element::Element;
use $crate::script_runtime::CanGc;
let element = self.upcast::<Element>();
element.set_trusted_type_url_attribute(&html5ever::local_name!($htmlname),
value, can_gc)
}
);
);
#[macro_export]
macro_rules! make_form_action_getter(
( $attr:ident, $htmlname:tt ) => (

View file

@ -2,6 +2,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::fmt;
use dom_struct::dom_struct;
use crate::dom::bindings::codegen::Bindings::TrustedScriptURLBinding::TrustedScriptURLMethods;
@ -32,6 +34,13 @@ impl TrustedScriptURL {
}
}
impl fmt::Display for TrustedScriptURL {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(&self.data)
}
}
impl TrustedScriptURLMethods<crate::DomTypeHolder> for TrustedScriptURL {
/// <https://www.w3.org/TR/trusted-types/#trustedscripturl-stringification-behavior>
fn Stringifier(&self) -> DOMString {

View file

@ -59,6 +59,13 @@ impl TrustedTypePolicy {
reflect_dom_object(Box::new(Self::new_inherited(name, options)), global, can_gc)
}
// TODO(36258): Remove when we refactor get_trusted_type_policy_value to take an enum
// value to handle which callback to call. The callback should not be exposed outside
// of the policy object, but is currently used in TrustedPolicyFactory::process_value_with_default_policy
pub(crate) fn create_script_url(&self) -> Option<Rc<CreateScriptURLCallback>> {
self.create_script_url.clone()
}
/// This does not take all arguments as specified. That's because the return type of the
/// trusted type function and object are not the same. 2 of the 3 string callbacks return
/// a DOMString, while the other one returns an USVString. Additionally, all three callbacks

View file

@ -6,8 +6,11 @@ use std::cell::RefCell;
use content_security_policy::CheckResult;
use dom_struct::dom_struct;
use html5ever::{LocalName, Namespace, QualName, local_name, ns};
use js::jsapi::JSObject;
use js::jsval::NullValue;
use js::rust::HandleValue;
use crate::dom::bindings::callback::ExceptionHandling;
use crate::dom::bindings::codegen::Bindings::TrustedTypePolicyFactoryBinding::{
TrustedTypePolicyFactoryMethods, TrustedTypePolicyOptions,
};
@ -21,6 +24,7 @@ use crate::dom::trustedhtml::TrustedHTML;
use crate::dom::trustedscript::TrustedScript;
use crate::dom::trustedscripturl::TrustedScriptURL;
use crate::dom::trustedtypepolicy::TrustedTypePolicy;
use crate::js::conversions::ToJSValConvertible;
use crate::script_runtime::{CanGc, JSContext};
#[dom_struct]
@ -137,6 +141,122 @@ impl TrustedTypePolicyFactory {
// Step 4: Return data.
data
}
/// <https://w3c.github.io/trusted-types/dist/spec/#process-value-with-a-default-policy-algorithm>
#[allow(unsafe_code)]
pub(crate) fn process_value_with_default_policy(
global: &GlobalScope,
input: String,
sink: &str,
can_gc: CanGc,
) -> Fallible<Option<DomRoot<TrustedScriptURL>>> {
// Step 1: Let defaultPolicy be the value of globals trusted type policy factory's default policy.
let global_policy_factory = global.trusted_types(can_gc);
let default_policy = match global_policy_factory.default_policy.get() {
None => return Ok(Some(TrustedScriptURL::new(input, global, can_gc))),
Some(default_policy) => default_policy,
};
let cx = GlobalScope::get_cx();
// Step 2: Let policyValue be the result of executing Get Trusted Type policy value,
// with the following arguments:
let policy_value = default_policy.get_trusted_type_policy_value(
|| {
// TODO(36258): support other trusted types as well by changing get_trusted_type_policy_value to accept
// the trusted type as enum and call the appropriate callback based on that.
default_policy.create_script_url().map(|callback| {
rooted!(in(*cx) let this_object: *mut JSObject);
rooted!(in(*cx) let mut trusted_type_name_value = NullValue());
unsafe {
"TrustedScriptURL".to_jsval(*cx, trusted_type_name_value.handle_mut());
}
rooted!(in(*cx) let mut sink_value = NullValue());
unsafe {
sink.to_jsval(*cx, sink_value.handle_mut());
}
let args = vec![trusted_type_name_value.handle(), sink_value.handle()];
// Step 4: Let policyValue be the result of invoking function with value as a first argument,
// items of arguments as subsequent arguments, and callback **this** value set to null,
// rethrowing any exceptions.
callback.Call_(
&this_object.handle(),
DOMString::from(input.to_owned()),
args,
ExceptionHandling::Rethrow,
can_gc,
)
})
},
false,
);
let data_string = match policy_value {
// Step 3: If the algorithm threw an error, rethrow the error and abort the following steps.
Err(error) => return Err(error),
Ok(policy_value) => match policy_value {
// Step 4: If policyValue is null or undefined, return policyValue.
None => return Ok(None),
// Step 5: Let dataString be the result of stringifying policyValue.
Some(policy_value) => policy_value.as_ref().into(),
},
};
Ok(Some(TrustedScriptURL::new(data_string, global, can_gc)))
}
/// Step 1 is implemented by the caller
/// <https://w3c.github.io/trusted-types/dist/spec/#get-trusted-type-compliant-string-algorithm>
pub(crate) fn get_trusted_type_compliant_string(
global: &GlobalScope,
input: String,
sink: &str,
sink_group: &str,
can_gc: CanGc,
) -> Fallible<String> {
let csp_list = match global.get_csp_list() {
None => return Ok(input),
Some(csp_list) => csp_list,
};
// Step 2: Let requireTrustedTypes be the result of executing Does sink type require trusted types?
// algorithm, passing global, sinkGroup, and true.
let require_trusted_types = csp_list.does_sink_type_require_trusted_types(sink_group, true);
// Step 3: If requireTrustedTypes is false, return stringified input and abort these steps.
if !require_trusted_types {
return Ok(input);
}
// Step 4: Let convertedInput be the result of executing Process value with a default policy
// with the same arguments as this algorithm.
let converted_input = TrustedTypePolicyFactory::process_value_with_default_policy(
global,
input.clone(),
sink,
can_gc,
);
// Step 5: If the algorithm threw an error, rethrow the error and abort the following steps.
match converted_input? {
// Step 6: If convertedInput is null or undefined, execute the following steps:
None => {
// Step 6.1: Let disposition be the result of executing Should sink type mismatch violation
// be blocked by Content Security Policy? algorithm, passing global,
// stringified input as source, sinkGroup and sink.
let (disposition, violations) = csp_list
.should_sink_type_mismatch_violation_be_blocked_by_csp(
sink, sink_group, &input,
);
global.report_csp_violations(violations);
// Step 6.2: If disposition is “Allowed”, return stringified input and abort further steps.
if disposition == CheckResult::Allowed {
Ok(input)
} else {
// Step 6.3: Throw a TypeError and abort further steps.
Err(Error::Type(
"Cannot set value, expected trusted type".to_owned(),
))
}
},
// Step 8: Return stringified convertedInput.
Some(converted_input) => Ok((*converted_input).to_string()),
}
// Step 7: Assert: convertedInput is an instance of expectedType.
// TODO(https://github.com/w3c/trusted-types/issues/566): Implement when spec is resolved
}
}
impl TrustedTypePolicyFactoryMethods<crate::DomTypeHolder> for TrustedTypePolicyFactory {

View file

@ -62,6 +62,8 @@ pub(crate) struct CSPViolationReportBuilder {
pub source_file: String,
/// <https://www.w3.org/TR/CSP3/#violation-effective-directive>
pub effective_directive: String,
/// <https://www.w3.org/TR/CSP3/#violation-policy>
pub original_policy: String,
}
impl CSPViolationReportBuilder {
@ -106,6 +108,12 @@ impl CSPViolationReportBuilder {
self
}
/// <https://www.w3.org/TR/CSP3/#violation-policy>
pub fn original_policy(mut self, original_policy: String) -> CSPViolationReportBuilder {
self.original_policy = original_policy;
self
}
/// <https://w3c.github.io/webappsec-csp/#strip-url-for-use-in-reports>
fn strip_url_for_reports(&self, mut url: ServoUrl) -> String {
let scheme = url.scheme();
@ -141,7 +149,7 @@ impl CSPViolationReportBuilder {
sample: self.sample,
blocked_url: self.resource,
source_file: self.source_file,
original_policy: "".to_owned(),
original_policy: self.original_policy,
line_number: self.line_number,
column_number: self.column_number,
status_code: global.status_code().unwrap_or(0),

View file

@ -416,7 +416,7 @@ DOMInterfaces = {
},
'HTMLScriptElement': {
'canGc': ['SetAsync', 'SetCrossOrigin', 'SetText']
'canGc': ['SetAsync', 'SetCrossOrigin', 'SetSrc', 'SetText']
},
'HTMLSelectElement': {

View file

@ -7,8 +7,8 @@
interface HTMLScriptElement : HTMLElement {
[HTMLConstructor] constructor();
[CEReactions]
attribute USVString src;
[CEReactions, SetterThrows]
attribute (TrustedScriptURL or USVString) src;
[CEReactions]
attribute DOMString type;
[CEReactions]

View file

@ -1,3 +0,0 @@
[generic-0_1-img-src.html]
[Should fire violation events for every failed violation]
expected: FAIL

View file

@ -1,3 +0,0 @@
[generic-0_1-script-src.html]
[Should fire violation events for every failed violation]
expected: FAIL

View file

@ -1,3 +0,0 @@
[generic-0_10_1.sub.html]
[Should fire violation events for every failed violation]
expected: FAIL

View file

@ -1,3 +0,0 @@
[generic-0_2_2.sub.html]
[Should fire violation events for every failed violation]
expected: FAIL

View file

@ -1,3 +0,0 @@
[generic-0_2_3.html]
[Should fire violation events for every failed violation]
expected: FAIL

View file

@ -1,3 +0,0 @@
[script-src-1_10.html]
[Test that securitypolicyviolation event is fired]
expected: FAIL

View file

@ -1,3 +0,0 @@
[script-src-strict_dynamic_double_policy_different_nonce.html]
[Unnonced script injected via `appendChild` is not allowed with `strict-dynamic` + a nonce-only double policy.]
expected: FAIL

View file

@ -1,6 +1,3 @@
[style-blocked.html]
[Violated directive is script-src-elem.]
expected: FAIL
[document.styleSheets should contain an item for the blocked CSS.]
expected: FAIL

View file

@ -1,6 +1,3 @@
[HTMLScriptElement-internal-slot.html]
[Test TT application when manipulating <script> elements during loading.]
expected: FAIL
[Setting .src to a plain string should throw an exception and not modify the script state, on an unconnected script element.]
expected: FAIL

View file

@ -1,6 +0,0 @@
[TrustedTypePolicyFactory-createPolicy-cspTests-none.html]
[Cannot create policy with name 'SomeName' - policy creation throws]
expected: FAIL
[Cannot create policy with name 'default' - policy creation throws]
expected: FAIL

View file

@ -1,6 +0,0 @@
[TrustedTypePolicyFactory-createPolicy-cspTests.html]
[Non-allowed name policy creation throws.]
expected: FAIL
[Duplicate name policy creation throws.]
expected: FAIL

View file

@ -11,9 +11,6 @@
[`Script.prototype.setAttribute.SrC = string` throws.]
expected: FAIL
[script.src accepts string and null after default policy was created.]
expected: FAIL
[script.src's mutationobservers receive the default policy's value.]
expected: FAIL

View file

@ -8,9 +8,6 @@
[iframe.srcdoc accepts only TrustedHTML]
expected: FAIL
[script.src accepts string and null after default policy was created]
expected: FAIL
[div.innerHTML accepts string and null after default policy was created]
expected: FAIL

View file

@ -16,6 +16,3 @@
[Setting HTMLScriptElement.text to a plain string]
expected: FAIL
[Setting HTMLScriptElement.src to a plain string]
expected: FAIL

View file

@ -12,21 +12,6 @@
[script.text no default policy]
expected: FAIL
[script.src default]
expected: FAIL
[script.src null]
expected: FAIL
[script.src throw]
expected: FAIL
[script.src undefined]
expected: FAIL
[script.src typeerror]
expected: FAIL
[div.innerHTML default]
expected: FAIL

View file

@ -3,9 +3,6 @@
[Count SecurityPolicyViolation events.]
expected: TIMEOUT
[script.src default]
expected: FAIL
[div.innerHTML default]
expected: FAIL