Implement inner slot for cryptographic nonce (#36965)

Also update the `html/dom/reflection-metadata.html` test
to handle the case where `nonce` does not reflect back
to the attribute after an IDL change.

Part of https://github.com/servo/servo/issues/4577
Fixes https://github.com/web-platform-tests/wpt/issues/43286

Signed-off-by: Tim van der Lippe <tvanderlippe@gmail.com>
This commit is contained in:
Tim van der Lippe 2025-05-14 12:21:21 +02:00 committed by GitHub
parent 3aff272e14
commit a24fce3ae7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 132 additions and 98 deletions

View file

@ -4313,7 +4313,7 @@ impl Document {
}, },
Some(csp_list) => { Some(csp_list) => {
let element = csp::Element { let element = csp::Element {
nonce: el.nonce_attribute_if_nonceable().map(Cow::Owned), nonce: el.nonce_value_if_nonceable().map(Cow::Owned),
}; };
csp_list.should_elements_inline_type_behavior_be_blocked(&element, type_, source) csp_list.should_elements_inline_type_behavior_be_blocked(&element, type_, source)
}, },

View file

@ -146,8 +146,8 @@ use crate::dom::intersectionobserver::{IntersectionObserver, IntersectionObserve
use crate::dom::mutationobserver::{Mutation, MutationObserver}; use crate::dom::mutationobserver::{Mutation, MutationObserver};
use crate::dom::namednodemap::NamedNodeMap; use crate::dom::namednodemap::NamedNodeMap;
use crate::dom::node::{ use crate::dom::node::{
BindContext, ChildrenMutation, LayoutNodeHelpers, Node, NodeDamage, NodeFlags, NodeTraits, BindContext, ChildrenMutation, CloneChildrenFlag, LayoutNodeHelpers, Node, NodeDamage,
ShadowIncluding, UnbindContext, NodeFlags, NodeTraits, ShadowIncluding, UnbindContext,
}; };
use crate::dom::nodelist::NodeList; use crate::dom::nodelist::NodeList;
use crate::dom::promise::Promise; use crate::dom::promise::Promise;
@ -2188,10 +2188,53 @@ impl Element {
}; };
} }
/// <https://html.spec.whatwg.org/multipage/#nonce-attributes>
pub(crate) fn update_nonce_internal_slot(&self, nonce: String) {
self.ensure_rare_data().cryptographic_nonce = nonce;
}
/// <https://html.spec.whatwg.org/multipage/#nonce-attributes>
pub(crate) fn nonce_value(&self) -> String {
match self.rare_data().as_ref() {
None => String::new(),
Some(rare_data) => rare_data.cryptographic_nonce.clone(),
}
}
/// <https://html.spec.whatwg.org/multipage/#nonce-attributes>
pub(crate) fn update_nonce_post_connection(&self) {
// Whenever an element including HTMLOrSVGElement becomes browsing-context connected,
// the user agent must execute the following steps on the element:
if !self.upcast::<Node>().is_connected_with_browsing_context() {
return;
}
let global = self.owner_global();
// Step 1: Let CSP list be element's shadow-including root's policy container's CSP list.
let csp_list = match global.get_csp_list() {
None => return,
Some(csp_list) => csp_list,
};
// Step 2: If CSP list contains a header-delivered Content Security Policy,
// and element has a nonce content attribute whose value is not the empty string, then:
if !csp_list.contains_a_header_delivered_content_security_policy() ||
self.get_string_attribute(&local_name!("nonce")).is_empty()
{
return;
}
// Step 2.1: Let nonce be element's [[CryptographicNonce]].
let nonce = self.nonce_value();
// Step 2.2: Set an attribute value for element using "nonce" and the empty string.
self.set_string_attribute(&local_name!("nonce"), "".into(), CanGc::note());
// Step 2.3: Set element's [[CryptographicNonce]] to nonce.
self.update_nonce_internal_slot(nonce);
}
/// <https://www.w3.org/TR/CSP/#is-element-nonceable> /// <https://www.w3.org/TR/CSP/#is-element-nonceable>
pub(crate) fn nonce_attribute_if_nonceable(&self) -> Option<String> { pub(crate) fn nonce_value_if_nonceable(&self) -> Option<String> {
// Step 1: If element does not have an attribute named "nonce", return "Not Nonceable". // Step 1: If element does not have an attribute named "nonce", return "Not Nonceable".
let nonce_attribute = self.get_attribute(&ns!(), &local_name!("nonce"))?; if !self.has_attribute(&local_name!("nonce")) {
return None;
}
// Step 2: If element is a script element, then for each attribute of elements attribute list: // Step 2: If element is a script element, then for each attribute of elements attribute list:
if self.downcast::<HTMLScriptElement>().is_some() { if self.downcast::<HTMLScriptElement>().is_some() {
for attr in self.attrs().iter() { for attr in self.attrs().iter() {
@ -2213,7 +2256,7 @@ impl Element {
// TODO(https://github.com/servo/servo/issues/4577 and https://github.com/whatwg/html/issues/3257): // TODO(https://github.com/servo/servo/issues/4577 and https://github.com/whatwg/html/issues/3257):
// Figure out how to retrieve this information from the parser // Figure out how to retrieve this information from the parser
// Step 4: Return "Nonceable". // Step 4: Return "Nonceable".
Some(nonce_attribute.value().to_string().trim().to_owned()) Some(self.nonce_value().trim().to_owned())
} }
// https://dom.spec.whatwg.org/#insert-adjacent // https://dom.spec.whatwg.org/#insert-adjacent
@ -4197,6 +4240,31 @@ impl VirtualMethods for Element {
self.tag_name.clear(); self.tag_name.clear();
} }
} }
fn post_connection_steps(&self) {
if let Some(s) = self.super_type() {
s.post_connection_steps();
}
self.update_nonce_post_connection();
}
/// <https://html.spec.whatwg.org/multipage/#nonce-attributes%3Aconcept-node-clone-ext>
fn cloning_steps(
&self,
copy: &Node,
maybe_doc: Option<&Document>,
clone_children: CloneChildrenFlag,
can_gc: CanGc,
) {
if let Some(s) = self.super_type() {
s.cloning_steps(copy, maybe_doc, clone_children, can_gc);
}
let elem = copy.downcast::<Element>().unwrap();
if let Some(rare_data) = self.rare_data().as_ref() {
elem.update_nonce_internal_slot(rare_data.cryptographic_nonce.clone());
}
}
} }
#[derive(Clone, PartialEq)] #[derive(Clone, PartialEq)]

