Пароли в .env, который отдаётся по HTTP: как проверить свой сайт
Откройте браузер и наберите адрес своего сайта со слэшем /.env на конце. Если вместо 404 вы увидите текст с DB_PASSWORD= или MAIL_PASSWORD= — сервер только что бесплатно раздал пароли всем, кто умеет вводить URL руками. Это не экзотика: файл окружения оказывается доступен по HTTP чаще, чем кажется, особенно после переноса проекта на новый сервер или смены веб-сервера, когда старые правила не переехали вместе с сайтом. Разберём, как проверить себя за пять минут, как настроить nginx и Apache так, чтобы это стало невозможно, и что делать, если проверка уже показала плохой результат.
Содержание
Откуда берётся эта дыра
Файл .env — стандарт для Laravel, Symfony, Node.js-приложений, Django и почти всего, что читает конфиг через dotenv. По задумке он лежит рядом с кодом, но веб-сервер должен отдавать наружу только выделенную папку — public/, dist/, wwwroot — а не весь корень проекта. Проблема начинается там, где эти две вещи расходятся:
- Document root указывает не туда. В Laravel корень —
public/, а.envлежит уровнем выше. Если nginx настроили на корень проекта, а не наpublic/, наружу торчит всё — включая.envи.git. - Перенос сайта на новый сервер. Старый конфиг переезжает по памяти или генерируется заново панелью управления, и правило
denyдля dotfiles, которое годами было в старом конфиге, просто не воспроизводится. - Смена веб-сервера. Apache через
.htaccessчасто блокирует dotfiles ещё в дистрибутивном конфиге. nginx ничего не знает про.htaccessи отдаёт любой файл в корне, если ему явно не запретили. - Docker. В
Dockerfileскопировали весь проект вместо только собранного фронтенда —.env.productionуехал в образ вместе с публичной директорией.
Итог один: файл, который должен видеть только процесс приложения на сервере, становится виден любому боту в интернете.
Как проверить свой сайт прямо сейчас
Проверяйте с внешней машины, не с самого сервера — важно то, что видит внешний мир:
for path in .env .env.local .env.production .env.backup \
.git/config docker-compose.yml wp-config.php.bak \
config/database.yml .aws/credentials; do
code=$(curl -s -o /dev/null -w "%{http_code}" "https://example.com/$path")
echo "$code /$path"
done
Что означают коды: 404 — файла нет или доступ заблокирован на уровне маршрутизации, хорошо. 403 — сервер знает про файл и явно запрещает доступ, тоже хорошо. 200 — сервер отдал содержимое, это провал: откройте curl https://example.com/.env без подавления вывода и смотрите, что утекло. 301/302 — не всегда безопасно, проверяйте через curl -L, иногда редирект даёт SPA-роутер, а сам файл всё равно доступен.
Отдельно проверьте .git/config — если каталог .git/ попал в webroot (частая история при деплое через git pull прямо в публичную директорию), по нему можно скачать всю историю репозитория инструментами вроде git-dumper, включая коммиты с паролями, которые вы давно удалили из последней версии кода.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверБолее широкая проверка
Ограничиваться одним файлом неправильно — если сломана логика запрета dotfiles, под ударом целый класс путей:
| Путь | Что это | Риск при утечке |
|---|---|---|
/.env* | Переменные окружения | Пароли БД, ключи API, секреты сессий |
/.git/ | Git-репозиторий | История кода и всех когда-либо закоммиченных секретов |
/docker-compose.yml | Оркестрация контейнеров | Пароли БД, порты внутренних сервисов |
/*.sql, /dump.sql | Бэкапы «на всякий случай» | Полный дамп базы данных |
/wp-config.php.bak | Резервная копия конфига CMS | То же, что .env, для WordPress |
Для регулярной проверки удобнее готовые сканеры с актуальным списком путей — мы разбирали подходящие инструменты в статье про сканирование уязвимостей сервера. Для точечной проверки одного сайта достаточно и цикла с curl выше — его можно поставить в cron и получать алерт, если статус вдруг поменялся с 404 на 200 после очередного деплоя.
Правильная настройка nginx
В nginx нет автоматического запрета dotfiles — его нужно прописать явно в каждом виртуальном хосте (или один раз в общем snippets-файле, подключаемом везде):
server {
listen 443 ssl http2;
server_name example.com;
root /var/www/example.com/public;
location ~ /\.(?!well-known) {
deny all;
return 404;
}
location ~* \.(env|ini|log|bak|sql|yml|yaml)$ {
deny all;
return 404;
}
location / {
try_files $uri $uri/ /index.php?$query_string;
}
}
location ~ /\.(?!well-known) блокирует всё, что начинается с точки, кроме .well-known — он нужен для ACME-подтверждения Let's Encrypt. Второе правило по расширениям — подстраховка на случай, если root по ошибке указывает не на public/, а на корень проекта. Но это именно подстраховка: root должен указывать строго на директорию с публичными файлами, а .env, vendor/, .git — физически лежать вне неё. Deny-правила — второй рубеж, а не единственный: если полагаться только на них, вы зависите от того, что ни одно правило нигде не забыто, а на практике забывают.
После правки — nginx -t, systemctl reload nginx и повторный curl с внешней машины: конфиг мог примениться не к тому серверному блоку, если на домен настроено несколько server{}.
Правильная настройка Apache
Дистрибутивы Debian/Ubuntu исторически поставляются с более консервативными дефолтами, но полагаться на них нельзя — многое зависит от того, включён ли AllowOverride. Правило нужно прописать явно, в основном конфиге виртуального хоста:
<VirtualHost *:443>
ServerName example.com
DocumentRoot /var/www/example.com/public
<Directory /var/www/example.com/public>
Options -Indexes
AllowOverride All
Require all granted
</Directory>
<FilesMatch "^\.">
Require all denied
</FilesMatch>
<FilesMatch "\.(env|ini|log|bak|sql|yml|yaml)$">
Require all denied
</FilesMatch>
</VirtualHost>
Если доступа к основному конфигу нет и всё управление идёт через .htaccess в корне проекта, те же правила пишутся туда же. Важная оговорка: .htaccess работает, только если у директории включён AllowOverride All (или хотя бы FileInfo Limit) в основном конфиге — на части хостингов это не так, и .htaccess просто не читается. Проверить легко: положите в .htaccess заведомо ломающую строку и посмотрите, упадёт ли сайт с 500-й ошибкой; если нет — файл не применяется, нужен доступ к основному конфигу или обращение в поддержку хостинга.
Правило для обоих серверов одно: лучшая защита — не полагаться на deny-правила как на единственный барьер, а физически убрать .env и секреты за пределы директории, которую отдаёт веб-сервер.
Если файл оказался доступен: действуйте немедленно
Если проверка показала 200 и содержимое .env открылось — не тратьте время на выяснение, скачивал ли кто-то файл реально. Боты, сканирующие интернет на типичные пути вроде /.env, работают постоянно и массово, отличить целевую атаку от фонового шума по одной строке в логе почти невозможно. Правильная реакция — считать все секреты скомпрометированными и действовать так, будто их уже забрали:
- Закройте дыру первой — разверните deny-правило, перезагрузите веб-сервер, повторите curl-проверку.
- Смените пароль базы данных и проверьте логи БД на подключения с незнакомых IP за период, пока файл был открыт.
- Перевыпустите все API-ключи: платёжные шлюзы, SMTP, облачные хранилища (в AWS IAM отзовите старые
AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEYи создайте новые), сторонние сервисы. - Смените секрет сессий и подписи токенов (
APP_KEY,JWT_SECRET) — это разлогинит всех пользователей, но безопасность важнее удобства. - Проверьте сервер на посторонние процессы и новые ключи SSH, если в
.envбыл доступ к деплою или панели управления. - Проверьте, не коммитился ли этот же
.envв Git. Если да, секреты остались в истории репозитория независимо от текущей доступности по HTTP — их нужно ротировать так же, как при прямой утечке. Похожая история с ключом, ненадолго попавшим в открытый доступ, разобрана в статье про ключ от CI, пролежавший в публичном репозитории восемнадцать минут — короткое окно доступности не делает утечку менее реальной. - Задокументируйте инцидент: что было открыто, с какого момента, какие секреты сменили и когда.
Отсутствие подозрительных запросов в логах ничего не доказывает при массовом автоматическом сканировании — часть ботов не логируется реверс-прокси или CDN. Дешевле сменить пароли за час, чем потом разбираться со слитой базой клиентов.
Как не допустить повторения
- Держите
.envвне webroot по умолчанию, а не полагайтесь только на deny-правило — не переносите его вpublic/«для удобства» и не указывайтеrootна корень проекта. - Добавьте проверку путей в чек-лист после каждого переноса сайта или смены веб-сервера — ошибка чаще всего появляется именно в момент миграции, когда конфиг переписывают заново. Общий чек-лист для нового сервера — в статье чек-лист безопасности нового сервера, а нюансы самого переезда — в статье про перенос сайта на новый VPS без простоя.
- Автоматизируйте проверку — cron раз в день с curl-запросами из раздела выше и алертом в Telegram при коде 200 дешевле, чем разбор инцидента постфактум.
- Где возможно, храните секреты вне файловой системы — через
EnvironmentFile=systemd-юнита с правами600вне webroot, Docker secrets или менеджер секретов. Это не отменяет deny-правила, но убирает файл из числа тех, что вообще можно случайно отдать по HTTP. - Ограничьте права на файл —
chmod 600 .env, владелец — пользователь приложения, а неwww-dataс широкими правами на всю директорию.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Как часто перепроверять сайт на эту уязвимость?
После каждого изменения конфигурации веб-сервера, переноса на новый сервер или смены nginx/Apache — обязательно, плюс автоматическая проверка по cron хотя бы раз в неделю.
Правило deny all в nginx решает проблему полностью?
Оно закрывает конкретный путь, но не первопричину. Если root указывает выше, чем нужно, любая забытая маска расширения снова оставит файл доступным. Правильный root, указывающий строго на public/, надёжнее списка запрещённых расширений.
Достаточно ли переименовать .env в .env.production, чтобы скрыть его от сканеров?
Нет. Сканеры проверяют десятки типичных вариаций имени. Здесь нужен явный запрет доступа и правильный root, а не смена имени файла.
Нужно ли проверяться, если сайт статический и .env там не используется?
Да — deploy-скрипты и сборщики фронтенда нередко оставляют служебные файлы (.git, конфиги CI) в папке, которую потом целиком выкладывают на сервер как есть.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →