АйТи Фреш
Главная / Статьи / Безопасность
Безопасность

Как шаблон mailcow открывал доступ ко всей переписке

Автор: Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш» · · ~18 мин чтения
Как шаблон mailcow открывал доступ ко всей переписке
Иллюстрация к статье «Как шаблон mailcow открывал доступ ко всей переписке».

Администратор меняет безобидный HTML-шаблон уведомления — и получает выполнение команд внутри почтового контейнера. Именно так работает CVE-2025-53909. Я, Семёнов Евгений Сергеевич, обычно начинаю проверку не с паники и не с поиска готового эксплойта, а с трёх вопросов: какая версия действительно запущена, кто мог войти в административную панель и что доступно пользователю vmail. Ниже покажу, почему контейнер не спасает переписку, как провести безопасный аудит без воспроизведения атаки и в какой последовательности обновлять mailcow.

Короткий ответ: да, переписка была под угрозой

CVE-2025-53909 — серверная инъекция в шаблон, или SSTI, в уведомлениях mailcow о квоте и карантине. До исправления введённый администратором Jinja2-шаблон обрабатывался как доверенный код. Уведомление формировалось не в браузере и не в изолированном предпросмотре панели, а внутри Dovecot-контейнера. Исследователи подтвердили выполнение команд от пользователя vmail. Исправленный релиз 2025-07 вышел 15 июля 2025 года, advisory GHSA-8p7g-6cjj-wr9m и запись CVE опубликованы 17 июля 2025 года. Классификация — CWE-1336 (неэкранированные спецсимволы в шаблонизаторе), оценка CVSS 3.1 — 9,1 (Critical), вектор CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H. Высокий балл при PR:H объясняется S:C: код выходит из контура веб-панели в почтовый контейнер.

Для эксплуатации нужен административный доступ к mailcow UI. Это важное ограничение: письмо от внешнего отправителя само по себе команду не выполнит, обычный пользователь шаблон не изменит, а случайный сканер интернета не получает RCE только потому, что видит страницу входа. Поэтому я не называю CVE «дырой без авторизации». Но и успокаивать себя фразой «злоумышленник уже должен быть администратором» нельзя. Украденный пароль от панели раньше означал управление доменами и ящиками, а здесь превращался в произвольный код в контексте почтового хранилища.

Пользователь vmail — не root хоста. Контейнер Dovecot не становится автоматически привилегированным, а переход на хост потребовал бы другой ошибки или опасной конфигурации Docker. Это хорошая новость. Плохая — в контейнер подключён том /var/vmail, где находятся ящики пользователей. Контекст vmail создан именно для работы с почтой. Поэтому чтение, изменение и удаление сообщений, воздействие на индексы и Sieve-фильтры — реалистичное последствие. Под «всей перепиской» я понимаю данные всех локальных ящиков этой установки, а не внешние архивы и не почту в других системах.

Контейнер ограничивает ущерб для хоста, но не защищает данные, ради которых запущен контейнер. Для почтового администратора это инцидент уровня всей mailcow-инсталляции.
Цифры и версии: Короткий ответ: да, переписка была под угрозой — схема
Цифры и версии: Короткий ответ: да, переписка была под угрозой. Открыть схему в полном размере

Где проходит сломанная граница доверия

Обычно рассуждают так: PHP-панель — контур управления, Dovecot — контур данных, Docker разделяет процессы. На схеме всё выглядит аккуратно. Но шаблон связывает эти контуры напрямую. Администратор сохраняет HTML с выражениями Jinja2, значение попадает в Redis, а quota_notify.py или quarantine_notify.py забирает его и рендерит внутри Dovecot-контейнера. В уязвимой реализации использовался обычный jinja2.Template. Это не подстановка пары заранее разрешённых маркеров, а полноценный механизм шаблонизации с доступом к объектной модели Python.

Триггер тоже удобен для атакующего. Квотное уведомление штатно появляется при пересечении порогов заполнения — в dovecot.conf mailcow заданы пороги 80 и 95 процентов (quota_warning = storage=95%%, quota_warning2 = storage=80%%). Карантинные уведомления запускаются согласно настройкам карантина. Получается отложенное выполнение: шаблон можно сохранить сейчас, а обработает его системный процесс позже. Если администратор ищет только интерактивную командную строку или подозрительный контейнер, он легко пропустит такую цепочку.