View file

@ -645,13 +645,16 @@ impl HTMLElementMethods<crate::DomTypeHolder> for HTMLElement {
Ok(internals) Ok(internals)
} }
// FIXME: The nonce should be stored in an internal slot instead of an
// attribute (https://html.spec.whatwg.org/multipage/#cryptographicnonce)
// https://html.spec.whatwg.org/multipage/#dom-noncedelement-nonce // https://html.spec.whatwg.org/multipage/#dom-noncedelement-nonce
make_getter!(Nonce, "nonce"); fn Nonce(&self) -> DOMString {
self.as_element().nonce_value().into()
}
// https://html.spec.whatwg.org/multipage/#dom-noncedelement-nonce // https://html.spec.whatwg.org/multipage/#dom-noncedelement-nonce
make_setter!(SetNonce, "nonce"); fn SetNonce(&self, value: DOMString) {
self.as_element()
.update_nonce_internal_slot(value.to_string())
}
// https://html.spec.whatwg.org/multipage/#dom-fe-autofocus // https://html.spec.whatwg.org/multipage/#dom-fe-autofocus
fn Autofocus(&self) -> bool { fn Autofocus(&self) -> bool {
@ -1138,6 +1141,15 @@ impl VirtualMethods for HTMLElement {
}, },
} }
}, },
(&local_name!("nonce"), mutation) => match mutation {
AttributeMutation::Set(_) => {
let nonce = &**attr.value();
element.update_nonce_internal_slot(nonce.to_owned());
},
AttributeMutation::Removed => {
element.update_nonce_internal_slot("".to_owned());
},
},
_ => {}, _ => {},
} }
} }

