
В этой статье я расскажу, для чего нужна кросс-доменная инъекция Cookie, где это можно использовать, и главное, как это реализовать. Эта статья никак не связана с методами хищения чужих куки, только с внедрением пользователю своих для дальнейшего отслеживания.
Для чего нужна кросс-доменная инъекция Cookie?
Кросс-доменная инъекция позволяет установить Cookie для запросов от a.com до b.com
Где это можно использовать?
К примеру, мы хотим дать нашим пользователям динамическую картинку, которую они смогут разместить у себя на сайте. Это может быть баннер, который будет меняться в зависимости от геолокации пользователя или любая другая реализация, где необходимо отображать статический контент в соответствии с условиями. Также с помощью этого подхода мы можем проводить идентификацию пользователя и "вести" его от сайта к сайту, что и делают некоторые инструменты таргетированной рекламы. 
Некоторые люди злоупотребляют этими возможностями и используют во вред другим вставляя пиксели отслеживания.
Цель:
Отследить открытие моего email получателем.
Решение:
Вставить баннер или пиксель в email по которому я смогу отследить открытие.
Условие:
Нужно не учитывать открытие мной письма.
Ограничения:
Мы не можем получить доступ к cookie через javascript на стороне a.com, но они будут отправляться вместе со всеми запросами к b.com
Данный подход не будет работать при включенной функции "Блокировать сторонние файлы cookie" в браузере
Схема работы:

Далее будет представлена серверная часть в виде b.com и клиентская часть в виде a.com
Реализация b.com:
Для серверной части я использую express.js - быстрое и легкое решения для моих нужд.
Структура приложения:

Приступим непосредственно к самому коду серверной части:
Серверbin/www:
#!/usr/bin/env node
/**
 * Module dependencies.
 */
var app = require('../app');
var debug = require('debug')('tracked-pixel:server');
var http = require('http');
/**
 * Get port from environment and store in Express.
 */
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
 * Create HTTP server.
 */
var server = http.createServer(app);
/**
 * Listen on provided port, on all network interfaces.
 */
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
 * Normalize a port into a number, string, or false.
 */
function normalizePort(val) {
  var port = parseInt(val, 10);
  if (isNaN(port)) {
    // named pipe
    return val;
  }
  if (port >= 0) {
    // port number
    return port;
  }
  return false;
}
/**
 * Event listener for HTTP server "error" event.
 */
function onError(error) {
  if (error.syscall !== 'listen') {
    throw error;
  }
  var bind = typeof port === 'string'
    ? 'Pipe ' + port
    : 'Port ' + port;
  // handle specific listen errors with friendly messages
  switch (error.code) {
    case 'EACCES':
      console.error(bind + ' requires elevated privileges');
      process.exit(1);
      break;
    case 'EADDRINUSE':
      console.error(bind + ' is already in use');
      process.exit(1);
      break;
    default:
      throw error;
  }
}
/**
 * Event listener for HTTP server "listening" event.
 */
function onListening() {
  var addr = server.address();
  var bind = typeof addr === 'string'
    ? 'pipe ' + addr
    : 'port ' + addr.port;
  debug('Listening on ' + bind);
}
 app.js с подключением библиотек:
const express = require('express');
const cookieParser = require('cookie-parser');
const logger = require('morgan');
const indexRouter = require('./routes');
const app = express();
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use('/', indexRouter);
module.exports = app;
 routes.js, в котором мы делаем всю "магию".