Исправление небольшое, но принципиальное. В обоих скриптах разработчики заменили создание обычного шаблона на jinja2.sandbox.SandboxedEnvironment().from_string(...) и добавили обработку SecurityError и TemplateError. Для релиза 2025-07 также обновили образ Dovecot до ghcr.io/mailcow/dovecot:2.34, для legacy — до 2.34-legacy. Песочница блокирует опасный доступ к Python-объектам, сохраняя нормальные переменные: для квоты это username и percent, для карантина — username, counter, hostname, quarantine_acl и массив meta.

Здесь моя позиция однозначна: административный шаблон всё равно должен считаться недоверенным вводом. Фраза «его редактирует только администратор» — не защита. У администратора крадут пароль, сессию или API-ключ; общую учётную запись оставляют бывшему подрядчику; панель публикуют наружу без второго фактора. Я видел все эти варианты чаще, чем осознанную атаку на Jinja2.

Проверять только версию веб-панели недостаточно. После неудачного или частичного обновления репозиторий может быть новым, а работающий Dovecot-контейнер — старым.
Как шаблон mailcow открывал доступ ко всей переписке — схема
Схема к статье. Открыть схему в полном размере

Стенд ветеринарной аптеки «Добрый корм»: как выглядела проверка

Покажу на обезличенном проекте. Название «Добрый корм» условное. Это ветеринарная аптека с зоотоварами: один торговый зал, склад и бухгалтерия, 9 рабочих мест и 14 почтовых ящиков (сотрудники плюс общие адреса для заказов, поставщиков и ветврача-консультанта). На момент проверки mailcow работал на Ubuntu Server 22.04: 2 vCPU, 8 Гбайт RAM, системный SSD 160 Гбайт, в /var/vmail было 41 Гбайт и около 260 тысяч сообщений — в основном накладные, прайсы поставщиков и сертификаты на ветпрепараты. Репозиторий стоял на теге 2025-05. Административная панель была доступна через публичный reverse proxy, а приходящий администратор и директор пользовались одной учётной записью администратора без второго фактора.

Конфигурация была вполне обычной. Антивирус включён, полнотекстовый поиск отключён ради экономии памяти на 8 Гбайт, HTTP и HTTPS mailcow привязаны к loopback, наружу трафик отдавал reverse proxy. Сам по себе такой bind хорош, но доступ к /admin прокси никак не ограничивал.

MAILCOW_HOSTNAME=mail.dobryi-korm.example
SKIP_CLAMD=n
SKIP_FTS=y
HTTP_BIND=127.0.0.1
HTTPS_BIND=127.0.0.1

Именно здесь администратор обычно говорит: «Контейнер же непривилегированный, значит максимум сломают уведомление». Нет. У Dovecot был штатно подключён vmail-vol-1:/var/vmail, а процесс квотного уведомления запускался как vmail. Граница Docker защищала операционную систему, но не содержимое почтового тома.

Мы не вставляли эксплуатационный шаблон даже на копии продуктивных данных. Это лишнее: версия 2025-05 уже определяла уязвимое состояние, а код запущенного контейнера показывал использование обычного Template. Сначала сохранили дампы значений QW_HTML и Q_HTML, журналы и снимок виртуальной машины. В шаблоне квоты оказался фирменный HTML, сделанный подрядчиком; шаблон карантина был стандартным. Явной вредоносной конструкции не нашли. Однако журналы reverse proxy хранились 14 дней, поэтому честный вывод звучал так: признаков эксплуатации нет, но весь период уязвимости доказательно закрыть нельзя.

В согласованное окно сделали полный backup, обновили stable с 2025-05 до 2025-07, получили Dovecot 2.34 и проверили наличие SandboxedEnvironment в обоих работающих скриптах. Окно заняло 14 минут, доставка почты восстановилась без потерь, все 14 ящиков прошли IMAP-проверку. Затем мы разделили административные учётные записи (директору оставили только доменного администратора без глобальных настроек), включили второй фактор и разрешили доступ к панели управления только через VPN. Массово менять пароли всех пользователей не стали: подтверждения чтения ящиков не было. Пароль общего администратора и API-ключ сменили обязательно.