View file

@ -31,7 +31,6 @@ use crate::dom::attr::Attr;
use crate::dom::bindings::cell::DomRefCell; use crate::dom::bindings::cell::DomRefCell;
use crate::dom::bindings::codegen::Bindings::DOMTokenListBinding::DOMTokenList_Binding::DOMTokenListMethods; use crate::dom::bindings::codegen::Bindings::DOMTokenListBinding::DOMTokenList_Binding::DOMTokenListMethods;
use crate::dom::bindings::codegen::Bindings::HTMLLinkElementBinding::HTMLLinkElementMethods; use crate::dom::bindings::codegen::Bindings::HTMLLinkElementBinding::HTMLLinkElementMethods;
use crate::dom::bindings::codegen::GenericBindings::HTMLElementBinding::HTMLElement_Binding::HTMLElementMethods;
use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::refcounted::Trusted; use crate::dom::bindings::refcounted::Trusted;
use crate::dom::bindings::reflector::DomGlobal; use crate::dom::bindings::reflector::DomGlobal;
@ -344,7 +343,7 @@ impl HTMLLinkElement {
destination: Some(destination), destination: Some(destination),
integrity: String::new(), integrity: String::new(),
link_type: String::new(), link_type: String::new(),
cryptographic_nonce_metadata: self.upcast::<HTMLElement>().Nonce().into(), cryptographic_nonce_metadata: self.upcast::<Element>().nonce_value(),
cross_origin: cors_setting_for_element(element), cross_origin: cors_setting_for_element(element),
referrer_policy: referrer_policy_for_element(element), referrer_policy: referrer_policy_for_element(element),
policy_container: document.policy_container().to_owned(), policy_container: document.policy_container().to_owned(),

View file

@ -781,7 +781,7 @@ impl HTMLScriptElement {
}; };
// Step 24. Let cryptographic nonce be el's [[CryptographicNonce]] internal slot's value. // Step 24. Let cryptographic nonce be el's [[CryptographicNonce]] internal slot's value.
let cryptographic_nonce = self.upcast::<HTMLElement>().Nonce().into(); let cryptographic_nonce = self.upcast::<Element>().nonce_value();
// Step 25. If el has an integrity attribute, then let integrity metadata be that attribute's value. // Step 25. If el has an integrity attribute, then let integrity metadata be that attribute's value.
// Otherwise, let integrity metadata be the empty string. // Otherwise, let integrity metadata be the empty string.

View file

@ -75,4 +75,5 @@ pub(crate) struct ElementRareData {
/// > Element objects have an internal [[RegisteredIntersectionObservers]] slot, /// > Element objects have an internal [[RegisteredIntersectionObservers]] slot,
/// > which is initialized to an empty list. This list holds IntersectionObserverRegistration records, which have: /// > which is initialized to an empty list. This list holds IntersectionObserverRegistration records, which have:
pub(crate) registered_intersection_observers: Vec<IntersectionObserverRegistration>, pub(crate) registered_intersection_observers: Vec<IntersectionObserverRegistration>,
pub(crate) cryptographic_nonce: String,
} }

View file

