
В этой статье я расскажу, для чего нужна кросс-доменная инъекция 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
ссылка на оригинал статьи https://habr.com/ru/articles/573046/
Добавить комментарий