Отследить и отменить специфический ajax-запрос

В последнее время назойливые вебмастеры отправляют разную метрику на сервер всё более изощренными методами. Недавно столкнулся с одним методом, который я назвал "background ajax".

Суть его состоит в следующем. На страницу скриптом помещается <style>, в котором на определенный <div> прописан background, содержащий URL, включающий в себя закодированную строку с данными. Через какое-то время в DOM страницы помещается <div>, включающий класс из селектора этого стиля. Браузер загружает картинку по этому URL, отправляя данные на сервер. Веб-сервер в ответ отправляет прозрачный пиксель.

Для того, чтобы было сложнее заблокировать этот процесс, каждая таблица стилей наводняется бесполезными но очень похожими на полезные стилями. С другой стороны DOM наводняется похожими друг на друга <div>.

Я накидал эмулятор этого процесса передачи основанным на псевдо-рандомных значениях и данных.

!(function(){

  // Период генерации стиля (мс)
  const styleGenPeriod = 1000;
  // Период генерации блока (мс)
  const divGenPeriod = 100;
  // Максимальный период отложенного появления блока
  const maxDeferredPeriod = 15000;
  // Перевес вероятности (x:1)
  const chanceFactor = 5;
  // Длина генерируемых имён стиля
  const styleNameLen = 20;
  // Длина генерируемых url-ов фона для загрузки блоками
  const bgUrlQueryLen = 80;
  // Количество водяных стилей в каждой таблице
  const sheetWaterStyleLen = 99;
  // Пороговое количество таблиц стилей на странице
  const sheetMaxCount = 60;
  // Пороговое количество блоков на странице
  const divMaxCount = 600;
  // Алфавит для генератора псевдо-рандомных строк
  const randAbc = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  // Длина алфавита для генератора
  const abcSize = 62;
  // URL в качестве префикса к url картинок для фона
  const locPrefix = location.href + (-1 > location.href.indexOf('?') ? '&' : '?');


  // Корневой элемент - родитель всех ajax-блоков
  const rootEl = document.querySelector('.root');

  // Функция генерирует новый заполненый нолями массив заданной длины
  const newArr = len => new Array(len).fill(0);
  // Функция генерирует псевдо-рандомное целое число от 0 <= x < max
  const randInt = max => Math.trunc(Math.random() * max);
  // Функция генерирует псевдо-рандомную строку заданной длины
  const randStr = len => newArr(len).map(()=>randAbc.charAt(randInt(abcSize))).join('');
  // Функция генерирует псевдо-рандомную пару "ключ - значение"
  const randEntry = ()=>[randStr(styleNameLen),randStr(bgUrlQueryLen)];
  // Функция для сортировщика 
  const shuffle = () => Math.sign(Math.random - .5);
  // Функция возвращает стиль по шаблону
  const styleTpl = ([cls,url])=>`.${cls}{background:url("${locPrefix}${url}")}`;
  
  // Список элементов стилей на странице
  const styles = [];
  // Список блочных элементов на странице
  const objects = [];

  // интервал генерирующий стили
  setInterval(()=>{
    // Генерируем псевдо-рандомную секретную пару "ключ - значение"
    const [ objectCls, objectUrl ] = randEntry();
    // Генерируем стиль со списком псевдо-рандомных стилей
    const styleCode = 
      [ [ objectCls, objectUrl ],      // помещаем нашу секретную пару в один архив
        ... newArr(sheetWaterStyleLen) // вместе с данными "для воды".
              .map(randEntry) ]    
      .sort(shuffle).sort(shuffle)     // Перемешивая дважды,
      .map(styleTpl)                   // обернём данные в стили по шаблону,
      .join('');                       // объединим в один код CSS.
    // Создаём элемент стиля
    const styleEl = document.createElement('style');
    // Добавляем в стиль код
    styleEl.innerHTML = styleCode;
    // Добавим элемент стиля в головку страницы
    document.head.appendChild(styleEl);
    // Если количество стилей больше порога (60 стилей)
    if(styles.push(styleEl) > sheetMaxCount){
      // Удаляем один стиль из начала списка
      document.head.removeChild(styles.shift());
    }
    // С вероятностью 20% выполнить код
    if(!randInt(chanceFactor)){
      // Не просто код, а отложенный на псевдо-рандомный промежуток времени код
      setTimeout(() => {
        // Создаем элемент блока
        let objectEl = document.createElement('div');
        // Устанавливаем ему стиль для спец-блока
        objectEl.className = objectCls;
        // Устанавливаем хоть какой-то innerHTML внутрь блока
        objectEl.innerHTML = '&nbsp;';
        // Добавляем блоковый элемент на страницу
        rootEl.appendChild(objectEl);
        // Как раз в это время браузер будет пытаться подгрузить картинку фона из стиля
        // Если блоковых элементов на странице больше порогового значения (600 блоков)
        if(objects.push(objectEl) > divMaxCount){
          // удаляем один блок сверху списка
          rootEl.removeChild(objects.shift());
        }
      },randInt(maxDeferredPeriod));
    }
  },styleGenPeriod);

  // Интервал, генерирующий блоки
  setInterval(()=>{
    // Создаем элемент блока
    let objectEl = document.createElement('span');
    // Устанавливаем элементу псевдо-рандомный класс
    objectEl.className = randStr(styleNameLen);
    // Устанавливаем хоть какой-то innerHTML внутрь блока
    objectEl.innerHTML = '&nbsp;';
    // Добавляем блоковый элемент на страницу
    rootEl.appendChild(objectEl);
    // В это время не происходит никакой загрузки картинки фона для блока
    // так как эти блоки "для воды"
    // Если блоковых элементов на странице больше порогового значения (600 блоков)
    if(objects.push(objectEl) > divMaxCount){
      // удаляем один блок сверху списка
      rootEl.removeChild(objects.shift());
    }
  },divGenPeriod);

})();
 .root{
  position: fixed;
  bottom:0;
  right:0;
  width: 1px;
  overflow: hidden;
}
<div class="root"></div>

Выполнив этот скрипт, получившиеся ajax-запросы можно наблюдать на вкладке Network в консоли разработчика. Также можно видеть сгенерированные <style> и <div> на вкладке Elements после инспектирования, например, корневого элемента.

Моя задача состоит из нескольких уровней:

  1. Идентифицировать именно этот метод передачи метрики от необходимых для отображения страницы стилей.

  2. Отследить событие передачи данных.

  3. Отменить передачу данных, при этом не останавливая работу других скриптов на странице.

  4. Заменить данные в передаче, то есть отправить на сервер безопасные данные.

Я не прошу сделать работу за меня. Для меня это вообще не является обязательной для выполнения задачей. Но сама задача интересная. Рад буду любым идеям, даже не обязательно помогающим найти решение.


Ответы (1 шт):

Автор решения: Aziz Umarov

Думаю возможно накидать поверх XMLHttpRequest send декоратор, который будет оповещать, проверять адрес, .... при каждом send пример отсюда;

XMLHttpRequest.prototype.realSend = XMLHttpRequest.prototype.send;
// here "this" points to the XMLHttpRequest Object.
var newSend = function(vData) { console.log("data: " + vData); this.realSend(vData); };
XMLHttpRequest.prototype.send = newSend;

в вашем случае если это не ваш запрос то можно вместо данных отправить комбинацию из трех букв дабы скрипт в который заинжектили код не упал.

PS. Прошу не холиварить, а опубликовать ответ в случае существовании другого решения.

→ Ссылка