In my previous blog post, I discussed different methods used to measure ads viewability. In the end of it, I’ve mentioned those methods could be spoofed by bad guys, and today I’ll show you how. Originally I wanted to analyze the code mentioned here, but since I wasn’t able to find it, I decided to write it myself.

Disclaimer: everything written below, and generally in this blog, is intended for educational purposes only.

Let’s assume we have a bot who wants to trick viewability metrics. Let’s assume the ads are served inside cross origin iframes, so geometric approach is irrelevant here. The constrains of this approach is that the code must be executed in the same browsing context (frame) as the measurement code. This is not a problem for bot, as it controls the browser and can execute code in any browsing context.

There are several approaches that will work, the one I chose is making the APIs behave as if the ad is viewable. The main benefit of this approach is that it’s universal and will work regardless of the specific viewability measurement vendor, as opposed to the replay approach where each requires different implementation for each vendor.

Another decision is to make the API behave in JavaScript. The main benefit is ease of implementation and portability between different browsers, as opposed to do it on the browser level which requires more work and different implementation for each browser.

The main drawback of this method is that it can be detected by using reflection, that is, it’s possible to detect that the APIs were tampered with, although I believe it’s possible to over come this drawback. Maybe I’ll write how in the future 🙂

Note that these examples aren’t complete nor robust and probably won’t work as is in production, they are presented as a PoC for educational purposes.

So lets go over each and every browser optimization and API measurement method:

requestAnimationFrame and timers throttling

As documented in MDN:

Some processes are exempt from this throttling behavior. In these cases, you can use the Page Visibility API to reduce the tabs’ performance impact while they’re hidden. Tabs which are playing audio are considered foreground and aren’t throttled.

Tabs running code that’s using real-time network connections (WebSockets and WebRTC) go unthrottled in order to avoid closing these connections timing out and getting unexpectedly closed.

IndexedDB processes are also left unthrottled in order to avoid timeouts.

Sweet, so this one is easy, all we need to do is to play audio, create websocket or webrtc connection, or open indexedDB:

function createWebSocket () { var ws = new WebSocket('wss://echo.websocket.org'); ws.onmessage = function () { ws.send('message'); }; }

This websocket server is just an example one that will replay the exact message you send him, so it will just keep the connection open and prevent the throttling of the viewability measurement callback.

IntersectionObserver

IntersectionObserver callbacks are firing with array of IntersectionObserverEntry values as argument. Vendors often relay on the the intersectionRatio and the isIntersecting properties in order to determine viewability, so we will make both return the proper values regardless:

function spoofIntersection () { Object.defineProperties(IntersectionObserverEntry.prototype, { 'intersectionRatio': { get: function () { return 1.0; } }, 'isIntersecting': { get: function () { return true; } } }); }

Now intersetionRatio is always 1 and isIntersecting is always true. after running this code inside MDN threshold example, you can see all boxes report 100% interstion ratio, although it’s clearly not the case:

document.hasFocus

Easy, just make it to always return true:

function spoofFocus () { Object.defineProperty(document, 'hasFocus', { get: function () { return function () { return true; } } }); }

Page Visibility API

Make document.visibilityState to always return “visible”:

function spoofVisibilityState () { Object.defineProperty(document, 'visibiltyState', { get: function () { return 'visible'; } }); }

Make document.hidden to always return false:

function spoofHidden () { Object.defineProperty(document, 'hidden', { get: function () { return false; } }); }

mozPaintCount

Increment with random value each time a script reads its value:

function spoofMozPaintCount () { var paintCount; Object.defineProperty(window, 'mozPaintCount', { get: function () { return paintCount += Math.round(Math.random()*50); } }); }

document.elementFromPoint

Make it return a random element regardless of the x,y arguments, but return null when negative x,y are passed in order to appear sane:

function spoofElementFromPoint () { Object.defineProperty(document, 'elementFromPoint', { get: function () { return function (x, y) { if (x >= 0 && y >= 0) { return document.all[Math.floor(Math.random() * document.all.length)]; } return null; } } }); }

And that’s it! Wer’e always viewable now. In the next post I’ll show you how to spoof viewability for SafeFrame, which too many people accidentally consider as a magic solution to all types of problems.