Отсутствие подозрительного текста в текущем шаблоне не доказывает отсутствие атаки: шаблон могли вернуть к нормальному виду после выполнения. Нужны журналы, история доступа и сохранённые артефакты.
Цифры и версии: Стенд ветеринарной аптеки «Добрый корм»: как выглядела проверка — схема
Цифры и версии: Стенд ветеринарной аптеки «Добрый корм»: как выглядела проверка. Открыть схему в полном размере

Что проверить прямо сейчас без эксплуатации

Начните с инвентаризации. Выполняйте команды из каталога установки mailcow. git describe показывает тег репозитория, git rev-parse — точный коммит, а список образов помогает увидеть частичное обновление. Метка dirty означает локальные изменения: перед обновлением их надо разобрать, а не бездумно перезаписывать.

cd /opt/mailcow-dockerized
git describe --tags --always --dirty
git rev-parse HEAD
git status --short
docker compose config --images | sort -u
docker compose ps

Если stable ниже 2025-07 — установка уязвима. Если используется legacy, проверьте наличие коммита 8c5f6c0 (git merge-base --is-ancestor 8c5f6c0 HEAD && echo patched) и образ dovecot:2.34-legacy, но на сентябрь 2026 года оставаться на legacy уже нельзя: официальная поддержка этой линии закончилась в феврале 2026 года. Я выбираю переход на актуальный stable, а не вечное удержание единственного backport-коммита.

Затем смотрите работающий контейнер. Это безопасная проверка кода, а не PoC. Обе команды должны найти импорт и создание SandboxedEnvironment в двух скриптах. Заодно видно, что сервис квотных уведомлений настроен на пользователя vmail.

docker compose exec -T dovecot-mailcow sh -lc 'grep -n "SandboxedEnvironment" /usr/local/bin/quota_notify.py /usr/local/bin/quarantine_notify.py'
docker compose exec -T dovecot-mailcow sh -lc 'grep -A8 "^service quota-warning" /etc/dovecot/dovecot.conf'
docker compose exec -T dovecot-mailcow id vmail

Если тег новый, а совпадений нет хотя бы в одном скрипте, я считаю узел неисправленным до выяснения причин. Возможны старый контейнер, закреплённый image, незавершённый docker compose up -d или локальная модификация.

До сброса пользовательских шаблонов сохраните их с правами только root. Переменная REDISPASS передаётся в контейнер redis-mailcow в актуальных docker-compose.yml; на очень старых установках без пароля Redis уберите из команды ключ -a. Значения QW_HTML и Q_HTML могут содержать чувствительный или вредоносный текст, поэтому не открывайте их в браузере и не пересылайте в общий чат. Снимите хеши и соберите доступные журналы. Старые записи могли уже ротироваться — это ограничение надо зафиксировать в отчёте.

cd /opt/mailcow-dockerized
umask 077
docker compose exec -T redis-mailcow sh -lc 'redis-cli --no-auth-warning -a "$REDISPASS" GET QW_HTML' > /root/QW_HTML.txt
docker compose exec -T redis-mailcow sh -lc 'redis-cli --no-auth-warning -a "$REDISPASS" GET Q_HTML' > /root/Q_HTML.txt
sha256sum /root/QW_HTML.txt /root/Q_HTML.txt
docker compose logs --no-color --since 2025-07-01T00:00:00 dovecot-mailcow nginx-mailcow php-fpm-mailcow > /root/mailcow-cve-2025-53909.log

Проверьте входы администраторов, обращения к настройкам квоты и карантина, неожиданные ошибки шаблонизатора, запуски уведомлений и исходящие соединения контейнера. Официальный advisory готовых индикаторов компрометации не публикует, поэтому признаки я собираю сам. Веб-интерфейс сохраняет шаблоны через внутренний API: в версии 2025-07 это POST на /api/v1/edit/quota_notification (шаблон квоты, ключ QW_HTML) и /api/v1/edit/quarantine (глобальные настройки карантина, ключ Q_HTML). Маршруты в старых релизах могут отличаться, поэтому сверяйте их с data/web/json_api.php своей версии.

Дальше — поиск по собранному журналу и истории API. Сами по себе совпадения не доказывают атаку: администратор мог честно менять фирменный шаблон. Подозрительным я считаю изменение шаблона вне согласованных работ, с незнакомого IP или в нерабочее время, а также появление в сохранённом HTML конструкций вида __class__, __globals__, __builtins__, import, popen, os. — в легитимном шаблоне уведомления им взяться неоткуда. В журнале Dovecot смотрите запуски quota_notify.py/quarantine_notify.py с трассировками Python и неожиданные процессы sh, curl, wget от vmail.

