0

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
Mark J Miller
  • 4,751
  • 5
  • 44
  • 74
  • What exactly are the calls and/or assignments you are trying to intercept? Probably you shouldn't be using `Proxy` at all, it just makes everything more complicated. – Bergi Aug 21 '21 at 14:59
  • I’m trying to intercept calls made by browser fingerprinting scripts. My intention is to log them and analyze the data they are collecting. Other scripts I’ve looked at fail to completely log all the data being collected, this is an example of how the fingerprinting circumvents attempts to intercept operations: by using a prototype of an object which is not configurable. – Mark J Miller Aug 21 '21 at 15:22
  • Could you add the concrete calls made by some example browser fingerprinting script, please? Does it only call `localStorage.getItem()` and `localStorage.setItem()`, or does it access the `localStorage` in any other way? – Bergi Aug 21 '21 at 17:26
  • @Bergi the specific example that sent me searching in this case uses the prototype method I demonstrated at the start of my question. Part of the reason though for my approach is because the scripts are usually obfuscated so I’m trying to observe their calls through interception instead of reverse engineering. So instead of solving for a single website at a single point in time I’m trying to develop a broad solution that can observe arbitrary scripts along with changes in which data is collected and how. – Mark J Miller Aug 21 '21 at 19:51

1 Answers1

1

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);
    },
});
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • I'll accept this as my answer as it does answer the question "is there a simpler way". However, I'm trying to wrap a large number of objects in the browser and I don't know all the methods or properties which are being used up front without reverse engineering obfuscated scripts for each website. It isn't practical for me to repeat this option for all the methods I may need, so I'm going to stick with my original method. – Mark J Miller Aug 21 '21 at 20:23
  • @MarkJMiller You can actually enumerate all the global native objects and their prototypes and methods. As for proxying builtin methods (be they native to js or provided by the host), we [already discussed this in your last qustion](https://stackoverflow.com/questions/68778159/javascript-why-doesnt-proxy-intercept-methods/68779009#comment121551689_68779009) - it's still the same problem. – Bergi Aug 21 '21 at 20:45
  • Added a [JSFiddle](https://jsfiddle.net/xgzc9qev/) for this answer. – Mark J Miller Aug 23 '21 at 15:34