# теория · шаг 2 из 5
Что утекает из корня сайта
Коротко
Каталог сайта - это не только то, что ты собирался показывать. Рядом лежат служебные файлы, и без двух строк они отдаются как обычная статика.
- Замер на голом конфиге:
.env,.git/HEADиdump.sql- все 200 с содержимым. С двумя запрещающими блоками - все 403. autoindex onв каталоге без индекса выкладывает список файлов целиком.server_tokens offубирает версию, но не словоnginx.- Проверяется всё это одним списком curl - и делать это надо на чужих конфигах, которые принимаешь в поддержку.
Если помнишь про location ~ /\. и знаешь, что именно скрывает server_tokens - листай до «Теперь сам».
Сначала ответь сам
Сайт выложен из репозитория: git clone, настроили nginx, root указывает на
каталог проекта. Что можно скачать снаружи?
for p in /.env /.git/HEAD /dump.sql /files/; do printf '%-12s ' $p; curl -s http://shop.local$p | head -1; done
/.env DB_PASS=secret
/.git/HEAD ref: refs/heads/main
/dump.sql backup
/files/ <html> ... (список файлов при autoindex)
Всё. Пароль к базе, служебные файлы репозитория, дамп и листинг каталога. nginx не виноват: его попросили отдавать каталог, он и отдаёт - в нём нет понятия «служебный файл».
На что это похоже
Переезд в новый офис: коробки внесли в приёмную и открыли двери для клиентов. В коробках вперемешку каталог продукции, бухгалтерия и связка запасных ключей. Никто не прятал бухгалтерию специально - просто выставили всё, что привезли.
Каталог развёртывания устроен так же: в нём лежит и то, что показывают, и то, чем это собрали.
Механизм: что попадает в каталог сайта
Правило
location ~ /\. { deny all; }
location ~* \.(bak|old|sql|log|ini|sh|swp)$ { deny all; }
for p in /.env /.git/HEAD /dump.sql /; do printf '%-12s ' $p; curl -s -o /dev/null -w '%{http_code}\n' http://shop.local$p; done
/.env 403
/.git/HEAD 403
/dump.sql 403
/ 200
/files/ эти два блока не закрывают - листинг каталога отключают отдельно,
об этом ниже. Первая строка закрывает всё, что начинается с точки в любом сегменте пути:
.env, .git, .htaccess, редакторские .swp. Вторая - резервные копии и
дампы по расширению.
Исключение из первой строки понадобится ровно одно - для ACME:
location ^~ /.well-known/acme-challenge/ { root /var/www/certbot; }
location ~ /\. { deny all; }
Модификатор ^~ отменяет проверку регулярок (четвёртый раздел), поэтому
запрещающий блок до челленджа не доберётся. Без этой пары продление сертификата
сломается - и разбор будет долгим, потому что причина в блоке, который писали
ради безопасности.
Разбор: листинг каталога
autoindex on;
Директива по умолчанию off, и включают её осознанно - обычно для внутреннего
файлового обмена. Беда в том, что «внутренний» со временем становится публичным:
каталог остаётся, ограничение по адресу теряется при переезде, а список файлов
рассказывает и про имена, и про размеры, и про даты.
Если листинг нужен, его закрывают вместе с каталогом:
autoindex on;
allow 192.168.1.0/24;
deny all;
Отдельная тонкость: каталог без index.html и без autoindex отдаёт 403, а
не 404. По этому 403 сканер понимает, что каталог существует.
Разбор: что рассказывает о себе сервер
Server: nginx/1.31.5
Server: nginx
server_tokens off убирает версию, но не слово nginx: полностью скрыть
продукт штатными средствами нельзя (для этого нужен сторонний модуль или
пересборка). Смысл настройки скромный: сканер всё равно опознает сервер по
поведению, но массовый перебор «уязвимых версий по баннеру» мимо тебя пройдёт.
Заодно версия исчезает и со страниц ошибок - там она печатается в подвале.
Что ломается без этого
Заголовки безопасности, которых «нет». X-Frame-Options,
X-Content-Type-Options, Content-Security-Policy ставят через add_header -
и они пропадают в любом дочернем location, где есть свой add_header (правило
из второго раздела). Классическая картина: заголовки стоят на уровне server,
проверяются на главной, а на /api/ их нет.
for p in / /api/health /assets/app.js; do
echo "$p: $(curl -sI https://shop.local$p | grep -ci x-content-type-options)"
done
Три единицы - хорошо. Ноль хотя бы в одной строке - ищи add_header уровнем
ниже.
Служебное, доехавшее до интернета. stub_status, панели метрик, /debug
фреймворка, страница профайлера. Всё это закрывают allow/deny или
auth_basic, а лучше - вообще не публикуют на том же адресе.
Зачем это в работе
Список проверок для чужого конфига, который принимаешь в поддержку. Занимает минуту, а находит почти всегда:
for p in /.env /.git/HEAD /.git/config /dump.sql /config.php.bak \
/uploads../etc/passwd /files/ /server-status /stub_status; do
printf '%-28s %s\n' "$p" "$(curl -s -o /dev/null -w '%{http_code}' https://shop.local$p)"
done
Любая двухсотка в этом списке - находка. Дальше по важности: .env и .git
означают компрометацию (пароли и весь код), дамп - утечку данных, листинг -
разведку.
И помни про порядок действий после находки: сначала закрыть, потом считать
утёкшим всё, что было доступно, - менять пароли и перевыпускать ключи. Файл,
который отдавался месяц, наверняка уже скачан: .git собирают автоматом
готовыми утилитами.
Теперь сам
Закрыли .env и .git двумя строками, проверили - 403. Через неделю выясняется,
что кто-то скачал репозиторий целиком. Как это возможно?
Скорее всего, его скачали до того, как поставили запрет: 403 сегодня ничего
не говорит о вчера. Проверить можно по access_log - искать запросы к
/.git/ за прошлые месяцы; сканеры ходят по ним постоянно, и в логе это видно.
Второй вариант - копия репозитория лежит по другому адресу: в бэкапе, в
поддомене со старой версией сайта, в архиве site.zip в том же каталоге.
Главное
В корне сайта должны находиться только предназначенные для публикации файлы. Запреты на скрытые пути и резервные копии проверяй запросами, включая исключение для ACME. Если секрет уже был доступен, закрытие маршрута прекращает выдачу, но не отменяет необходимость заменить раскрытые данные.
Комментарии
Пока нет комментариев. Будь первым!
Оставить комментарий