Открытый .git на сайте и забытый .env: как исходный код утекает наружу
Это не экзотика и не сложная атака. Сайт работает, ничего не сломано, а по адресу
ваш-сайт.ру/.git/config браузер спокойно показывает текст — и вместе с ним
наружу уходит исходный код проекта и вся история его правок. Разберём, откуда это берётся,
чем на самом деле опасно и как закрыть за один вечер.
Что такое каталог .git и почему он оказался на сервере
.git — служебный каталог системы контроля версий Git: в нём лежит вся история
проекта, то есть каждая версия каждого файла, которая когда-либо была сохранена
разработчиком. Сам сайт для работы этот каталог не использует: он нужен только людям,
которые пишут код.
На боевом сервере ему делать нечего, и всё же он там регулярно оказывается. Причины однотипные и скучные:
- Папку проекта скопировали на сервер целиком.
scp -r, FTP-клиент, «просто залил через файловый менеджер в панели хостинга» — переносится всё, включая скрытые каталоги. Скрытые они, кстати, только для файлового менеджера: веб-сервер их отдаёт как обычные файлы. - Сайт развернули через
git cloneпрямо в каталог, из которого он отдаётся. Удобно: обновление сводится кgit pull. Побочный эффект —.gitлежит внутри публичной части сайта и доступен всем. - Корень сайта указывает не туда. У проектов на современных фреймворках публичной должна быть только подпапка (обычно
public/), а всё остальное — уровнем выше. Если в настройках хостинга корнем указана папка проекта целиком, наружу выставляются разом и.git, и.env, и всё прочее содержимое. - В конфигурации веб-сервера просто нет запрещающего правила. Стандартный Apache по умолчанию закрывает только собственные файлы вида
.htaccess, а nginx «из коробки» не закрывает ничего. Всё, что лежит в корне, он честно отдаёт. - Сайт переезжал. Смена хостинга или подрядчика — это почти всегда копирование каталога как есть, вместе с накопленным мусором.
Обратите внимание: ни в одном пункте нет злого умысла или редкой ошибки. Это обычный путь обычного сайта, поэтому находка встречается у компаний любого размера.
Почему это опаснее, чем «ну увидят исходники»
Первая реакция владельца обычно спокойная: «Ну и что, там нет ничего секретного, обычный
сайт на PHP». Проблема в том, что открытый .git — это не «посмотреть пару
файлов». Это доступ к архиву проекта, из которого восстанавливается рабочий
исходный код целиком, а вместе с ним — вся история изменений.
Что из этого следует практически:
- Видна вся логика сайта. Как проверяется оплата, как формируется ссылка на скачивание документа, какие параметры принимает форма, где стоят проверки прав, а где их забыли. Искать уязвимость наугад больше не нужно — её можно прочитать.
- Видны подключаемые компоненты и их версии — а к версиям привязаны опубликованные уязвимости.
- В
.git/configлежит адрес репозитория, а иногда и логин с токеном доступа к нему, если репозиторий подключали по HTTPS со встроенными учётными данными. Это уже не про сайт, а про доступ ко всей вашей кодовой базе, включая другие проекты. - Видны имена и почты разработчиков из истории коммитов — готовый список целей для писем «от подрядчика».
- И главное — секреты из прошлых версий файлов. Об этом ниже, потому что именно здесь ошибаются чаще всего.
Ключевая мысль: удалить пароль из файла — не значит удалить его из истории
Git устроен так: каждое сохранение (коммит) фиксирует состояние файлов на тот момент и хранит его навсегда. Новый коммит не переписывает старый, а добавляется рядом. История — это не журнал изменений, это набор полных снимков.
Отсюда типичная история, которая происходила почти в каждом проекте:
- Разработчик в спешке вписал пароль от базы данных прямо в файл конфигурации и сохранил изменения.
- Через неделю опомнился, вынес пароль в переменные окружения, а из файла удалил. Сделал новый коммит.
- В текущей версии кода пароля нет. Все спокойны.
Но коммит недельной давности никуда не делся — и в нём этот пароль лежит открытым текстом. Любой, у кого есть копия репозитория, читает не только последнюю версию файла, но и все предыдущие. То есть каждый ключ, пароль и токен, который хотя бы один раз попадал в коммит, остаётся доступным навсегда, даже если из рабочего кода его убрали в тот же день.
Именно поэтому открытый .git опасен даже на сайте-визитке, где «нечего
красть»: интересен не текущий код, а то, что в этом коде когда-то лежало и было забыто.
Что обычно лежит рядом
Открытый .git редко бывает единственной находкой: причина у всех этих файлов
общая — на сервер выложили рабочую папку целиком. Проверять стоит сразу весь набор.
| Файл | Что в нём | Чем грозит |
|---|---|---|
.env |
пароль базы, ключи платёжного шлюза, токены рассылок, секрет сессий | самый тяжёлый случай: доступы отдаются готовым списком, без всякого восстановления кода |
dump.sql, backup.zip, site.tar.gz |
выгрузка базы или архив сайта, забытые в корне после переноса | клиентская база, заказы, хеши паролей пользователей — скачиваются одним запросом |
.svn |
то же, что .git, но от системы Subversion |
встречается на старых сайтах и проверяется злоумышленниками так же автоматически |
composer.lock, package.json |
полный список библиотек с точными версиями | сам по себе не уязвимость, но избавляет атакующего от подбора: видно, что устарело |
.idea/, .vscode/, .DS_Store |
настройки редактора, иногда параметры подключения к серверам; .DS_Store — перечень файлов в папке |
раскрывает внутреннюю структуру проекта и служебные пути |
phpinfo.php, test.php, info.php |
полная конфигурация PHP: версии, пути, модули, иногда переменные окружения | оставляют «на пять минут» при отладке и забывают на годы |
Все эти адреса злоумышленники не угадывают — их перебирают автоматически, по списку, у всех подряд. Поэтому «наш сайт маленький, кому он нужен» здесь не работает: никто не выбирал ваш сайт персонально.
Как проверить у себя за минуту
Инструменты не нужны, достаточно браузера. Откройте по очереди (подставив свой домен):
ваш-сайт.ру/.git/configваш-сайт.ру/.envваш-сайт.ру/dump.sqlиваш-сайт.ру/backup.zipваш-сайт.ру/phpinfo.php
Что означает ответ:
| Что вы увидели | Что это значит |
|---|---|
| Текст файла или предложение скачать его | Плохо. Файл доступен всему интернету прямо сейчас. Переходите к разделу «Как закрыть», а если это .git или .env — потом обязательно к разделу про перевыпуск секретов. |
| 403 Forbidden | Доступ закрыт — уже неплохо, но повод посмотреть настройку. Ответ 403 подтверждает, что файл существует, и часто означает, что закрыт только он, а не весь каталог. Проверьте заодно /.git/HEAD и /.git/index: бывает, что закрыли одно, а рядом всё открыто. |
| 404 Not Found или обычная страница ошибки сайта | Нормально. Либо файла нет, либо сервер настроен отдавать 404 на такие запросы — это и есть желаемое поведение. |
Такие открытые файлы — как раз то, что находит любая внешняя проверка сайта, в том числе бесплатная. О том, где вообще проходит граница возможностей автоматических сканеров, мы писали отдельно: что находят бесплатные сканеры уязвимостей и чего они не покажут.
Как закрыть
nginx
В блок server { ... } добавляется правило, запрещающее выдачу всего, что
начинается с точки:
location ~ /\.(?!well-known) {
deny all;
}
Оно закрывает разом .git, .env, .svn,
.idea и любые другие скрытые файлы и каталоги.
Исключение .well-known убирать нельзя. Каталог
/.well-known/ тоже начинается с точки, но он служебный и публичный: через него
Let’s Encrypt подтверждает, что домен ваш, когда выпускает и продлевает бесплатный
TLS-сертификат. Если закрыть скрытые файлы «все подряд», сертификат перестанет продлеваться —
и месяца через три сайт встретит посетителей предупреждением браузера о небезопасном
соединении. Конструкция (?!well-known) в правиле означает ровно одно: «всё, что
с точки, кроме well-known».
Пара практических замечаний:
- Разместите этот блок выше других правил
location ~в том жеserver: nginx применяет первое подошедшее регулярное правило по порядку в конфиге. deny all;отвечает кодом 403, то есть подтверждает существование файла. Если хотите не подтверждать — замените строку наreturn 404;.- После правки проверьте конфигурацию и примените её:
nginx -t, затемsystemctl reload nginx. Перезагрузка без проверки — хороший способ уронить сайт из-за опечатки.
Резервные копии в корне точкой не начинаются, поэтому их закрывают отдельным правилом:
location ~* \.(sql|zip|tar|gz|bak|old)$ {
return 404;
}
Перед добавлением убедитесь, что сайт не раздаёт такие файлы легально: если у вас есть
раздел с загрузкой прайса в .zip, он перестанет работать. Правильнее вообще не
хранить дампы и архивы внутри публичной папки — переложите их уровнем выше.
Apache
Для Apache 2.4 достаточно одной строки в файле .htaccess в корне сайта
(создайте его, если файла нет):
RedirectMatch 404 /\.(?!well-known)
Логика та же: любой адрес, содержащий скрытый файл или каталог, получает 404, а
.well-known продолжает работать. Резервные копии закрываются отдельно:
<FilesMatch "\.(sql|zip|tar|gz|bak|old)$">
Require all denied
</FilesMatch>
Если хостинг запрещает использовать .htaccess или директивы в нём игнорируются,
правило нужно добавить в конфигурацию виртуального хоста — это делает хостер или системный
администратор.
Правильный вариант: не выкладывать служебные каталоги вообще
Правило в конфиге — это страховка, а не решение. Решение — чтобы .git и
.env физически не лежали в публичной части сайта:
- Выкладывайте результат сборки, а не рабочую копию репозитория. На сервер должны попадать только файлы, нужные для работы сайта.
- Если копируете вручную — исключайте служебное явно. У
rsyncдля этого есть--exclude: например,rsync -a --exclude='.git' --exclude='.env' .... У FTP-клиентов есть фильтры передачи. - Разворачиваете через
git clone— клонируйте не в публичную папку. Держите репозиторий уровнем выше корня сайта, а наружу отдавайте только нужный подкаталог. - Проверьте, что корнем сайта указана именно публичная папка (для типовых фреймворков —
public/), а не каталог проекта целиком. Это одна настройка в панели хостинга, и она закрывает сразу половину описанных проблем.
Если каталог был открыт: закрыть доступ — это только половина
Самая частая и самая дорогая ошибка на этом шаге — остановиться. «Правило добавили, теперь 404, вопрос решён». Не решён.
Пока каталог был доступен, содержимое мог скачать кто угодно, и следов это почти не оставляет: обычный запрос к обычному файлу, ничего подозрительного в логах. Автоматические переборщики ходят по чужим сайтам постоянно и складывают найденное впрок — использовать добытые ключи могут через недели после того, как вы всё закрыли. У вас нет способа доказать, что копию никто не забрал. Значит, исходить нужно из того, что забрали.
Все секреты, которые лежали в коде и в истории, считаются скомпрометированными и подлежат перевыпуску. Практический список:
- Пароль базы данных — сменить, обновить в конфигурации приложения. Заодно проверьте, доступна ли база снаружи: если да, закрыть.
- Ключи платёжного шлюза — перевыпустить в личном кабинете эквайринга. Утёкший ключ — это не только чужие операции, но и доступ к данным ваших платежей.
- Ключи внешних сервисов и API — доставка, СМС, карты, CRM, склад, любые интеграции. Отзывать по списку, а не «те, что кажутся важными».
- Токены почтовых рассылок — с ними отправляют письма от вашего имени и с вашей репутацией домена, а получают их ваши клиенты.
- Секрет сессий и ключ шифрования приложения — со старым секретом можно подделать сессионную куку и войти под чужой учётной записью, не зная пароля. Учтите: смена секрета разлогинит всех пользователей — это неприятно, но это правильная цена.
- Ключи доступа к самому репозиторию, если они были в
.git/config, и любые SSH-ключи, попавшие в проект. - Пароли администраторов сайта — сменить, а заодно просмотреть список учётных записей: не появилось ли новых.
Перевыпуск ключей — работа на несколько часов и почти всегда с простоем интеграций.
Соблазн решить, что «наверное, никто не успел», очень велик. Но цена ошибки несимметрична:
перевыпустить ключи впустую — потерянный вечер, не перевыпустить вовремя — списания по
вашему платёжному ключу и рассылка от вашего имени через месяц, когда связь с открытым
.git уже никто не установит.
Как не допустить повторения
- Внесите
.envи подобные файлы в.gitignore— тогда они физически не попадут в репозиторий. Рядом держите.env.exampleс теми же именами настроек, но с пустыми значениями: новый разработчик поймёт, что нужно заполнить, а секрета в истории не появится. - Секреты — в переменных окружения или в хранилище хостинга, не в файлах кода. Правило простое: если значение нельзя показать постороннему, оно не должно попадать в репозиторий даже на минуту.
- Разные ключи для тестовой и боевой среды. Тестовый контур почти всегда защищён хуже, и именно из него ключи утекают чаще. Если ключи одинаковые, утечка из теста — это утечка с боевого сайта.
- Проверяйте после каждого переезда и каждой выкладки вручную. Смена хостинга, подрядчика, перенос на новый сервер — три ситуации, в которых
.gitвозвращается на место чаще всего. Проверка занимает минуту. - Поставьте регулярную внешнюю проверку. Ручная проверка хороша ровно один раз: она ничего не скажет о файле, который появится в корне через полгода после спешного релиза в пятницу. Смысл имеет только повторяющийся контроль.
- Если один такой файл нашёлся — ищите остальные. Причина у них общая, поэтому по одному они встречаются редко.
Проверьте свой сайт бесплатно
ShieldSafe ищет открытые снаружи служебные файлы — .git, .env,
забытые дампы и отладочные страницы, — а также известные уязвимости в используемых
версиях компонентов. Каждая находка объясняется простым языком, с готовой формулировкой
для разработчика. Устанавливать на сервер ничего не нужно.