Passing Proxy object as thisArgument to apply throws TypeError: Illegal Invocation

480 Views Asked by At

I'm trying to trap calls to Storage. As far as I can tell there are two ways to call either setItem or getItem:

    sessionStorage.setItem("foo", "bar");
    let item = sessionStorage.getItem("foo");

    Storage.prototype.setItem.call(sessionStorage, "foo", "bar");
    let item2 = Storage.prototype.getItem.call(sessionStorage, "foo");

Using sessionStorage in Code Snippet throws a security error, so here's the code in JS Fiddle

TLDR: I can handle all cases if I do this, but it seems hacky. Is there a better/cleaner way to accomplish my goal? (NOTE: I don't have control over caller, this is why I'm covering both cases):

try {
    console.log("\n\n");
    let ss = new Proxy(sessionStorage, {
        get: function (getTarget, p) {
            if (p === "__this") {
                // kinda hacky, but allows us to unwrap Proxy for binding
                return getTarget;
            }
            console.log("sessionStorage.get proxy called")
            return new Proxy(Reflect.get(getTarget, p), {
                apply(applyTarget, thisArg, argArray) {
                    console.log("sessionStorage.get.apply called");
                    Reflect.apply(applyTarget, getTarget, argArray);
                }
            })
        },
    });

    SP = new Proxy(Object.create(Storage.prototype), {
        get: function (getTarget, p) {
            console.log("Storage.get proxy called")
            return new Proxy(Reflect.get(getTarget, p), {
                apply(applyTarget, thisArg, argArray) {
                    console.log("Storage.get.apply called");
                    try {
                        return Reflect.apply(applyTarget, thisArg, argArray);
                    } catch (e) {
                        // unpack proxy if we're double-wrapped (both target and thisArg are Proxy)
                        return Reflect.apply(applyTarget, thisArg.__this, argArray);
                    }
                }
            })
        },
    });
    SPW = {};
    Object.defineProperty(SPW, 'prototype', {
        value: SP,
        configurable: false,
    });
    si = SPW.prototype.setItem;
    gi = SPW.prototype.getItem;
    si.call(ss, "foo", "3");
    console.log(`Storage.prototype.getItem: ${gi.call(ss, "foo")}`);
    console.log(`Storage.prototype Worked`)
} catch (e) {
    console.log(`Storage.prototype Caught ${e.stack}`);
}

JSFiddle.

Result:

Storage.get proxy called
Storage.get proxy called
Storage.get.apply called
Storage.get.apply called
Storage.prototype.getItem: 3
Storage.prototype Worked

I don't see any other way than including a "hidden" __this property so that the caller can "unwrap" the Proxy object and get the reference to the original sessionStorage. Is there a better way to do this?

Background

If it helps here are examples of only wrapping either sessionStorage or Storage, but not both:

Wrap sessionStorage

For the apply trap, I have to pass getTarget instead of thisArg because the latter is Proxy object and if I pass it an Illegal Invocation error is thrown.

    try {
        let ss = new Proxy(sessionStorage, {
            get: function (getTarget, p) {
                console.log("sessionStorage.get called")
                return new Proxy(Reflect.get(getTarget, p), {
                    apply(applyTarget, thisArg, argArray) {
                        console.log("sessionStorage.get.apply called");
                        Reflect.apply(applyTarget, getTarget, argArray);
                    }
                })
            },
        });
        ss.setItem("foo", "1");
        console.log(`Proxy.sessionStorage.getItem: ${ss.getItem("foo")}`);
        console.log(`Proxy.sessionStorage Worked`)
    } catch (e) {
        console.log(`Proxy.sessionStorage Caught ${e.stack}`);
    }

JS Fiddle

Result:

sessionStorage.get called
sessionStorage.get.apply called
sessionStorage.get called
sessionStorage.get.apply called
sessionStorage.get called
sessionStorage.get.apply called
Proxy.sessionStorage.getItem: undefined
Proxy.sessionStorage Worked

Wrap Storage.prototype

Here I have to first create a new object with a separate prototype because the prototype property descriptor for Storage has configurable set to false. Once I've done that I essentially have to do the same thing as the previous case by passing the "unwrapped" getTarget instead of the Proxy instance pointed to by thisArg.

    try {
        console.log("\n\n");
        SP = new Proxy(Object.create(Storage.prototype), {
            get: function (getTarget, p) {
                console.log("Storage.get proxy called")
                return new Proxy(Reflect.get(getTarget, p), {
                    apply(applyTarget, thisArg, argArray) {
                        console.log("Storage.get.apply called");
                        try {
                            return Reflect.apply(applyTarget, thisArg, argArray);
                        } catch (e) {
                            console.log("apply failed when passing thisArg");
                            return Reflect.apply(applyTarget, getTarget, argArray);
                        }
                    }
                })
            },
        });
        SPW = {};
        Object.defineProperty(SPW, 'prototype', {
            value: SP,
            configurable: false,
        });
        si = SPW.prototype.setItem;
        gi = SPW.prototype.getItem;
        si.call(sessionStorage, "foo", "2");
        console.log(`Storage.prototype.getItem: ${gi.call(sessionStorage, "foo")}`);
        console.log(`Storage.prototype Worked`)
    } catch (e) {
        console.log(`Storage.prototype Caught ${e.stack}`);
    }

JS Fiddle

Result:

Storage.get proxy called
Storage.get proxy called
Storage.get.apply called
Storage.get.apply called
Storage.prototype.getItem: 2
Storage.prototype Worked
1

There are 1 best solutions below

4
On BEST ANSWER

I think you're making this far too complicated. There's no reason to involve a proxy here, just monkey-patch the two methods:

const proto = Storage.prototype;
const originalSet = proto.setItem;
const originalGet = proto.getItem;
Object.assign(proto, {
    setItem(key, value) {
        console.log(`Setting ${JSON.stringify(key)} to ${JSON.stringify(value)} on a ${this.constructor.name}`);
        return originalSet.call(this, key, value);
    },
    getItem(key) {
        console.log(`Getting ${JSON.stringify(key)} from a ${this.constructor.name}`);
        return originalGet.call(this, key);
    },
});