Меню
RU

Как нам удалось создать безопасный и функциональный HTML email-рендерер

Как сейчас везде плохо и почему

В мире электронной почты качество отображения HTML-писем оставляет желать лучшего. Основные проблемы связаны с различиями в поддержке HTML и CSS между почтовыми клиентами и веб-интерфейсами. Часто письма, идеально выглядящие в одном почтовом сервисе, могут отображаться совершенно некорректно в другом. Пользователи разочаровываются, конверсия снижается, а имидж бренда ухудшается.

Во многом это обусловлено тем, что в мире есть всего несколько сервисов-монополий, у которых есть веб-интерфейс отображения писем, и всего несколько популярных почтовых клиентов. Gmail или Yahoo ничего не меняли в своих движках отображения годами, несмотря на то, что HTML и CSS за эти годы значительно продвинулись вперёд по возможностям отображения контента. К примеру, адаптивность появилась с внедрением медиа-запросов около 10–12 лет назад, а поддержка тёмной темы в CSS — около 4–5 лет назад, но до сих пор ни один крупный почтовый сервис не поддерживает эти функции. Ему просто незачем что-то менять — это веб-мастера должны приспосабливаться к весьма причудливым и нигде не описанным ограничениям сервиса. К примеру, кто-то поддерживает анимации, кто-то — нет. Gmail не поддерживает даже свой (изобретённый и популяризированный компанией Google) формат изображений webp, iCloud поддерживает, а Yandex — нет. И таких несовместимостей буквально сотни. Веб-мастера вынуждены словно окунуться в атмосферу раннего интернета, когда шла война браузеров и приходилось идти на страшные ухищрения, чтобы добиться схожего отображения веб-страниц в разных браузерах.

Да, конечно, зачастую те или иные ограничения (к примеру, отсутствие скриптов) сделаны исходя из соображений безопасности (вряд ли бы мы хотели получить письмо с интерактивным баннером, который сливает наши данные рекламной сети, верно?), но зачастую набор ограничений выглядит действительно безумно — ну чем Gmail помешали простые фоновые изображения, задаваемые с помощью background-image?

Что мы придумали

Когда мы делали первые пробы пера на рынке электронной почты с Временной почтой AdGuard, мы потратили много времени, изучая возможности по отображению писем. Мы определённо не хотели превращаться во второй Gmail, изобретая собственное подмножество HTML/CSS, урезав все компоненты, которые нам показались лишними. Но и оставлять пользователей один на один с веб-угрозами мы не собирались.

Часть 1. 7 строк JavaScript-кода, что решили проблему

К счастью, мир веб-разработки не стоит на месте и разработчики браузеров давно работают над тем, чем мы в итоге и воспользовались — над браузерными песочницами.

Что это такое? Это набор техник ограничения возможностей страницы — буквально список возможностей, очень похожий на CSP (впрочем, его мы тоже используем, об этом чуть позже): «не используй cookie родительской страницы», «не исполняй JavaScript», «не передавай HTTP Referrer, когда кто-то переходит по ссылке из фрейма» и ряд подобных директив. Звучит интересно, не так ли? Давайте рассмотрим технические детали реализации.

Из API мы получаем тело письма как строку, в которой находится HTML-разметка, нам нужно эту разметку отобразить в браузере, используя возможности песочницы:

const html = '<html><body>....</body></html>';

const iframe = document.createElement('iframe');

iframe.credentialless = true;
iframe.sandbox = 'allow-popups allow-popups-to-escape-sandbox';
iframe.referrerpolicy = 'no-referrer';

iframe.srcdoc = html;

document.body.appendChild(iframe);

Стоит теперь добавить немного стилей, спозиционировать фрейм на родительской странице — и вы прекрасны, вы только что сделали довольно безопасный движок отображения HTML-писем без дурацких ограничений!

В идеальном мире, пожалуй, на этом можно было бы и остановиться, ведь основную опасность — JavaScript — мы отключили, но, к сожалению, современные письма — это просто гора всевозможных трекеров, которые пытаются собрать как можно больше данных о вас. Это zero-pixels — невидимые изображения, которые передают на сервер ваш IP-адрес и данные о вашем девайсе, и специальные отслеживающие ссылки, которые позволяют сохранить каждое ваше действие с письмом. И мы должны были с этим что-то сделать.

Часть 2. Анонимизация пользователей

В процессе разработки мы наткнулись на великолепный инструмент оценки безопасности нашего движка — emailprivacytester.com.

Этот сервис генерирует HTML-письмо, в котором есть ряд специально сформированных элементов, что тестируют те или иные уязвимости. Запускаем диагностику, отправляем себе письмо на ящик AdGuard TempMail и… получаем множество утечек IP-адреса.

