Node.js кросс-доменная инъекция Cookie

Kate

Administrator
Команда форума
В этой статье я расскажу, для чего нужна кросс-доменная инъекция Cookie, где это можно использовать, и главное, как это реализовать. Эта статья никак не связана с методами хищения чужих куки, только с внедрением пользователю своих для дальнейшего это отслеживания.

Для чего нужна кросс-доменная инъекция Cookie?​

Кросс-доменная инъекция позволяет установить Cookie для запросов от a.com до b.com

Где это можно использовать?​

К примеру, мы хотим дать нашим пользователям динамическую картинку, которую те смогут разместить у себя на сайте. Это может быть баннер, который будет меняться в зависимости от геолокации пользователя или любая другая реализация, где необходимо отображать статический контент в соответствии с условиями. Также с помощью этого подхода мы можем проводить идентификацию пользователя и "вести" его от сайта к сайту, что и делают некоторые инструменты таргетированной рекламы.

Некоторые люди злоупотребляют этими возможностями и используют во вред другим вставляя пиксели отслеживания.

Цель:​

Отследить открытие моего email получателем.

Решение:​

Вставить баннер или пиксель в email по которому я смогу отследить открытие.

Условие:​

Нужно не учитывать открытие мной письма.

Ограничения:​

  • Мы не можем получить доступ к cookie через javascript на стороне a.com, но они будут отправляться вместе со всеми запросами к b.com
  • Данный подход не будет работать при включенной функции "Блокировать сторонние файлы cookie" в браузере

Схема работы:​

d5a721b6d30db3c0118a6d35208e7b43.png

Далее будет представлена серверная часть в виде b.com и клиентская часть в виде a.com

Реализация b.com:​

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

Структура приложения следующая:

ef7dbdf72e648a8b5d07d62ad7184135.png

Приступим непосредственно к самому коду серверной части:

Сервер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.

<img src="http://localhost:3000/auth" alt="auth"/>
<img src="http://localhost:3000/test" alt="test"/>
Так как я имею полный доступ к реализации клиентской части, то делаю это через 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" />';
})
В процессе написания скрипта обнаружил, что данный метод не работает с XMLHttpRequest, т.к. полученные сookie не будут отправлены браузером при тест запросе.

И так, запускаем наш сервер

npm start

Проверяем все ли работает. Тестировать запросы буду с сайта mail.google.com.

Получаем cookie
Получаем cookie
Cookie отправились
Cookie отправились

Итог​

Мне удалось реализовать кросс-доменную инъекцию сookie, что позволяет в дальнейшем добавить:

  • Получение id пользователя, который отправляет письмо и передавать его на авторизацию;
  • Проверку наличия cookie, чтобы фильтровать "тестовый" запрос и не учитывать собственные просмотры картинки;
Данный пример демонстративный и не является production кодом.

 
Сверху