В этом примере я использую base64 простого прозрачного пикселя 1х1.
const express = require('express');
const router = express.Router();
const whitePixel = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=';
//отдаю пиксель
const getPixelResponse = (res) => {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Content-type', 'image/png');
  res.end(Buffer.from(whitePixel, 'base64'));
}
//устанавливаю максимальное значение TTL
const getMaxAge = () => 60 * 60 * 24 * 1000 * 365;
//название моей куки
const cookieName = 'myCookie';
//первый запрос на идентификацию пользователя
router.get('/auth', function(req, res, next) {
  if (!req.cookies[cookieName]) {
    res.cookie(cookieName, 'test', { 
      maxAge: getMaxAge(), 
      httpOnly: false, 
      domain: 'localhost',
      secure: true, 
      sameSite: 'none'
    });
  }
  getPixelResponse(res);
});
//проверяем что мы действительно получаем нашу куку
router.get('/test', function(req, res, next) {
  console.log('Cookies', req.cookies)
  getPixelResponse(res);
});
module.exports = router;
Давайте разберем все тонкости реализации. Одна из самых главных частей, без которой ничего не будет работать представлена в строках
secure: true,
sameSite: 'none'
В документации сказано, что:
"Безопасные" (secure) cookie отсылаются на сервер только если запрос выполняется по протоколу SSL и HTTPS. Однако важные данные никогда не следует передавать или хранить в cookies, поскольку сам их механизм весьма уязвим в отношении безопасности, а флаг
secureникакого дополнительного шифрования или средств защиты не обеспечивает.
Из этого выходит, что необходимо поднять наш сервер с SSL, хотя при тесте в браузере Chrome я смог обратится к локальному серверу, но с предупреждением
Mixed Content: The page at 'https://example.com' was loaded over HTTPS, but requested an insecure element 'http://localhost:3000/auth'. This request was automatically upgraded to HTTPS, For more information see https://blog.chromium.org/2019/10/no-more-mixed-messages-about-https.htm
Также мы устанавливаем флаг sameSite: 'none'. 
Из документации видим:
Файлы cookie будут отправляться во всех контекстах, то есть в ответах как на собственные запросы
SameSite=None, так и на запросы из разных источников. Если он установлен,Secureатрибут cookie также должен быть установлен (иначе файл cookie будет заблокирован).
Полное описание по sameSIte смотрим здесь.
С сервером мы закончили, перейдем к клиенту.
Реализация a.com:
На клиенте нам необходимо вставить код отправки запросов.
Так как мы отдаем с сервера картинки, то и вставить мы можем их через HTML или Javascript.
HTML версия:
<img src="http://localhost:3000/auth" alt="auth"/>
<img src="http://localhost:3000/test" alt="test"/>
Javascript версия:
Так как я имею полный доступ к реализации клиентской части, то делаю это через javascript, что позволяет мне сначала дождаться загрузки auth запроса (получить нужные cookie) и только потом отправлять запрос на тест.
const preloadImage = function(url, callback)
{
    let img = new Image();
    img.src = url;
    img.onload = callback;
}
preloadImage('http://localhost:3000/auth', ()=>{
    document.body.innerHTML += '<img src="http://localhost:3000/test" alt="test" />';
})
package.json
{
  "name": "pixel",
  "author": "mldev",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "start": "node ./bin/www"
  },
  "dependencies": {
    "cookie-parser": "~1.4.4",
    "debug": "~2.6.9",
    "express": "~4.17.1",
    "morgan": "~1.9.1"
  }
}
Запускаем наш сервер
npm start 
Проверяем все ли работает. Тестировать запросы буду с сайта mail.google.com.


Итог
Мне удалось реализовать кросс-доменную инъекцию сookie, что позволяет в будущем добавить:
Получение id пользователя, который отправляет письмо и передавать его на авторизацию;
Проверку наличия cookie, чтобы фильтровать "тестовый" запрос и не учитывать собственные просмотры картинки;
Данный пример демонстративный и не является production кодом.
Код сервера на github
https://github.com/MykhailoDev/cookie-injection
Комментарии (7)

Kuch
15.08.2021 23:13Немного не понял о чём статья.
Мы ведь получается получили куки с нашего же сервера. Но какой в этом смысл?

svarogdev Автор
16.08.2021 09:55Добрый день, все верно, мы получаем куку с нашего сервера и это дает нам огромную возможность для манипуляции на чужом сервере. К примеру, можем показывать нужный нам контент в зависимость от значения куки. Мой сервер может установить id пользователя и отображать два банера в зависимости от четности id. Мы вставляем банер на a.com с ссылкой на b.com/baner.png и в зависимости от куки можем показать котика или собаку. Это простой пример

Kuch
16.08.2021 10:12Мы ведь сами выставили какое-то значение и потом его же прочитали? Мы точно так же можем отправить запрос на сервер и прочитать то, что передадим в body. Результат тот же. Зачем тут куки? Мы ведь не смогли прочитать куки другого сервера, только свои

svarogdev Автор
16.08.2021 10:38Мы вставляем изображение с ссылкой на b.com/baner.png, в body мы ничего не можем отправить

svarogdev Автор
16.08.2021 10:54+1Можем рассмотреть другой пример с этим подходом. Сделаем счетчик посещаемости сайта, который будем давать нашим клиентам для размещения на их сайтах. Мы даем клиенту картинку b.com/img.png, которою он разместить у себя. Когда к нему придет его пользователь, то откроется картинка со значением счетчика, будет установлена кука, что он уже посетил сайт и данные запишутся в базу. Когда пользователь посещает сайт во второй раз, то счетчик не считает его посещение уникальным т.к. у него уже установлена кука
          
 
csshacker
В кои-то веки что-то не по теме хелловорлдов на ноде. Спасибо.