francois.remy.dev at outlook.com
Wed Jan 9 08:30:19 PST 2013
> >> When later getting that node out of the DOM with .firstChild, what
> >> should be handed back? The proxy that was passed in, the JS object
> >> that proxy was wrapping, something else (e.g. an exception is thrown)?
> > The principle of least surprise would say the proxy that was passed in
> That's actually not that great either. If you're handing out proxies as
> membranes, and the caller of .firstChild should be getting a different
> membrane than the caller of appendChild had, you lose.
I'm not sure I got your idea, but I maybe did. Okay, let's take a simple example, then.
Let's say we have a <P> element and a membrane around it whose goal is to only allow you to change textContent (but you can read everything else, if you want to). When you read something and that the return value is an object, it's wrapped in a membrane where you can't change anything (that way if you take el.parentElement.firstChild you get a second, more restrictive membrane for el).
What you firstly claim is that the membrane is completely ineffective if you can retrieve the object via the DOM in any other form than the membrane you used in first hand because as soon as you added the element in the DOM, you can use document.getElementById(el.uniqueID) to retrieve it unprotected (or you can use something else).So, whatever membrane was used should continue to be used. From now one, any DOM API will need to return this "readonly except textContent" version to preserve compatibilty.
What you claimed in second position is that even if you do that, it's still a broken design. Indeed, let's say you sent the readonly version to a function to make sure it doesn't modify the object. Again, if you added the element in the DOM, it can retrieve it and the function will recieve the other membrane, which allow him to change textContent.
However, this second problem is not related to the DOM but to globally accessible variables. If you maintain an object stored somewhere in the globally accessible world (be it the DOM or any kind of global variable), even if you create a membrane around those objects, the user can retrieve an unsecure version of it via the Map. No membrane system can be secure through global variable access, so we should not worry about that.
According to me, the only thing we want to make sure with a Proxy is that you can't actually extract the value of a DOMObject that's not added nor addable to any globally accessible map (like a CustomEvent). If you can, by creating an element and calling dispatchEvent on that element using the membrane and get the unmembraned CustomEvent in the event listener, then we've a problem.
I think it should be possible to work around this problem by making sure every object has a C++ Decorator (in this case ProxiedCustomEvent) which inherits from the base class (CustomEvent) and forward all calls to the extracted object identity but has a method like "getScriptValue" that returns the membrane from which the native value was extracted.
When a native method is called with a Proxy, a new ProxiedCustomEvent(proxy.target, proxy) is created and passed to the native method. When the ProxiedCustomEvent is given back to the script world, his getScriptValue method is called to return the original proxy.
So, basically, el.appendChild(membrane) will cause el.lastChild===membrane to be true, and dispatchEvent(membranedEvent) will not allow to retrieve an unmembraned event object.
Albeit possible, this is quite a bit of work, however. In the mean time, we should probably make it impossible to proxy an host object whose object identity rely on something else that the prototype chain. That means that we should probably get this concept of "object identity" specced somewhere.
The question in this case should be: can I create a "secure but broken" readonly proxy from a DOM object that cannot be used as parameter for native functions (if we allow a Proxy to take the native object identity of the target when used in native calls OR in the mean time if we make the proxyfication throw)?
Yes: you can create a new, empty target with no object identity and create getters/setters to encapsulate the real DOM object. Actually, you didn't remove any ability by allow proxies to take the object identity.
However, if the Proxy cannot possibly use the identity of the target object in native calls, you'll not be able to emulate it. So, if we want to use the most capable solution, we need a way to "transfer" target -> proxy identity, and that means we need to "throw as not impelemented" the proxification of native objects in the mean time.
The only problem I still see with the ProxiedXXX decorator approach is that, normally, when you are using a proxy, you can "control" the value returned by some property calls (let's say you pretend innerHTML is "" while this is not true) but in the "native" world, your barrier will not apply and therefore one can get the innerHTML by using a DOMSerializer. If we want to use the proxy's own innerHTML we need to reveal to it the actual algorithm applied on the object and perform a lot of casting and typecheck. This is not ideal.
I would certainly understand if the ECMAScript group settled up not to work on Proxied native elements and specify that it should throw on creation. However, I would advise to create an Object.hasIdentity(...) method that returns true if the given object has a native identity (and can therefore not be proxied nor deep-cloned). Because, when you think about it, I can deep-clone any "JS" object and except that o!===o2 they will be the same and work in the same contexts, but this is not true for DOM objects, because the "clone" will not have a valid object identity.
We already leak that info, maybe it's time to acknowledge it in a spec.
More information about the es-discuss