@ -8,12 +8,13 @@ use js::rust::HandleObject;
use script_bindings::str::DOMString; use script_bindings::str::DOMString;
use stylo_dom::ElementState; use stylo_dom::ElementState;
use crate::dom::attr::Attr;
use crate::dom::bindings::codegen::Bindings::SVGElementBinding::SVGElementMethods; use crate::dom::bindings::codegen::Bindings::SVGElementBinding::SVGElementMethods;
use crate::dom::bindings::inheritance::Castable; use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom}; use crate::dom::bindings::root::{Dom, DomRoot, MutNullableDom};
use crate::dom::cssstyledeclaration::{CSSModificationAccess, CSSStyleDeclaration, CSSStyleOwner}; use crate::dom::cssstyledeclaration::{CSSModificationAccess, CSSStyleDeclaration, CSSStyleOwner};
use crate::dom::document::Document; use crate::dom::document::Document;
use crate::dom::element::Element; use crate::dom::element::{AttributeMutation, Element};
use crate::dom::node::{Node, NodeTraits}; use crate::dom::node::{Node, NodeTraits};
use crate::dom::virtualmethods::VirtualMethods; use crate::dom::virtualmethods::VirtualMethods;
use crate::script_runtime::CanGc; use crate::script_runtime::CanGc;
@ -59,11 +60,33 @@ impl SVGElement {
can_gc, can_gc,
) )
} }
fn as_element(&self) -> &Element {
self.upcast::<Element>()
}
} }
impl VirtualMethods for SVGElement { impl VirtualMethods for SVGElement {
fn super_type(&self) -> Option<&dyn VirtualMethods> { fn super_type(&self) -> Option<&dyn VirtualMethods> {
Some(self.upcast::<Element>() as &dyn VirtualMethods) Some(self.as_element() as &dyn VirtualMethods)
}
fn attribute_mutated(&self, attr: &Attr, mutation: AttributeMutation, can_gc: CanGc) {
self.super_type()
.unwrap()
.attribute_mutated(attr, mutation, can_gc);
let element = self.as_element();
if let (&local_name!("nonce"), mutation) = (attr.local_name(), mutation) {
match mutation {
AttributeMutation::Set(_) => {
let nonce = &**attr.value();
element.update_nonce_internal_slot(nonce.to_owned());
},
AttributeMutation::Removed => {
element.update_nonce_internal_slot(String::new());
},
}
}
} }
} }
@ -85,13 +108,16 @@ impl SVGElementMethods<crate::DomTypeHolder> for SVGElement {
// <https://html.spec.whatwg.org/multipage/#globaleventhandlers> // <https://html.spec.whatwg.org/multipage/#globaleventhandlers>
global_event_handlers!(); global_event_handlers!();
// FIXME: The nonce should be stored in an internal slot instead of an
// attribute (https://html.spec.whatwg.org/multipage/#cryptographicnonce)
// https://html.spec.whatwg.org/multipage/#dom-noncedelement-nonce // https://html.spec.whatwg.org/multipage/#dom-noncedelement-nonce
make_getter!(Nonce, "nonce"); fn Nonce(&self) -> DOMString {
self.as_element().nonce_value().into()
}
// https://html.spec.whatwg.org/multipage/#dom-noncedelement-nonce // https://html.spec.whatwg.org/multipage/#dom-noncedelement-nonce
make_setter!(SetNonce, "nonce"); fn SetNonce(&self, value: DOMString) {
self.as_element()
.update_nonce_internal_slot(value.to_string())
}
// https://html.spec.whatwg.org/multipage/#dom-fe-autofocus // https://html.spec.whatwg.org/multipage/#dom-fe-autofocus
fn Autofocus(&self) -> bool { fn Autofocus(&self) -> bool {

View file

@ -474524,7 +474524,7 @@
[] []
], ],
"reflection.js": [ "reflection.js": [
"b2c3b30aae36b390a60c05b39901826ba71e0b1a", "eeecd450fca8139e924affb298e7feb1a1fb46fb",
[] []
], ],
"render-blocking": { "render-blocking": {

View file

@ -1,24 +0,0 @@
[nonces.html]
[Basic nonce tests for meh in HTML namespace]
expected: FAIL
[Basic nonce tests for div in HTML namespace]
expected: FAIL
[Basic nonce tests for script in HTML namespace]
expected: FAIL
[Basic nonce tests for meh in SVG namespace]
expected: FAIL
[Basic nonce tests for svg in SVG namespace]
expected: FAIL
[Basic nonce tests for script in SVG namespace]
expected: FAIL
[Basic nonce tests for style in HTML namespace]
expected: FAIL
[Basic nonce tests for link in HTML namespace]
expected: FAIL

View file

@ -1,6 +1,3 @@
[script-nonces-hidden-meta.sub.html] [script-nonces-hidden-meta.sub.html]
[Writing 'nonce' IDL attribute.]
expected: FAIL
[createElement.nonce.] [createElement.nonce.]
expected: FAIL expected: FAIL

View file

@ -1,30 +1,3 @@
[script-nonces-hidden.html] [script-nonces-hidden.html]
[Reading 'nonce' content attribute and IDL attribute.]
expected: FAIL
[Cloned node retains nonce.]
expected: FAIL
[Cloned node retains nonce when inserted.]
expected: FAIL
[Writing 'nonce' IDL attribute.]
expected: FAIL
[Document-written script's nonce value.]
expected: FAIL
[createElement.nonce.] [createElement.nonce.]
expected: FAIL expected: FAIL
[setAttribute('nonce') overwrites '.nonce' upon insertion.]
expected: FAIL
[createElement.setAttribute.]
expected: FAIL
[Custom elements expose the correct events.]
expected: FAIL
[Nonces don't leak via CSS side-channels.]
expected: FAIL

View file

@ -2,9 +2,3 @@
expected: TIMEOUT expected: TIMEOUT
[Document-written script executes.] [Document-written script executes.]
expected: NOTRUN expected: NOTRUN
[createElement.nonce.]
expected: FAIL
[Writing 'nonce' IDL attribute.]
expected: FAIL

View file

@ -1,22 +1,4 @@
[svgscript-nonces-hidden.html] [svgscript-nonces-hidden.html]
expected: TIMEOUT expected: TIMEOUT
[Reading 'nonce' content attribute and IDL attribute.]
expected: FAIL
[Cloned node retains nonce.]
expected: FAIL
[Cloned node retains nonce when inserted.]
expected: FAIL
[Document-written script executes.] [Document-written script executes.]
expected: NOTRUN expected: NOTRUN
[createElement.nonce.]
expected: FAIL
[createElement.setAttribute.]
expected: FAIL
[Writing 'nonce' IDL attribute.]
expected: FAIL

View file

@ -967,6 +967,7 @@ ReflectionTests.reflects = function(data, idlName, idlObj, domName, domObj) {
"previous value", "getAttribute()"); "previous value", "getAttribute()");
ReflectionHarness.assertEquals(idlObj[idlName], previousIdl, "IDL get"); ReflectionHarness.assertEquals(idlObj[idlName], previousIdl, "IDL get");
} else { } else {
var previousValue = domObj.getAttribute(domName);
idlObj[idlName] = idlTests[i]; idlObj[idlName] = idlTests[i];
if (data.type == "boolean") { if (data.type == "boolean") {
// Special case yay // Special case yay
@ -976,6 +977,11 @@ ReflectionTests.reflects = function(data, idlName, idlObj, domName, domObj) {
var expected = idlDomExpected[i] + ""; var expected = idlDomExpected[i] + "";
if (data.isNullable && idlDomExpected[i] === null) { if (data.isNullable && idlDomExpected[i] === null) {
expected = null; expected = null;
} else if (idlName == "nonce") {
// nonce doesn't reflect the value, as per /content-security-policy/nonce-hiding/
// tests that confirm that retrieving the nonce value post IDL change does not
// reflect back to the attribute (for security reasons)
expected = previousValue;
} }
ReflectionHarness.assertEquals(domObj.getAttribute(domName), expected, ReflectionHarness.assertEquals(domObj.getAttribute(domName), expected,
"getAttribute()"); "getAttribute()");