From d9928760ed2049befd59f5fc0b53d0d3ff74ff95 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Mon, 12 Oct 2026 06:55:06 +0800 Subject: [PATCH] fix(webidl): isolate internal descriptor records from prototype pollution --- moli-webapi-declare/src/declaration.rs | 5 +++ moli-webapi-declare/src/prototype.rs | 11 +++--- moli-webapi-declare/tests/declare.rs | 52 ++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 5 deletions(-) diff --git a/moli-webapi-declare/src/declaration.rs b/moli-webapi-declare/src/declaration.rs index d52213a313..f59bcbb2fd 100644 --- a/moli-webapi-declare/src/declaration.rs +++ b/moli-webapi-declare/src/declaration.rs @@ -64,6 +64,10 @@ pub trait WebApiObjectDeclaration<'s> { #[derive(WebApiObject)] #[webapi(plain, data_properties, enumerable, no_dynamic_constructor)] pub struct DataPropertyDescriptorDeclaration<'scope> { + // V8 converts this internal record with ToPropertyDescriptor, which also + // reads inherited get/set fields. Author Object.prototype must not enter it. + #[webapi(prototype, value = v8::null(scope))] + prototype: (), value: v8::Local<'scope, v8::Value>, enumerable: bool, configurable: bool, @@ -73,6 +77,7 @@ pub struct DataPropertyDescriptorDeclaration<'scope> { impl<'scope> DataPropertyDescriptorDeclaration<'scope> { pub fn new(value: v8::Local<'scope, v8::Value>, writable: bool, enumerable: bool) -> Self { Self { + prototype: (), value, enumerable, configurable: true, diff --git a/moli-webapi-declare/src/prototype.rs b/moli-webapi-declare/src/prototype.rs index c1ac56c28f..af6b36dc6e 100644 --- a/moli-webapi-declare/src/prototype.rs +++ b/moli-webapi-declare/src/prototype.rs @@ -68,11 +68,12 @@ where let prototype = prototype .to_v8_value(scope) .ok_or_else(|| BindError::new("failed to convert declared prototype"))?; - let prototype = v8::Local::::try_from(prototype) - .map_err(|_| BindError::new("declared prototype must be an object"))?; - let installed = object - .set_prototype(scope, prototype.into()) - .unwrap_or(false); + if !prototype.is_object() && !prototype.is_null() { + return Err(BindError::new( + "declared prototype must be an object or null", + )); + } + let installed = object.set_prototype(scope, prototype).unwrap_or(false); if !installed { return Err(BindError::new("failed to set declared prototype")); } diff --git a/moli-webapi-declare/tests/declare.rs b/moli-webapi-declare/tests/declare.rs index c77a5cbaf3..240aeadbe4 100644 --- a/moli-webapi-declare/tests/declare.rs +++ b/moli-webapi-declare/tests/declare.rs @@ -3094,3 +3094,55 @@ mod interfaces { pub(super) SampleTemplate; } } + +#[test] +fn null_prototype_declarations_ignore_object_prototype_pollution() { + #[derive(WebApiObject)] + #[webapi(plain)] + struct NullPrototype { + #[webapi(prototype, value = v8::null(scope))] + prototype: (), + } + + ensure_v8(); + let mut isolate = v8::Isolate::new(v8::CreateParams::default()); + let scope = pin!(v8::HandleScope::new(&mut isolate)); + let scope = &mut scope.init(); + let context = v8::Context::new(scope, Default::default()); + let scope = &mut v8::ContextScope::new(scope, context); + let object = NullPrototype::new().bind(scope).unwrap(); + assert!(object.get_prototype(scope).unwrap().is_null()); + let invalid = v8::Integer::new(scope, 1); + assert!(moli_webapi_declare::set_declared_prototype(scope, object, &invalid).is_err()); + assert!(object.get_prototype(scope).unwrap().is_null()); + run_script( + scope, + "Object.prototype.get = () => { throw Error('inherited getter') };", + ); + let value = v8::Integer::new(scope, 7); + let descriptor = + moli_webapi_declare::DataPropertyDescriptorDeclaration::new(value.into(), false, true) + .bind(scope) + .unwrap(); + assert!(descriptor.get_prototype(scope).unwrap().is_null()); + let global = context.global(scope); + assert_eq!( + global.create_data_property( + scope, + v8::String::new(scope, "descriptor").unwrap().into(), + descriptor.into() + ), + Some(true) + ); + assert!( + run_script( + scope, + r#"(() => { + const target={};Object.defineProperty(target,'value',descriptor); + const d=Object.getOwnPropertyDescriptor(target,'value'); + return target.value===7 && d.writable===false && d.enumerable && d.configurable; + })()"# + ) + .is_true() + ); +}