Руководства по безопасности

Случай, когда .env в Laravel был доступен публично: ошибка размещения на шаред-хостинге и как её исправить

На шаред-хостинге .env, .git и дампы БД Laravel читаются по HTTP, если проект лежит под веб-корнем, а не только public/. Исправление в три шага.

Опубликовано 2026-06-07 6 мин чтения
На этой странице

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

Карточка инцидента — INCIDENT FILE
Класс
Открытость .env / .git / дампов БД (неверная настройка)
Серьёзность
Критическая (каждый секрет читаем по HTTP)
Причина
Тело Laravel размещено прямо под public_html (выставляться должен только public/)
Распространение
Каждое новое приложение повторяло то же неверное размещение → десятки
Постоянное решение
Тело вне docroot + симлинк только на public/

Худшие файлы — .env и .git

.env хранит все секреты в одном файле: аутентификация БД, почта, внешние API, ключ шифрования приложения (APP_KEY). При публичном .git злоумышленники восстанавливают не только текущие значения, но и каждую прошлую ротацию, пройдя по истории git. См. Что такое .env.

Что происходило

На шаред-хосте многие Laravel-приложения были разложены так. Вот опасная и правильная раскладки рядом:

Опасная раскладка (частая)

~/public_html/
└── app/          ← весь Laravel (неверно)
    ├── .env      ← читается по /app/.env
    ├── .git/     ← clone восстанавливает всю историю
    ├── vendor/  config/  storage/
    └── public/   ← ЕДИНСТВЕННОЕ, что должно тут быть

Правильная раскладка

~/laravel/app/    ← тело вне веб-корня
├── .env  .git/   ← недостижимо по HTTP = безопасно
└── public/       ← только это, выставлено через симлинк
        └── index.php

Типичные файлы, которые возвращали 200 OK внешнему миру:

ФайлЧем опасен
.env / .env.bk_*аутентификация БД, почта, внешние API-ключи, APP_KEY — всё в открытом виде
директория .git/git clone восстанавливает исходник и всю историю
composer.lock / package-lock.jsonточные версии зависимостей = подсказывают атакующим, какие известные CVE применимы
credentials.jsonOAuth client secrets, в комплекте
*.sql / db-*.sql.gzполный дамп базы данных, десятки МБ в сжатом виде

Самое опасное: та же ошибка распространяется

Раз разложив так, каждое новое приложение того же рода повторяет то же неверное размещение. Одна неверная настройка превращается в десятки. Правило: в момент, как заметили, проверьте их все.

Исправление в три шага

Шаг 1 — Первая помощь: заблокируйте чувствительные файлы на уровне HTTP

Сначала остановите «дальнейшую утечку». Это обратимо и не влияет на обычные страницы. Добавьте блок deny в начало public_html/.htaccess (защитный пример конфигурации):

# === SECURITY BLOCK ===
# 404 на всё под .env / .git
RedirectMatch 404 (?i)/\.(env|git)(\..*)?(/|$)
 
# запретить резервные копии, дампы, файлы учётных данных
<FilesMatch "(?i)\.(sql|sql\.gz|bak|old|swp|save|orig|tgz)$|^credentials\.json$|\.bk[._]">
    Require all denied
</FilesMatch>
 
# если внутренности Laravel видны на /<app>/storage/... и т. п.
RedirectMatch 403 (?i)^/[^/]+/(storage|bootstrap/cache|config|database|resources|routes|tests|vendor)(/|$)
# === END ===

Но чёрному списку нужно новое правило для каждого нового файла, так что что-то всегда упускается. Вы забудете composer.lock; новые имена файлов не будут покрыты. Относитесь к нему как к временной мере, а надёжное исправление сделайте в Шаге 3.

Шаг 2 — Смените ключи (считайте, что их видели)

Даже после блокировки через .htaccess считайте секреты уже увиденными. По приоритету:

  1. Внешние API-ключи (высший приоритет, немедленно): они завязаны на биллинг и захват аккаунта. Отзовите + перевыпустите OAuth CLIENT_SECRET, API-ключи и облачные учётные данные, с минимальными привилегиями.
  2. OAuth client secrets.
  3. APP_KEY: перегенерация может сломать сессии и сделать зашифрованные столбцы БД нерасшифровываемыми — сначала проверьте, на что это повлияет.
  4. Почта/SMTP, учётные данные БД.

Ошибка при ротации, которая реально случилась

