0

I need to hijack XHR on a page, I start from proxy the XHR object, first proxy new operator:

class ClassHandler {
  constructor(proxy) {
    this.proxy = proxy;
  }

  construct(target, args) {
    const obj = new target(...args);
    return new Proxy(obj, new this.proxy(obj));
  }
}

(function(XMLHttpRequest) {
  unsafeWindow.XMLHttpRequest = new Proxy(
    XMLHttpRequest,
    new ClassHandler(XhrHandler)
  );
})(XMLHttpRequest);

Then XhrProxy needs to proxy everything to real XHR object, I found this is very complicated, so separate it into several components.

Basic properties

const ProxyGetTarget = Symbol('ProxyGetTarget');
const ProxyGetHandler = Symbol('ProxyGetHandler');
class ObjectHandler {
  constructor(target) {
    this.target = target;
  }

  get(target, prop, receiver) {
    if (target.hasOwnProperty(prop)) {
      return Reflect.get(target, prop, receiver);
    } else if (prop == ProxyGetTarget) {
      return target;
    } else if (prop == ProxyGetHandler) {
      return this;
    } else {
      const value = target[prop];
      if (typeof value == 'function')
        return new Proxy(value, new FunctionHandler(value));
      return value;
    }
  }

  set(target, prop, value) {
    return Reflect.set(target, prop, value);
  }
}

I injected a property ProxyGetTarget which returns the real target for future use. Next I'll explain why proxy functions with FunctionHandler in the object.

Member function calls

This is a bit tricky, when calling a function on the object:

xhr.open()

xhr is our proxy object, not the real native XHR object, the open function will be called with this set to the proxy object, In order to forward open call to real xhr object, we need proxy all functions returned by get:

class FunctionHandlerBase extends ObjectHandler {
  apply(target, thisArg, argumentsList) {
    const realTarget = thisArg[ProxyGetTarget];
    if (!realTarget) throw new Error('illegal invocations');
    return this.call(this.target, thisArg, realTarget, argumentsList);
  }
}

class FunctionHandler extends FunctionHandlerBase {
  call(fn, proxy, target, argumentsList) {
    fn.apply(target, argumentsList);
  }
}

With this setup, I can inject any code into any member function using following method:

class XhrHandler extends ObjectHandler{
  get(target, prop, receiver) {
    if (prop === 'open') {
      return new Proxy(target.open, new this.open(target.open));
    } else {
      return super.get(target, prop, receiver);
    }
  }
}

XhrHandler.prototype.open = class extends FunctionHandlerBase {
  call(fn, proxy, realTarget, argumentsList) {
    // Do whatever before real xhr.open call
    // ...
    // ...

    // Call real xhr.open
    return fn.apply(realTarget, argumentsList);
  }
};

Event listeners

As event listeners will expose real object as this argument in the callback function, I don't want anyone get my secret real target, I should also wrap it with the proxy.

class EventTargetHandler extends ObjectHandler {
  constructor(target) {
    super(target);
    this.listeners = {};
  }

  getListeners(type) {
    if (!this.listeners.hasOwnProperty(type))
      this.listeners[type] = new Map();
    return this.listeners[type];
  }

  get(target, prop, receiver) {
    if (prop === 'addEventListener') {
      return new Proxy(
        target.addEventListener,
        new this.addEventListener(target.addEventListener, this)
      );
    } else if (prop === 'removeEventListener') {
      return new Proxy(
        target.removeEventListener,
        new this.removeEventListener(target.removeEventListener, this)
      );
    } else return super.get(target, prop, receiver);
  }
}

EventTargetHandler.prototype.addEventListener = class extends FunctionHandlerBase {
  call(fn, proxy, realTarget, argumentsList) {
    const type = argumentsList[0];
    const listener = argumentsList[1];
    const bridge = listener.bind(proxy);
    argumentsList[1] = bridge;
    proxy[ProxyGetHandler].getListeners(type).set(listener, bridge);
    return fn.apply(realTarget, argumentsList);
  }
};

EventTargetHandler.prototype.removeEventListener = class extends FunctionHandlerBase {
  call(fn, proxy, realTarget, argumentsList) {
    const type = argumentsList[0];
    const listener = argumentsList[1];
    const cache = proxy[ProxyGetHandler].getListeners(type);
    if (cache.has(listener)) {
      argumentsList[1] = cache.get(listener);
      cache.delete(listener);
    }
    return fn.apply(realTarget, argumentsList);
  }
};

Event handlers

Not done yet ... I need to wrap all set to the event handler properties.

Problems

Hmm, hijack XHR ? easy task, let's do it in 5 minutes.

...

...

...

After 5 hours, I'm still struggling with those proxies.

Maybe I misunderstand the propose of the design of Proxy API, I found it very complicated to wrap an object, almost impossible.

I just want my Proxy behaves as same as the origin object, is there any easier approach ?

Zang MingJie
  • 5,164
  • 1
  • 14
  • 27
  • 1
    What is the higher level objective of the hijacking? Seems over complicated creating a proxy for each method – charlietfl Dec 23 '18 at 15:23
  • @charlietfl My goal is easy, just make my proxy behave as same as the target. Sometimes return a this bounded function may work, but what if used like this `xhr1.open.apply(xhr2, ['GET', 'https://example.com/'])` – Zang MingJie Dec 23 '18 at 15:36
  • 1
    OK. Take a look at https://stackoverflow.com/a/53401323/1175966 and from that explain what additional functionality would be needed. Trying to understand what your higher level objective is – charlietfl Dec 23 '18 at 15:44
  • @charlietfl Without use of proxy, it can be easily detected by page, `XMLHttpRequest.prototype.open` should be a native function, but has been replaced by an js function. – Zang MingJie Dec 23 '18 at 16:23
  • 1
    What does *"it can be easily detected by page"* mean? You still haven't explained higher level objective – charlietfl Dec 23 '18 at 17:11
  • a) Yes, proxying native (exotic) objects is complicated b) I'd suggest to do it less generic, do not use dynamically created `class` instances for your handlers but simply plain objects. Start simply, add abstractions later when you feel you need them. – Bergi Dec 23 '18 at 19:36
  • @Bergi I also realized that native functions are causing problems, unlike pure js function, the native function doesn't accept proxy object as this. But the event handler/listener problem doesn't related to native functions. Real `this` is exposed to the constructor of the object, if the object stores real `this` somewhere then pass it outside via event handler/listener may cause problems. – Zang MingJie Dec 23 '18 at 19:55
  • Yes, that's another reason to keep the wrapper as thin as possible. But you still haven't told us *why* you "*need to hijack XHR on a page*" and what you want to do with the highjacked objects. – Bergi Dec 23 '18 at 19:58
  • @Bergi write an UserScript using tampermonkey. I want to make it as stable as I can, so the hijacked object should behave as same as original object. – Zang MingJie Dec 23 '18 at 20:04

0 Answers0