Почему Vite за Nginx блокирует hostname и как настроить allowedHosts
После обновления Vite внутренний адрес перестал открываться, а `allowedHosts: true` мгновенно всё починил? Я оставляю точный список: сначала выясняю, какой Host передаёт Nginx, затем разрешаю нужное имя. Я — Семёнов Евгений Сергеевич, директор ООО «АйТи-Фреш». Покажу на лабораторном стенде, какие имена Compose действительно нужны, почему отключение проверки меняет защиту и как проверить результат.
Обновление включило проверку, которой раньше не было
Узнаваемая ситуация: контейнер запущен, Nginx отвечает, но вместо приложения приходит Blocked request. This host ("frontend") is not allowed. Первым делом я смотрю тело ответа и статус. Такой 403 означает, что запрос дошёл до проверки Vite. Ошибки DNS, отказ соединения и типичный 502 от прокси разбираются иначе. Пересобирать Docker-сеть и менять порты до этого простого разделения — верный способ добавить к одной неисправности ещё две.
Проверка Host появилась при исправлении GHSA-vg6x-rcgg-rjx6: первые исправленные версии — 6.0.9, 5.4.12 и 4.5.6, advisory опубликовано 20 января 2025 года. Поэтому даже обновление patch-версии могло сломать доступ через внутреннее имя. Это историческая граница изменения, а не рекомендация установить сегодня 6.0.9. В 2026 году нужно брать исправленный выпуск поддерживаемой ветки. Описание изменения и сценария reverse proxy.
Я начинаю с фактически работающего процесса: docker compose exec frontend npm ls vite, затем смотрю команду запуска в Compose и docker compose exec nginx nginx -T. Версия в старом README ничего не доказывает. Заодно выясняю, запущен vite или vite preview: у preview есть собственный preview.allowedHosts, который по умолчанию наследует server.allowedHosts, но может быть переопределён. Исправлять соседний режим бессмысленно. Настройки preview.
Разрешать нужно имя из HTTP Host, а не все имена контейнеров
В этой задаче легко смешать три разные вещи: адрес в браузере, имя для соединения Nginx с upstream и заголовок Host, который получает Vite. Пусть браузер обращается к portal.vector.test:8080, а Nginx соединяется с frontend:5173. Из этого ещё не следует, какое имя проверит Vite. Решает конфигурация прокси. В исходной реализации middleware читает req.headers.host и сравнивает hostname без порта; Origin и X-Forwarded-Host эту проверку не заменяют. Код проверки Vite 6.0.9.
Без явного proxy_set_header Host Nginx по умолчанию использует $proxy_host. Для proxy_pass http://frontend:5173 это будет frontend:5173, и разрешать потребуется frontend. Если проксирование идёт через именованную upstream-группу vitepool, в Host может оказаться vitepool. А с proxy_set_header Host $host до Vite дойдёт имя из клиентского обращения. Поэтому я сначала читаю итоговый конфиг с include-файлами, а уже потом составляю список. Правила proxy_set_header.
Мой выбор для обычного dev-стенда — сохранять $host и явно разрешать адрес приложения. Это проще проверять и объяснять следующему разработчику. Имя сервиса Compose нужно только для реального маршрута, где оно становится HTTP Host: например, для прямого запроса тестового контейнера. Названия сети, проекта, контейнера Nginx и всех соседних сервисов в список не попадают «на всякий случай». Docker DNS обеспечивает поиск сервиса, но не превращает его имя в обязательный элемент allowedHosts. Сеть и имена сервисов Compose.
Стенд «Петля и пряжа»: воспроизведение и схема для Compose
Для разбора беру стенд «Петля и пряжа», моделирующий внутреннюю панель заявок. Название условное; это лабораторное воспроизведение, а не история раскрытого клиентского проекта. В измеренной части использованы Vite 6.0.9, Node.js 20.20.2 и Nginx 1.30.4. Vite слушал 127.0.0.1:5173, Nginx работал в контейнере с сетью хоста, а имя frontend было локальным alias для 127.0.0.1. Три конфигурации прокси слушали loopback-порты 18080–18082. Это проверка HTTP-заголовков; работу внутреннего DNS Compose этим опытом я не подтверждаю.
Сначала в allowedHosts стояло только portal.vector.test. Прокси без явного Host вернул 403 с именем frontend. Прокси с Host $host дал 200 для portal.vector.test и 403 для evil.invalid. Добавление frontend в точный массив восстановило первый маршрут, сохранив отказ для чужого имени на втором. Затем контрольное включение true изменило ответ для evil.invalid на 200. Вот чем закончилась проверка: обе точные конфигурации работоспособны, но у каждой свой ожидаемый Host; общий выключатель пропускает дополнительный нежелательный запрос. Браузерный HMR в этом опыте не проверялся. После возврата точного списка отдельно проверен входной фильтр Nginx на порту 18083: нужное имя получило 200, чужое — 400. По окончании опыта временный контейнер удалён, исторический Vite остановлен.
Для переноса схемы в Compose ниже предполагается готовый проект с package-lock.json, поддерживаемым Vite 8.2.x и скриптом dev: vite. Это отдельный пример конфигурации, не отчёт о запуске всей связки. Файл compose.yaml:
services:
frontend:
image: node:24-bookworm-slim
working_dir: /app
command: ["sh", "-c", "npm ci && npm run dev"]
volumes:
- .:/app
- frontend_modules:/app/node_modules
nginx:
image: nginx:1.30.4-alpine
ports:
- "127.0.0.1:8080:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- frontend
volumes:
frontend_modules:У frontend нет ports: Nginx обращается к контейнерному 5173 через общую сеть. expose для такой связи необязателен. При этом отсутствие публикации порта не изолирует frontend от остальных участников сети. Публикация портов Docker.
Порт 8080 доступен с Docker-хоста; для браузера сопоставьте portal.vector.test с 127.0.0.1 через hosts или DNS. Команды ниже используют --resolve, который действует только для соответствующего запроса curl. depends_on задаёт порядок запуска, но не ждёт окончания npm ci: ранний 502 ожидаем. Тег Node обновляется внутри ветки, поэтому для воспроизводимого командного стенда я фиксирую проверенный digest образа. Node 24 LTS выбран для нового окружения; исторический runtime воспроизведения переносить не нужно. Ни нормативов по памяти, ни оценок производительности из этого маленького опыта вывести нельзя.
Конфигурация, которую я оставляю после исправления
В существующем vite.config.ts дополняю блок server, сохраняя плагины React/Vue и остальные настройки проекта. Ниже показан минимальный пример. Протокол, порт и путь в массив не входят. host: '0.0.0.0' нужен, чтобы Vite слушал интерфейс контейнера и был доступен соседнему Nginx; это другая настройка. strictPort не позволяет незаметно уйти с 5173 на следующий порт, пока прокси продолжает стучаться в старый. При таком устройстве мне не нужен frontend в allowedHosts: Nginx передаёт portal.vector.test.
import { defineConfig } from 'vite'
export default defineConfig({
server: {
host: '0.0.0.0',
port: 5173,
strictPort: true,
allowedHosts: ['portal.vector.test'],
},
})В файле nginx.conf я сохраняю Host и отдельно отклоняю неизвестные имена на входе. Этот файл заменяет /etc/nginx/conf.d/default.conf официального образа. map находится в контексте http, куда включается conf.d; внутрь server его переносить нельзя. Отдельный default server нужен потому, что один server_name сам по себе не запрещает остальные имена: запрос может попасть в сервер по умолчанию. Выбор виртуального сервера Nginx.
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80 default_server;
server_name _;
return 400;
}
server {
listen 80;
server_name portal.vector.test;
location / {
proxy_pass http://frontend:5173;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
}Добавить ровно frontend вместо изменения Host тоже допустимо, если это осознанная архитектура. Но здесь есть существенная оговорка. Когда прокси принимает любое внешнее имя и всем запросам подставляет разрешённый frontend, Vite уже не видит исходное чужое имя. Точный массив остаётся в файле, а нужную границу фактически должен обеспечивать Nginx. Я предпочитаю, чтобы это было видно в конфигурации, а не существовало только в голове её автора.
Что снимает allowedHosts: true и насколько это опасно
allowedHosts: true разрешает любые имена. При DNS rebinding страница атакующего может сначала загрузиться с его сервера, а затем через изменённый DNS обратиться к доступному dev-серверу, сохраняя собственное доменное имя в запросе. Проверка Host позволяет отвергнуть такое обращение. Возможность эксплуатации зависит от браузера и сетевых ограничений; утверждать, что после одной строки непременно украдут весь репозиторий, неправильно. Но доступ к исходникам, которые обслуживает dev-сервер, — вполне предметный риск. Модель угроз из advisory Vite.
В закрытом стенде с контролируемым доступом риск обычно ниже, чем у открытого порта на рабочем ноутбуке. Эта строка сама не публикует порт и не отменяет VPN, авторизацию прокси или ограничения файлов. Однако и точный список не заменяет аутентификацию: обычный HTTP-клиент способен отправить допустимый Host вручную. Я не драматизирую временную диагностическую правку, но оставлять её постоянной считаю плохой экономией. Указать одно известное имя ненамного сложнее, чем написать true.
У точного массива тоже есть границы. Vite автоматически допускает localhost, его поддомены и IP-адреса; есть дополнительные разрешения из других настроек, например server.origin. Поэтому обещание «теперь разрешён только один Host вообще» неверно. Запись .vector.test дополнительно охватывает корневое имя и все его поддомены; для одного портала я её не использую. Разрешать стоит только контролируемые имена. Документация allowedHosts, дополнительные разрешения в коде.
Проверяю отказ отдельно от успешного открытия
После запуска docker compose up -d жду готовности Vite в docker compose logs frontend, затем проверяю Nginx командой docker compose exec nginx nginx -t. Одной открывшейся страницы недостаточно. Мне нужны положительный запрос и отрицательный: иначе конфигурация с true выглядит столь же исправной, как точный список. Для показанной Compose-схемы ожидаются следующие ответы:
curl --resolve portal.vector.test:8080:127.0.0.1 \
-o /dev/null -s -w '%{http_code}\n' \
http://portal.vector.test:8080/
# 200
curl -H 'Host: foreign.test' \
-o /dev/null -s -w '%{http_code}\n' \
http://127.0.0.1:8080/
# 400 от NginxВторой запрос проверяет входной фильтр Nginx, а не allowedHosts самого Vite. Для отдельной проверки приложения я использую Node внутри frontend: публиковать 5173 ради диагностики не требуется. Здесь правильный адрес должен получить 200, а чужое имя — 403. Это функциональная проверка политики имён, не демонстрация эксплуатации DNS rebinding.
docker compose exec frontend node --input-type=module -e '
for (const host of ["portal.vector.test", "foreign.test"]) {
const r = await fetch("http://127.0.0.1:5173/", {
headers: { Host: host }
});
console.log(host, r.status);
}'Дальше проверяю HMR в браузере: соединение WebSocket получает 101, изменение исходника появляется без ручной перезагрузки. Заголовки Upgrade и Connection в конфиге выше нужны именно для этого. Если HTML уже отдаётся, а обновления не приходят, снова расширять allowedHosts не надо. Сначала смотрю WebSocket URL и проксирование. В Vite 8.1 параметры соединения перенесены в server.ws; прежние поля server.hmr ещё совместимы, но устаревают. Для показанного маршрута ручные ws-настройки обычно не нужны. WebSocket в Nginx, изменения Vite 8.1.
Ещё одна ловушка — HTTPS перед Nginx. Документация говорит о пропуске host-check, когда HTTPS включён у самого Vite. Если TLS завершается на прокси, а дальше стоит proxy_pass http://frontend:5173, сервер Vite остаётся HTTP-сервером и проверка действует. Замок в адресной строке этого не меняет. Я также не добавляю cors: true для исправления Host: разрешения Origin и hostname решают разные задачи, и массовое снятие обеих проверок только затрудняет диагностику. Условие установки middleware в Vite.
Что закрепить, чтобы следующая сборка не вернула проблему
Порядок действий для меня простой: выяснить реальный Host, восстановить нужный маршрут точным разрешением, проверить чужое имя, затем проверить HMR. Очистку всех кешей, смену Docker DNS и переезд на другой порт откладываю до появления соответствующих симптомов. После правки фиксирую рядом Nginx-конфиг, Vite-конфиг и короткие проверки. Комментарий «portal.vector.test приходит через Host $host» полезнее длинного списка имён, происхождение которых через месяц никто не вспомнит. В автоматическую проверку после обновления зависимости я включаю оба HTTP-сценария. Положительный ловит сломанный доступ команды, отрицательный — случайный возврат true. Если прямые CI-запросы действительно используют имя frontend, это отдельное документированное разрешение, которое удаляется вместе с соответствующим маршрутом.
Учитываю и способ доставки изменений. Новый конфиг, который запечён в образ, требует пересборки; изменённые переменные Compose — пересоздания контейнера, одного restart недостаточно. После пересоздания frontend его IP может поменяться, а простой статический upstream Nginx — сохранить прежний адрес: для этого стенда предусматриваю reload или перезапуск Nginx. Такая неисправность обычно проявляется как ошибка связи. Добавление ещё одного hostname её не исправит.
На 5 сентября 2026 года регулярные исправления получает Vite 8.2; для нового окружения я выбираю поддерживаемую ветку и Node 24 LTS. Исторические 6.0.9 и Node 20 из воспроизведения не являются целевым комплектом. А если внутренний портал уже обслуживает сотрудников, сначала выясняю, нужен ли ему вообще dev-сервер: обычную SPA собираю через vite build и отдаю dist Nginx. vite preview тоже предназначен для проверки сборки, а не production. Для SSR нужен предусмотренный фреймворком runtime. Поддержка Vite, релизы Node.js, развёртывание сборки.
Частые вопросы
Нужно ли разрешать frontend, nginx и имя сети Compose?
Только если конкретное имя реально приходит в HTTP Host допустимого запроса. При Host $host и адресе portal.vector.test имя frontend остаётся адресом соединения Nginx с сервисом и само по себе не требует разрешения.
Можно ли настроить разрешённое имя через environment?
Да. Для Vite 8.2 передайте процессу через Compose environment переменную __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS=portal.vector.test. Она дополняет разрешения. Поддержка нескольких имён через запятую появилась в Vite 8.1; для совместимости со старыми ветками используйте массив в конфиге. После изменения environment пересоздайте контейнер.
Почему доступ по IP работает, хотя имени нет в списке?
IP-адреса разрешены Vite автоматически, как localhost и его поддомены. Точный массив задаёт дополнительные имена и не превращает Host-проверку в сетевой ACL.
Почему после исправления 403 не работает HMR?
Проверьте проксирование WebSocket и адрес соединения в браузере. Успешная выдача HTML не подтверждает работу HMR; расширение allowedHosts не исправит отсутствующие Upgrade и Connection.
Если после обновления Vite стенд держится на `allowedHosts: true`, я помогу разобрать цепочку запросов и настроить точные разрешения. В «АйТи-Фреш» можно обратиться с задачей по Nginx, Docker и инфраструктуре разработки — начнём с конфигурации и проверяемого результата.
Бесплатная консультация →
Источники
- [Vite: security advisory GHSA-vg6x-rcgg-rjx6](https://github.com/vitejs/vite/security/advisories/GHSA-vg6x-rcgg-rjx6) — 20.01.2025; первые исправленные версии 6.0.9, 5.4.12, 4.5.6; разделы о reverse proxy и DNS rebinding.
- [Vite: Server Options](https://vite.dev/config/server-options) — server.host, server.allowedHosts, server.strictPort, server.https, server.cors и server.ws; проверено 05.09.2026.
- [Vite: Preview Options](https://vite.dev/config/preview-options) — preview.allowedHosts и наследование server.allowedHosts.
- [Vite 6.0.9: исходный код hostCheck.ts](https://raw.githubusercontent.com/vitejs/vite/v6.0.9/packages/vite/src/node/server/middlewares/hostCheck.ts) — Чтение req.headers.host, выделение hostname без порта, ответ HTTP 403.
- [Nginx: ngx_http_proxy_module](https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_set_header) — proxy_set_header, значение Host $proxy_host по умолчанию и наследование директив.
- [Nginx: обработка запросов](https://nginx.org/en/docs/http/request_processing.html) — Выбор server_name и default_server.
- [Nginx: WebSocket proxying](https://nginx.org/en/docs/http/websocket.html) — Передача Upgrade и Connection, map в контексте http.
- [Docker: Networking in Compose](https://docs.docker.com/compose/how-tos/networking/) — Обнаружение по имени сервиса, контейнерные порты, изменение IP при пересоздании.
- [Vite: changelog 8.1.0](https://raw.githubusercontent.com/vitejs/vite/v8.1.0/packages/vite/CHANGELOG.md) — 23.06.2026; перенос параметров соединения в server.ws и поддержка нескольких имён в __VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS.
- [Vite: политика поддержки релизов](https://vite.dev/releases) — Состояние поддержки веток на 05.09.2026; регулярные исправления для 8.2.
- [Node.js: релизы](https://nodejs.org/en/about/previous-releases) — На 05.09.2026 Node 24 имеет статус LTS, Node 20 — EOL.
- [Vite: развёртывание статической сборки](https://vite.dev/guide/static-deploy) — Сборка dist и ограниченное назначение vite preview; отдельный подход для SSR.