У некоторых OAuth-провайдеров перевыпуск client secret также аннулирует существующие refresh_token — обновление по сохранённым токенам падает и вызывает простой продакшена. Всегда держите готовым поток повторной авторизации до перевыпуска. И обновите свой локальный .env / конфигурацию тоже, иначе следующий деплой вернёт старое значение.

Шаг 3 — Реструктуризация: вынесите тело вне docroot (коренное исправление)

Переразложите каждое приложение так:

# 1. Вынести тело вне веб-корня (та же файловая система → mv мгновенный, атомарный)
mv ~/example/public_html/app  ~/example/app-laravel
 
# 2. Поместить в веб-корень лишь минимальный бутстрап
mkdir -p ~/example/public_html/sub
cat > ~/example/public_html/sub/index.php <<'PHP'
<?php require __DIR__ . '/../../app-laravel/public/index.php';
PHP
# 3. Скопировать Laravel public/.htaccess, сделать симлинки на статику, очистить кэш конфигурации

В этой форме .env, .git и vendor/ просто не существуют в веб-корне, так что вы в безопасности без опоры на правила .htaccess. Полные шаги — в Держим .env вне публичного веба на шаред-хостинге.

✗ Опасно: тело внутри веб-корня

— граница веба (читаемо по HTTP) —

.env · .git · vendor · public

↑ всё внутри линии = прочитать может кто угодно

✓ Безопасно: тело вне веб-корня

— граница веба (читаемо по HTTP) —

только public/ (симлинк)

.env · .git снаружи = недостижимы

Всё решает одно: внутри или снаружи веб-корня. Если .env и .git сидят внутри, их может прочитать кто угодно по HTTP.

Проблемы, с которыми мы столкнулись при исправлении

  • Проблема области поддомена: перенацеливание docroot зарегистрированного поддомена на другое дерево через симлинк может не последовать и вернуть 500. → Держите docroot реальной директорией и require по абсолютному пути из маленького index.php внутри неё (форма «бутстрап-редирект») для устойчивости.
  • opcache помнит сломанное состояние: opcache может запомнить неудачную загрузку index.php и продолжать возвращать 500 даже после пересборки. Крайнее средство: сменить имя файла (entry.php и т. п.).
  • «Незарегистрированный» docroot поддомена, читаемый через родителя: https://sub.example/ может быть недействителен, тогда как https://example/sub/ всё ещё показывает содержимое — легко упускаемый путь. При проверке блокируйте и путь родительского домена.
  • Захардкоженные секреты в config/*.php: в сочетании с публичным .git история раскрывает каждую ротацию. Всегда идите через env() и никогда не кладите реальные значения в значения по умолчанию.

Урок: стандартная раскладка сервера облегчает эту ошибку

Ранняя практика шаред-хостинга клала всё прямо под public_html. Когда Laravel (который ожидает выставленным только public/) стал там широко использоваться, опасная раскладка «проект под public_html/<app>/, выставить его public/» стала де-факто стандартом — собственные документы вендоров используют примеры на базе public_html, что помогло ей распространиться. Поэтому исправляйте это процессом, а не бдительностью:

Профилактика рецидива процессом

  • Сделайте дефолтом развёртывания «тело вне docroot, симлинк только на public/».
  • Проверки CI/lint: никогда не коммитить .env; никогда не размещать проект прямо под public_html/<app>.
  • Самопроверка после деплоя: убедиться, что /.env и /.git/config не получаемы снаружи.

Этот сайт встраивает привычку «проверь свой сайт сам» в свой обучающий трек. → Чем опасны .env и API-ключи

Читать дальше

FAQ

QМой .env открыт — что делать в первую очередь?
A

Сначала заблокируйте путь через .htaccess, затем смените внешние API-ключи и OAuth-секреты в .env по приоритету (считайте, что их видели), затем перенесите тело приложения вне веб-корня, чтобы быть в безопасности без опоры на правила.

QПочему размещать Laravel под public_html опасно?
A

Laravel построен так, чтобы выставлять только public/. Поместите весь проект в docroot — и всё выше него: .env (все ваши секреты), .git (восстановимая история), vendor/ — становится читаемым по HTTP.

QДостаточно ли блокировки чёрным списком в .htaccess?
A

Как временная мера работает, но каждое новое имя файла требует нового правила, так что что-то всегда упускается. Надёжное исправление — структурное изменение: вынести тело приложения вне docroot и выставлять только public/ через симлинк.