Руководства по безопасности
У Laravel-приложений .env был доступен всему миру — самая частая ошибка шаред-хостинга
На шаред-хостинге у Laravel-приложений .env, .git, дампы БД и OAuth-секреты оказываются читаемыми по HTTP. Причина: весь проект под веб-корнем вместо только public/. Исправление в три шага и почему это общеотраслевой дурной паттерн.
Неверная настройка, которая реально случается на шаред-хостинге, с убранными доменами и значениями и превращённая в урок. Это не про поиск чужих сайтов — это про защиту своего (или унаследованного) развёртывания.
- Класс
- Открытость .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.json | OAuth 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 считайте секреты уже увиденными. По приоритету:
- Внешние API-ключи (высший приоритет, немедленно): они завязаны на биллинг и захват аккаунта. Отзовите + перевыпустите OAuth
CLIENT_SECRET, API-ключи и облачные учётные данные, с минимальными привилегиями. - OAuth client secrets.
APP_KEY: перегенерация может сломать сессии и сделать зашифрованные столбцы БД нерасшифровываемыми — сначала проверьте радиус поражения.- Почта/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 снаружи = недостижимы
Ловушки, которых «нет в учебнике»
- Проблема области поддомена: перенацеливание 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-ключи
Читать дальше
- Глоссарий: Что такое .env · Что такое CVE
- Защита: Держим .env вне публичного веба на шаред-хостинге
- Инцидент: Когда украденный API-ключ привёл к мошенническим счетам (другой путь утечки — среда выполнения)
FAQ
QМой .env открыт — что делать в первую очередь?
Остановите кровотечение (заблокируйте путь через .htaccess), затем смените внешние API-ключи и OAuth-секреты в .env по приоритету (считайте, что их видели), затем перенесите тело приложения вне веб-корня, чтобы быть в безопасности без опоры на правила.
QПочему размещать Laravel под public_html опасно?
Laravel построен так, чтобы выставлять только public/. Поместите весь проект в docroot — и всё выше него: .env (все ваши секреты), .git (восстановимая история), vendor/ — становится читаемым по HTTP.
QДостаточно ли блокировки чёрным списком в .htaccess?
Как первая помощь работает, но по сути это игра в «прибей крота» — каждое новое имя файла добавляет дыру. Настоящее исправление — структурное изменение: вынести тело приложения вне docroot и выставлять только public/ через симлинк.