cd /opt/mailcow-dockerized
grep -nE 'POST /api/v1/edit/(quota_notification|quarantine) ' /root/mailcow-cve-2025-53909.log
grep -nE 'quota_notify|quarantine_notify|Traceback|jinja2' /root/mailcow-cve-2025-53909.log
grep -nE '__class__|__globals__|__builtins__|__subclasses__|popen|import' /root/QW_HTML.txt /root/Q_HTML.txt
docker compose exec -T redis-mailcow sh -lc 'redis-cli --no-auth-warning -a "$REDISPASS" LRANGE API_LOG 0 -1' > /root/mailcow-api-log.json
docker compose exec -T dovecot-mailcow ps -o user,pid,ppid,args | grep -E '^vmail' 

Помните про ротацию: список API_LOG в Redis ограничен по длине, а docker compose logs хранит только то, что не вытеснено драйвером журналов. Если журналов за весь период с установки до обновления нет, в отчёте прямо пишем «исключить эксплуатацию невозможно».

Не проверяйте продуктивную систему готовой SSTI-нагрузкой. Для установления уязвимости достаточно версии и кода контейнера; попытка эксплуатации меняет доказательства и может повредить почтовые данные.

Обновление и действия при признаках компрометации

Если следов атаки нет, порядок простой: свежая резервная копия, проверка доступного обновления, переход на актуальный stable и функциональный тест. Релиз 2025-07 — минимальная исправленная граница, а не рекомендуемая версия для новой установки в 2026 году. Официальный update.sh обновляет репозиторий и образы; --check только сообщает о наличии обновления (код выхода 0 — обновление есть, 3 — нет), а ключ --stable нужен лишь для перехода с legacy или nightly на stable-ветку; ручная замена одной строки image оставляет остальные компоненты в несогласованном состоянии.

cd /opt/mailcow-dockerized
sudo install -d -m 700 /srv/backup/mailcow
sudo MAILCOW_BACKUP_LOCATION=/srv/backup/mailcow ./helper-scripts/backup_and_restore.sh backup all
sudo ./update.sh --check
sudo ./update.sh
docker compose ps

Копию из /srv/backup/mailcow после проверки нужно вынести с почтового хоста. Backup на том же диске помогает откатить ошибку администратора, но почти бесполезен при потере узла или доступе атакующего к root.

После обновления снова выполните проверку двух скриптов, просмотрите состояние контейнеров и отправьте обычное тестовое письмо. Затем проверьте IMAP, SMTP submission, SOGo, квотное и карантинное уведомления на отдельном тестовом ящике. Шаблоны восстанавливайте из панели: пустое значение возвращает вариант по умолчанию. Фирменный HTML собирайте заново только из документированных переменных, а не копируйте непроверенный старый шаблон целиком.

При признаках компрометации порядок другой. Сначала ограничьте /admin на reverse proxy или VPN, не выключая без необходимости приём SMTP. Сохраните снимок VM, Redis, журналы и сведения о контейнерах. После этого обновляйте, меняйте пароли всех административных учётных записей, API-ключи и связанные секреты автоматизации, завершайте активные сессии. Если подтверждено чтение /var/vmail, изменение Sieve или удаление писем, я считаю скомпрометированными все локальные ящики и подключаю руководство: здесь уже нужны восстановление из независимой копии, оценка утечки и решение о смене пользовательских паролей.

Чего я не делаю — не начинаю с перестройки всего сервера. Подтверждённое выполнение было от vmail внутри непривилегированного контейнера; автоматического root на хосте advisory не обещает. Полная переустановка оправдана, если найдены выход из контейнера, изменение host-файлов, неизвестные контейнеры, Docker socket в доступном контексте или иные признаки закрепления. Без них сначала спасаем доказательства и почтовые данные.

Обновление закрывает уязвимость, но не отменяет уже выполненные команды. Если есть индикаторы атаки, одного успешного `update.sh` недостаточно.
Порядок действий: Обновление и действия при признаках компрометации — схема
Порядок действий: Обновление и действия при признаках компрометации. Открыть схему в полном размере

Что я меняю в эксплуатации mailcow после этой CVE