Так как с JavaScript и ему подобными вещами мы разобрались, давайте сконцентрируемся на утечках IP-адреса. Такая утечка — это когда отправитель письма может узнать ваш адрес, просто отправив вам письмо. Конечно, если вы пользуетесь AdGuard VPN, то он узнает адрес VPN-сервера, но стоит открыть один раз письмо без VPN — и злоумышленник сможет узнать ваше местонахождение с точностью до города. Разумеется, мы не могли позволить такому случиться с нашими пользователями. Что делать?

Image proxy

На самом деле, всё уже было придумано до нас — и все «большие» почтовики давно это делают: они оборачивают все картинки в свою прокси.

Как? Например, если в вашем письме было вот такое изображение:

<img src="http://my.website/image/cat.jpg" />

то при каждом открытии письма владелец сервера my.website будет получать HTTP-запрос. В его метаданных будет находиться ваш IP-адрес, а также немного сведений о вашем устройстве (операционная система, версия браузера и некоторые другие данные, позволяющие вас деанонимизировать).

Как работает прокси: он подменяет все src изображений на свой URL, делая что-то вроде этого:

<img src="https://email.provider/image?url=http://my.website/image/cat.jpg" />

Веб-сервер email.provider устроен так, что скачивает исходную картинку он лишь однажды, после чего сохраняет её к себе, и при повторном запросе владелец сервера my.website запрос уже не получает. И даже при первом запросе my.website получает запрос от имени email.provider — с IP-адресом его сервера и без каких-либо данных, указывающих на вас. Всё отлично, правда, за исключением того, что эти данные о визитах теперь собирает сам email.provider (а мы помним, что, как правило, это большая корпорация — Google, Yandex или Yahoo).

В общем, мы сделали аналогичное решение, за исключением того, что наша image proxy, как и другие продукты AdGuard, не собирает и не хранит данные пользователей, а полностью анонимна.

Так как прокси готов, нужно как-то заменить все изображения в письме на «обёрнутые» в прокси, для этого нужно разобрать наш исходный HTML уже как DOM-документ. Работать с DOM в песочнице мы не можем — в песочнице нет JavaScript, и мы не можем его там выполнить даже для нашего кода, так как это автоматически разрешит выполнение JavaScript-кода, пришедшего в письме. А если мы работаем с DOM вне контекста песочницы, то хотелось бы получить чуть больше гарантий того, что наш HTML чист, после чего его можно было бы обрабатывать.

К счастью, мы нашли потрясающую библиотеку DOMPurify — она умеет чистить и приводить в безопасный и структурированный вид HTML-контент. Она отлично протестирована и по достоинству очень высоко оценена экспертами по безопасности. Начнём её использовать:

const html = '<html><body>....</body></html>';
const clean = DOMPurify.sanitize(html, {
    // восстановить частичный HTML до полноценного документа, если это необходимо
    WHOLE_DOCUMENT: true,
    // эти теги не имеют смысла в контексте HTML-письма, поэтому мы их удалим
    FORBID_TAGS: ['audio', 'video', 'button', 'input', 'form'],
});

const iframe = document.createElement('iframe');

// набор действий с песочницей из первого шага

iframe.srcdoc = clean;

document.body.appendChild(iframe);

Отлично, мы избавились от мусора, теперь можно вернуться к обработке изображений. DOMPurify поможет нам и в этом! У неё есть отличный механизм hooks — мы сможем решить вторую задачу, не меняя контекст:

