Меню
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 893 25893 отзыва
Отлично!

AdGuard для Windows

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

AdGuard для Mac

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

AdGuard для Android

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

AdGuard для iOS

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

AdGuard Content Blocker

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

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

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

Помощник AdGuard

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

AdGuard DNS

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

AdGuard Home

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

AdGuard Pro для iOS

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

AdGuard для Safari

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

AdGuard для Android TV

AdGuard для Android TV — единственное приложение, которое блокирует рекламу, защищает ваши данные и действует как фаервол для Smart TV. Получайте предупреждения о веб-угрозах, используйте безопасный DNS, а ваш трафик будет зашифрован. Смотрите любимые сериалы безопасно и без рекламы!
25 893 25893 отзыва
Отлично!

AdGuard Temp Mail

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

AdGuard Mail β

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