From ce772df1e5c071b62205a84e1f76630e2ce4a4e7 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Mon, 13 Jul 2026 15:37:04 +0300 Subject: [PATCH] Fix Proxy get trap being bypassed for properties named "revoke" JsProxy.Get contained a special case that skipped the get trap entirely for any property named "revoke", so `new Proxy({}, { get: () => 'trapped' }).revoke` returned undefined instead of 'trapped'. Per the spec (Proxy [[Get]], https://tc39.es/ecma262/#sec-proxy-object-internal-methods-and-internal-slots-get-p-receiver) there is no such exception: the trap must be called for every property key. The bypass is dead legacy from the original Reflect & Proxy implementation (f89886063, 2019), where revocation was wired through dynamic property lookups on the proxy itself. Today ProxyConstructor.Revocable captures the JsProxy instance in a closure and nulls its target/handler slots directly, so nothing depends on "revoke" resolving against the target. Cross-checked with V8 (Node v24.18.0): `new Proxy({}, {get: () => 'trapped'}).revoke` prints "trapped", and property access on a revoked proxy still throws "TypeError: Cannot perform 'get' on a proxy that has been revoked". Adds tests for the trapped "revoke" property and for the TypeError on property access after revocation. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01EsHuKuE4UihKZp5HYapDHE --- Jint.Tests/Runtime/ProxyTests.cs | 18 ++++++++++++++++++ Jint/Native/JsProxy.cs | 3 +-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Jint.Tests/Runtime/ProxyTests.cs b/Jint.Tests/Runtime/ProxyTests.cs index eb6b5fc3a5..19b3d26b96 100644 --- a/Jint.Tests/Runtime/ProxyTests.cs +++ b/Jint.Tests/Runtime/ProxyTests.cs @@ -23,6 +23,24 @@ public void ProxyCanBeRevokedWithoutContext() "); } + [Fact] + public void GetTrapIsCalledForPropertyNamedRevoke() + { + Assert.Equal("trapped", _engine.Evaluate("new Proxy({}, { get: () => 'trapped' }).revoke").AsString()); + } + + [Fact] + public void AccessingPropertyOnRevokedProxyThrowsTypeError() + { + _engine.Execute(@" + var revocable = Proxy.revocable({ foo: 1 }, { get: () => 'trapped' }); + var proxy = revocable.proxy; + revocable.revoke(); + "); + var ex = Assert.Throws(() => _engine.Evaluate("proxy.foo")); + AssertJsTypeError(_engine, ex, "Cannot perform 'foo' on a proxy that has been revoked"); + } + [Fact] public void ProxyToStringUseTarget() { diff --git a/Jint/Native/JsProxy.cs b/Jint/Native/JsProxy.cs index 011810c2a8..14b0f22fa8 100644 --- a/Jint/Native/JsProxy.cs +++ b/Jint/Native/JsProxy.cs @@ -24,7 +24,6 @@ internal sealed class JsProxy : ObjectInstance, IConstructor, ICallable private static readonly JsString TrapOwnKeys = new JsString("ownKeys"); private static readonly JsString TrapConstruct = new JsString("construct"); - private static readonly JsString KeyFunctionRevoke = new JsString("revoke"); private static readonly JsString KeyIsArray = new JsString("isArray"); public JsProxy( @@ -129,7 +128,7 @@ public override JsValue Get(JsValue property, JsValue receiver) AssertTargetNotRevoked(property); var target = _target; - if (KeyFunctionRevoke.Equals(property) || !TryCallHandler(TrapGet, [target, TypeConverter.ToPropertyKey(property), receiver], out var result)) + if (!TryCallHandler(TrapGet, [target, TypeConverter.ToPropertyKey(property), receiver], out var result)) { return target.Get(property, receiver); }