Intercepting new Image().src requests

April 14th, 2017. Tagged: JavaScript

Maybe you're an attacker who sneaked in a little JavaScript to an unsuspecting site and would like to, well, sneak. Or maybe you want to know what exactly all these third-party analytics scripts are "calling home". Or maybe just for fun - wouldn't it be cool to intercept and log all requests made with new Image().src

new Image().src

It's a common pattern. A ping. Collect all the data then send it like:

new Image().src = 'http://example.org?time=' + Date.now() + '&...';

It's a strange API, that. As soon as you set a property (src), it does something. It sends a request. Weird. Oh well, it is what it is.

Interception

If it was any other normal method, like Array.prototype.map you can just overwrite it. But overwriting Image.prototype.src is a/ useless and b/ some browsers won't let you. Try Firefox:

> Image.prototype.src = "dude"
TypeError: 'set src' called on an object that does not implement interface HTMLImageElement.

Proxies!

JavaScript grows more powerful by the minute. What will they think of next? Proxies! That sounds like a lovely idea. Let's get to work!

First off, a copy of the original we're going to meddle with:

const NativeImage = Image;

Next, a "class" that will replace the original:

class FakeImage {
  constructor(w, h) {
    // magic!
  }
}

Finally, overwriting the original:

Image = FakeImage;

Now, how about that magic in the middle?

First, an instance of the original Image:

const nativeImage = new NativeImage(w, h);

Next, a handler that proxies calls to set and get methods and properties:

const handler = {
  set: function(obj, prop, value) {
    if (prop === 'src') {
      console.log('gotcha ' + value);
    }
    return nativeImage[prop] = value;
  },
  get: function(target, prop) {
    return target[prop];
  }
};

Finally, returning a Proxy instance that passes all there is to pass through the handler and onto the native Image instance.

return new Proxy(nativeImage, handler);

As you can see, all you need to do is check when src is being set and log it or do whatever with it. Interception complete!

Demo with all the code. Here it is in action in Firefox:

intercept

Hmm, someone might get suspicious

In the console:

> Image.name
"FakeImage"

Ouch.

Or even worse:

> Image.toString()

"function FakeImage(w, h) {
  const nativeImage = new NativeImage(w, h);
  const handler = {
  .....

... which should be more like native, all secret and such:

> NativeImage.toString()
"function Image() {
    [native code]
}"

Not good. An extra diligent developer might be checking for fakes before calling new Image(). (Who does that!? But still...)

Trying a naive approach won't cut it:

> Image.name = 'dude'
"dude"
> Image.name
"FakeImage"

Luckily, there's Object.defineProperty:

Object.defineProperty(FakeImage, 'name', {
  enumerable: false,
  configurable: false,
  writable: false,
  value: 'Image'
});

Testing:

> Image.name
"Image"

Tada!

Same with that toString() (and while at it, toSource() which is a Firefox invention):

Object.defineProperty(FakeImage, 'toString', {
  enumerable: true,
  configurable: false,
  writable: true,
  value: function() {
    return NativeImage.toString();
  }
});

if ('toSource' in NativeImage) { // FF extra
  Object.defineProperty(FakeImage, 'toSource', {
    enumerable: false,
    configurable: false,
    writable: true,
    value: function() {
      return NativeImage.toSource();
    }
  });
}

Testing now:

> Image.toString()
"function Image() {
    [native code]
}"
> Image.toSource()
"function Image() {
    [native code]
}"

Can you tell the fake? Don't think so.

Did you notice the NativeImage.toSource() call? Instead of hardcoding the [native code] mumbo-jumbo string, just ask the original. Especially given that browsers vary in the exact output.

Suspicious still...

What about toString() on instances? What about valueOf()?

> new Image().toString()
"[object Object]"
> new Image().valueOf()
Proxy { <target>: <img>, <handler>: Object }

Compare to the original:

> new NativeImage().valueOf()
<img>
> new NativeImage().toString()
"[object HTMLImageElement]"

Oh crap! No one must see them proxies and objects.

The fix is in the get method of the Proxy handler. Some properties are functions. Handle accordingly:

get: function(target, prop) {
  let result = target[prop];
  if (typeof result === 'function') {
    result = result.bind(target);
  }
  return result;
}

Boom! Like a charm!

fake

Fake it till you make it!

Recall the ole Object.prototype.toString.call call, y'all? People have used it since forever to tell, for example, real arrays from array-like things, such as arguments and NodeLists. (That was in the olden days before Array.isArray()).

Still very useful to tell, e.g. native JSON support vs a polyfill.

How does our little Image "polyfill" behave?

> Object.prototype.toString.call(Image)
"[object Function]"
> Object.prototype.toString.call(NativeImage)
"[object Function]"

Hm, alright. Next?

> Object.prototype.toString.call(new Image)
"[object Object]"
> Object.prototype.toString.call(new NativeImage)
"[object HTMLImageElement]"

Poop! We're caught in the act.

There's a fix. Wait for it. Symbol.toStringTag. Yup, that's right.

Back in the constructor, before you return...

const prox = new Proxy(nativeImage, handler);
try {
  prox[Symbol.toStringTag] = 'HTMLImageElement';
} catch(e){}
return prox;

What magic is this!

You're a wizard in a blizzard,
A mystical machine gun!

(Actually Chrome returns HTMLImageElement to begin with, so no fix is needed. And the fix is wrapped in try-catch, cause Chrome doesn't like it. Safari is more like Firefox returning "[object ProxyObject]" instead of "[object Object]" without the toStringTag fix.)

Cherry on top

No one checks the prototypes of the potentially useful things, but, hey, we're overachieving here.

Firefox and Safari agree:

> Object.prototype.toString.call(NativeImage.prototype)
"[object HTMLImageElementPrototype]"

Chrome, the oddball:

Object.prototype.toString.call(NativeImage.prototype)
"[object HTMLImageElement]"

But all agree that our Image is smelly:

> Object.prototype.toString.call(Image.prototype)
"[object Object]"

Fix:

FakeImage.prototype[Symbol.toStringTag] = NativeImage.prototype.toString();

Again, not hardcoding a string, but giving the browser-dependent different output, piggybacking on the native Image.

Yup!

Fake it till you make it. Result to play with.

Our fake is still recognizable in the browser console (like console.log(new Image())) but your victim (unsuspecting logging-reporting-ads-analytics script) is code. It doesn't look at the console. An Object.prototype.toString.call() is usually the extend of all checks for nativeness. If that.

Bye!

Tell your friends about this post on Facebook and Twitter

Sorry, comments disabled and hidden due to excessive spam.

Meanwhile, hit me up on twitter @stoyanstefanov