From ef497c268c38abb9e894007be6b9333afba16333 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Sun, 26 Jul 2026 20:57:36 +0300 Subject: [PATCH] Stop DOM nodes from claiming inherited members as own properties `DomNodeInstance.GetOwnProperty` falls back to returning the prototype's descriptor when the node has no own property of that name. `[[GetOwnProperty]]` is not supposed to look past the object itself, so a node ends up disagreeing with itself: document.body.hasOwnProperty('firstChild') // true Object.getOwnPropertyDescriptor(b, 'firstChild') // an accessor Object.getOwnPropertyNames(document.body) // [] Object.keys(document.body) // [] Library code that feature-detects with `hasOwnProperty` before walking the prototype chain gets the wrong answer, and anything that pairs `getOwnPropertyNames` with `getOwnPropertyDescriptor` sees an object whose keys and descriptors do not agree. Drop the fallback. An indexer is the only thing that can legitimately produce an own property here, so that probe stays; everything else is a member of the DOM interface, lives on the prototype, and is found by the engine's ordinary own -> prototype lookup. Reads, writes, method calls and `in` are unaffected - only the "is this mine?" answer changes. As a side effect the node stops answering the prototype's own-property probe on behalf of the prototype, which is what lets an engine cache a prototype lookup: such caches only engage once the receiver genuinely reports no own property. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0179sA2T7HuRfRfSc2JirFik --- src/AngleSharp.Js.Tests/DomTests.cs | 28 ++++++++++++++++++++ src/AngleSharp.Js/Proxies/DomNodeInstance.cs | 18 +++++-------- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/AngleSharp.Js.Tests/DomTests.cs b/src/AngleSharp.Js.Tests/DomTests.cs index e14fe45..bfd1e4c 100644 --- a/src/AngleSharp.Js.Tests/DomTests.cs +++ b/src/AngleSharp.Js.Tests/DomTests.cs @@ -82,5 +82,33 @@ public async Task ConsoleKeepsPropertiesAssignedToIt() var result = await "(function () { window.console.marker = 'kept'; return window.console.marker; })()".EvalScriptAsync(); Assert.AreEqual("kept", result); } + + [Test] + public async Task InheritedMemberIsNotAnOwnPropertyOfTheNode() + { + var result = await "document.createElement('div').hasOwnProperty('firstChild')".EvalScriptAsync(); + Assert.AreEqual("False", result); + } + + [Test] + public async Task InheritedMemberIsStillVisibleOnTheNode() + { + var result = await "('firstChild' in document.documentElement) + ',' + (typeof document.documentElement.appendChild)".EvalScriptAsync(); + Assert.AreEqual("true,function", result); + } + + [Test] + public async Task InheritedAccessorStillReadsAndWrites() + { + var result = await "(function () { var d = document.createElement('div'); d.id = 'jint'; return d.id; })()".EvalScriptAsync(); + Assert.AreEqual("jint", result); + } + + [Test] + public async Task AssignedPropertyIsReportedConsistently() + { + var result = await "(function () { var d = document.createElement('div'); d.custom = 1; return d.hasOwnProperty('custom') + ',' + Object.getOwnPropertyNames(d).join(); })()".EvalScriptAsync(); + Assert.AreEqual("true,custom", result); + } } } diff --git a/src/AngleSharp.Js/Proxies/DomNodeInstance.cs b/src/AngleSharp.Js/Proxies/DomNodeInstance.cs index dba951b..d70ba4c 100644 --- a/src/AngleSharp.Js/Proxies/DomNodeInstance.cs +++ b/src/AngleSharp.Js/Proxies/DomNodeInstance.cs @@ -67,18 +67,14 @@ public DomEventInstance.Registration RemoveEventHandler(DomEventInstance ev) public override PropertyDescriptor GetOwnProperty(JsValue property) { - if (Prototype is DomPrototypeInstance prototype) + // An indexer is the only thing that can turn into an own property of the node + // itself. The members of the DOM interface live on the prototype, so finding + // them is the engine's job - answering them here would make the node claim + // every inherited member as its own. + if (Prototype is DomPrototypeInstance prototype && + prototype.TryGetFromIndex(_value, property.ToString(), out var descriptor)) { - if (prototype.TryGetFromIndex(_value, property.ToString(), out var descriptor)) - { - return descriptor; - } - - var prototypeProperty = prototype.GetOwnProperty(property); - if (prototypeProperty != PropertyDescriptor.Undefined) - { - return prototypeProperty; - } + return descriptor; } return base.GetOwnProperty(property);