DOMPurify.addHook('afterSanitizeAttributes', (node) => {
    if (node.tagName === 'IMG' && node.hasAttribute('src')) {
        // заменить оригинальный src ссылкой на ImageProxy
        const src = wrapWithImageProxy(node.getAttribute('src');
        node.setAttribute('src', src);
    }
});

Запускаем диагностику и видим, что количество IP-утечек действительно сократилось, но не до нуля. Как же так? Пришло время поговорить о CSS.

Утечки IP-адресов через CSS

CSS обладает поистине потрясающими возможностями по стилизации контента. Ни одна другая система к нему даже не приблизилась — лёгкость, простота и множество возможностей делают CSS одним из лучших изобретений программной инженерии. Но, как это часто водится с очень мощными инструментами, часть их возможностей можно использовать во вред. В нашей задаче «вредной» оказалась функция url(), которая позволяет загружать контент по ссылке и использовать его для оформления страницы. Самый часто встречающийся пример — фоновые изображения. Код их загрузки выглядит примерно так:

background-image: url(http://my.website/image/cat.jpg);

Проблемы здесь ровно те же, что и в прошлом разделе — утечка IP-адреса. Помимо фоновых изображений, функция url() может быть использована в следующих правилах — их довольно много, и синтаксис CSS-селектора позволяет задавать достаточно сложные правила вроде селектора из примера по ссылке выше:

background-image: cross-fade(20% url(http://my.website/image/first.png), url(http://my.website/image/second.png));

Подобное правило не так просто безопасно распарсить — для этого нужны подходящие инструменты. И нам вновь повезло его найти! csstree делает ровно то, что нам нужно — позволяет анализировать CSS и заменять некоторые типы его конструкций. В виде кода это выглядит так:

csstree.walk(ast, (node) => {
    if (node.type === 'Url') {
        node.value = wrapWithImageProxy(node.value);
    }
});

что позволило получить на выходе

background-image: cross-fade(20% url(https://img.agrd.eu/image?url=http://my.website/image/first.png), url(https://img.agrd.eu/image?url=http://my.website/image/second.png));

И вуаля, больше нет утечек IP-адресов!

CSP

Приватность данных — это такая область, где не может быть исключений. Никаких, ни единого. Поэтому нужно что-то придумать на тот случай, если обнаружится уязвимость в библиотеках или в нашем коде, который неправильно интерпретировал данные. Защиту последнего рубежа — когда все другие средства отказали. В нашем случае в этом качестве используется великий и ужасный CSP — набор директив, позволяющий гранулярно управлять доступом страницы к тем или иным ресурсам. Часто веб-мастера страдают из-за него, забывая, что какой-то новый ресурс, который будет нужен странице, в CSP не разрешён (да чего греха таить, мы и сами так ошибались несколько раз). Сайт ломается со включённым CSP, но нам, кажется, удалось его укротить. Получилось что-то вроде того:

# Разрешим изображения только с нашей image proxy
img-src data: https://img.agrd.eu/image;
# и запретим все остальные возможности куда-либо подключиться
script-src 'none';
style-src 'none';
font-src 'none';
connect-src 'none';
media-src 'none';
object-src 'none';
prefetch-src 'none';
child-src 'none';
frame-src 'none';
worker-src 'none';
frame-ancestors 'none';
...

# что можно заменить на
default-src 'none';

Теперь мы в полной безопасности: даже если сломается одна из библиотек или наш код в функции преобразования адресов изображений, CSP не позволит браузеру совершить запрос к серверу злоумышленника и тем самым передать ваш IP-адрес.

Итоги работы с конфиденциальностью данных

Благодаря возможностям современных браузеров и нескольким отличным библиотекам мы получили безопасное и качественное отображение HTML-писем без единой утечки IP-адресов, без JavaScript или какого-то иного потенциально опасного контента — всё находится полностью под нашим контролем.

Почему это здорово

Мы немного углубились в тему безопасности, сместив акцент с наиболее важного факта: нам удалось сделать на 100% HTML5/CSS3-совместимые HTML-письма, что не удавалось никому в мире! А значит, вам могут быть доступны все возможности современных HTML-страниц — и анимации, и адаптивность, и современные форматы изображений, и тёмные темы в том виде, который задумал автор письма (вам ведь тоже не нравится уродливая смена цветов с помощью примитивных алгоритмов?). Больше не нужно верстать сложные сетки таблицами как в 90-х, а свободно сверстать прекрасное адаптивное письмо для мобильных устройств — можно.

Очень странно, что в середине 2020 годов где-то в интернете может наблюдаться настолько дикое поведение, как отображение вашей почты почтовыми гигантами со всеми присущими им недостатками и уязвимостями.

Мы очень рады, что нам удалось создать безопасную, современную альтернативу, которая отвечает всем требованиям, стоящим перед современным почтовым клиентом.

Если что-то пошло не так

Мы гордимся тем, чего нам уже удалось достичь с Временной почтой AdGuard, но мы также понимаем, что всё ещё находимся в начале пути. Создавая новый продукт, почти невозможно полностью избежать ошибок и недочётов. Если вы обнаружили уязвимость, связанную с отображением писем, пожалуйста, напишите нам на security@adguard.com. Мы устраним уязвимость в максимально короткий срок, а вы сможете претендовать на вознаграждение в рамках нашей программы Bug Bounty.

Наша команда разработчиков очень серьёзно подходит к обратной связи от пользователей. Поэтому, если вы заметите, что ваше HTML-письмо отображается неверно, также отправьте нам сообщение на почту support@adguard.com, и мы обязательно исправим проблему.

Понравился пост?
25 693 25693 отзыва
Отлично!

AdGuard для Windows

AdGuard для Windows — это не просто «ещё один блокировщик». Это многоцелевой инструмент, который блокирует рекламу и доступ к опасным сайтам, ускоряет загрузку страниц и защищает детей от взрослого контента.
Скачивая программу, вы принимаете условия Лицензионного соглашения
Узнать больше
25 693 25693 отзыва
Отлично!

AdGuard для Mac

В отличие от других блокировщиков, AdGuard разработан с учётом специфики операционной системы macOS. Он не только блокирует рекламу в Safari и других браузерах, но и защищает вас от слежки, фишинга и мошенничества в сети.
Скачивая программу, вы принимаете условия Лицензионного соглашения
Узнать больше
25 693 25693 отзыва
Отлично!

AdGuard для Android

AdGuard для Android — это идеальное решение для Android-устройств. В отличие от других блокировщиков, AdGuard не требует root-доступа и позволяет управлять трафиком любых приложений на вашем устройстве.
Скачивая программу, вы принимаете условия Лицензионного соглашения
Узнать больше
25 693 25693 отзыва
Отлично!

AdGuard для iOS

Лучший блокировщик рекламы для iPhone и iPad. AdGuard устраняет рекламу в Safari, защищает ваши данные и ускоряет загрузку страниц. AdGuard для iOS использует новейшую технологию блокировки, которая обеспечивает непревзойденное качество фильтрации и позволяет применять множество различных фильтров одновременно
Скачивая программу, вы принимаете условия Лицензионного соглашения
Узнать больше
25 693 25693 отзыва
Отлично!

AdGuard Content Blocker

AdGuard Content Blocker устраняет все объявления в мобильных браузерах, которые поддерживают технологию блокировки контента — к примеру, Samsung Internet и Яндекс.Браузер. Он обладает меньшим количеством функций, чем AdGuard для Android, но при этом бесплатен, прост в установке и по-прежнему обеспечивает высокое качество блокировки рекламы.
Скачивая программу, вы принимаете условия Лицензионного соглашения
Узнать больше
25 693 25693 отзыва
Отлично!

Браузерное расширение AdGuard

AdGuard — самое быстрое и легкое браузерное расширение для блокировки всех типов рекламы! Выбирайте AdGuard для быстрого и безопасного серфинга без рекламы.
25 693 25693 отзыва
Отлично!

Помощник AdGuard

Дополнительное браузерное расширение для десктопных приложений AdGuard. Даёт доступ к таким функциям в браузере, как блокировка отдельных элементов, занесение сайта в белый список или отправление отчёта.
25 693 25693 отзыва
Отлично!

AdGuard DNS

AdGuard DNS – это альтернативный способ заблокировать рекламу, защитить личные данные и оградить детей от взрослых материалов. Он прост в настройке и использовании и обеспечивает необходимый минимум защиты от рекламы, трекинга и фишинга, независимо от платформы.
25 693 25693 отзыва
Отлично!

AdGuard Home

AdGuard Home — мощный сетевой инструмент против рекламы и трекинга. С усилением роли интернета вещей становится все более и более важным управлять всей вашей сетью. После настройки AdGuard Home будет охватывать ВСЕ ваши домашние устройства и для этого вам не понадобится программное обеспечение на стороне клиента.
25 693 25693 отзыва
Отлично!

AdGuard Pro для iOS

AdGuard Pro предлагает гораздо больше чем просто блокировку рекламы в Safari, которая есть в обычной версии. С помощью специальных настроек DNS вы сможете блокировать больше рекламы, защитить ваши личные данные и оградить детей от взрослого контента.
Скачивая программу, вы принимаете условия Лицензионного соглашения
Узнать больше
25 693 25693 отзыва
Отлично!

AdGuard для Safari

Расширения, блокирующие рекламу в Safari, переживают не лучшие времена с тех пор, как компания Apple вынудила всех использовать новый SDK. Познакомьтесь с нашим легко настраиваемым и молниеносным приложением!
25 693 25693 отзыва
Отлично!

AdGuard Temp Mail

Ваш временный почтовый ящик, чтобы на основную почту не приходил спам
25 693 25693 отзыва
Отлично!

AdGuard для Android TV

AdGuard для Android TV — единственное приложение, которое блокирует рекламу, защищает ваши данные и действует как фаервол для Smart TV. Получайте предупреждения о веб-угрозах, используйте безопасный DNS, а ваш трафик будет зашифрован. Смотрите любимые сериалы безопасно и без рекламы!
Загрузка AdGuard началась Стрелка указывает на файл: нажмите на него, и установка начнётся Выберите «Открыть», нажмите «OK» и дождитесь загрузки файла. В открывшемся окне перетащите значок AdGuard в папку «Приложения». Спасибо за выбор AdGuard! Выберите «Открыть», нажмите «OK» и дождитесь загрузки файла. В открывшемся окне нажмите «Установить». Спасибо за выбор AdGuard!
AdGuard есть и в мобильном варианте