Первое — убираю административную панель из общего интернета. Пользовательский webmail может быть публичным, /admin — нет. Доступ через VPN или список доверенных адресов резко сокращает вероятность кражи административной сессии. Второе — персональные admin-учётные записи и 2FA. Общий пароль удобен ровно до первого расследования: потом невозможно понять, кто и когда менял конфигурацию.

Третье — обновления по расписанию. Раз в месяц мы проверяем release notes и выполняем ./update.sh --check; security advisory разбираем вне очереди. Старые поддерживаемые ветки допустимы только до даты окончания поддержки. В 2026 году legacy, основанный на mailcow 2025-02, уже не вариант, даже если в него когда-то применили 8c5f6c0. Исправление одной CVE не компенсирует отсутствие следующих security updates.

Четвёртое — независимые журналы и резервные копии. Логи reverse proxy и административных входов отправляем на другой узел минимум на 90 дней, а резервную копию /var/vmail, Redis, MySQL и ключей шифрования храним вне mailcow-хоста. Отдельно контролируем изменение шаблонов квоты и карантина. В самой панели удобного неизменяемого аудита для такого расследования может не хватить, поэтому эту функцию я выношу на уровень proxy и SIEM.

И последнее. На что можно забить? На попытку доказать безопасность красивой Docker-схемой. Она ничего не говорит о правах процесса к подключённым томам. Не надо также срочно менять пароли всех 14 ящиков аптеки только по номеру CVE: сначала установите уязвимую версию, доступность панели и признаки эксплуатации. А вот обновление, закрытие /admin, 2FA и сохранение нормальных логов откладывать нельзя.

Главный вывод CVE-2025-53909: права администратора UI нельзя считать безопасно отделёнными от данных контейнера, если управляемая настройка затем интерпретируется как код.

Частые вопросы

Опасна ли CVE-2025-53909, если панель администратора не опубликована в интернете?

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

Достаточно ли увидеть тег 2025-07 или новее?

Нет. Проверьте запущенный Dovecot-контейнер и наличие `SandboxedEnvironment` в обоих скриптах. Новый Git checkout рядом со старым работающим контейнером ничего не исправляет.

Получает ли атакующий root на Docker-хосте?

Не автоматически. Подтверждено выполнение от vmail внутри контейнера. Для root или выхода на хост нужна дополнительная уязвимость либо опасная конфигурация. Но почтовый том уже находится внутри доступной зоны.

Нужно ли менять пароли всех почтовых пользователей?

Не только из-за наличия уязвимой версии. Сначала оцените доступ к UI и индикаторы эксплуатации. При подтверждённом чтении почты, изменении фильтров или краже секретов массовая ротация становится частью реагирования.

Защищает ли шифрование почты на диске?

Оно полезно против кражи выключенного диска, но не заменяет патч. Код исполняется в рабочем почтовом контуре, где сервис должен обрабатывать сообщения и связанные ключи. Рассматривать mail crypt как компенсацию этой SSTI нельзя.

Можно ли просто очистить оба шаблона?

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

Как по логам понять, что CVE-2025-53909 пытались использовать?

Готовых IoC в advisory нет. Ищите POST на `/api/v1/edit/quota_notification` и `/api/v1/edit/quarantine` вне согласованных работ, записи в `API_LOG`, конструкции `__class__`, `__globals__`, `popen` в сохранённых `QW_HTML`/`Q_HTML`, трассировки Python от `quota_notify.py` в журнале Dovecot и неожиданные процессы от vmail. Отсутствие находок при коротком сроке хранения журналов не доказывает отсутствие атаки.

Столкнулись с похожей задачей? Обращайтесь — решим

Если у вас происходит что-то из описанного в этой статье — или любая другая проблема с ИТ-инфраструктурой, — обращайтесь в любое время. Мои специалисты и я лично разберём ситуацию, найдём настоящую причину и доведём до решения.

Возьмёмся и за разовую задачу, и за постоянное обслуживание. Первичная консультация — бесплатно и без обязательств.

📞 +7 903 729-62-41 💬 MAX: +7 903 729-62-41 ✈ Telegram @ITfresh_Boss

С уважением, Семёнов Евгений Сергеевич, директор «АйТи Фреш» — IT-аутсорсинг для компаний до 50 рабочих мест, 15+ лет практики

Источники

© ООО «АйТи-Фреш» · Москва · Все статьи