1. 1
  2. 2
  3. 3
  4. 4
  5. 5

# теория · шаг 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
↳  выводfor p in /.env /.git/HEAD /dump.sql /files/; do ...; done (голый конфиг)
/.env        DB_PASS=secret
/.git/HEAD   ref: refs/heads/main
/dump.sql    backup
/files/      <html> ... (список файлов при autoindex)

Всё. Пароль к базе, служебные файлы репозитория, дамп и листинг каталога. nginx не виноват: его попросили отдавать каталог, он и отдаёт - в нём нет понятия «служебный файл».

На что это похоже

Переезд в новый офис: коробки внесли в приёмную и открыли двери для клиентов. В коробках вперемешку каталог продукции, бухгалтерия и связка запасных ключей. Никто не прятал бухгалтерию специально - просто выставили всё, что привезли.

Каталог развёртывания устроен так же: в нём лежит и то, что показывают, и то, чем это собрали.

Механизм: что попадает в каталог сайта

/var/www/shop index.html, app.js, style.css .env - пароли к базе .git/ - вся история кода dump.sql, config.php.bak nginx отдаёт всё, что найдёт по пути понятия «служебный файл» у него нет сканеры перебирают этот список за секунды - он одинаков у всех
Список известен наизусть и нападающему, и тебе. Разница в том, кто проверил первым.

Правило

/etc/nginx/conf.d/shop.local.confhttp server
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:

/etc/nginx/conf.d/shop.local.confhttp server
location ^~ /.well-known/acme-challenge/ { root /var/www/certbot; }
location ~ /\.                           { deny all; }

Модификатор ^~ отменяет проверку регулярок (четвёртый раздел), поэтому запрещающий блок до челленджа не доберётся. Без этой пары продление сертификата сломается - и разбор будет долгим, потому что причина в блоке, который писали ради безопасности.

Разбор: листинг каталога

/etc/nginx/conf.d/shop.local.confhttp server location /files/
autoindex on;

Директива по умолчанию off, и включают её осознанно - обычно для внутреннего файлового обмена. Беда в том, что «внутренний» со временем становится публичным: каталог остаётся, ограничение по адресу теряется при переезде, а список файлов рассказывает и про имена, и про размеры, и про даты.

Если листинг нужен, его закрывают вместе с каталогом:

/etc/nginx/conf.d/shop.local.confhttp server location /files/
autoindex on;
allow 192.168.1.0/24;
deny  all;

Отдельная тонкость: каталог без index.html и без autoindex отдаёт 403, а не 404. По этому 403 сканер понимает, что каталог существует.

Разбор: что рассказывает о себе сервер

↳  выводстрока Server из curl -sI: сверху умолчание, снизу с server_tokens off
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 в том же каталоге.

Главное

В каталоге сайта рядом с публичными файлами лежат служебные, и без запрета nginx отдаёт их как обычную статику - замер даёт 200 с содержимым .env, .git/HEAD и dump.sql. Закрывают их двумя блоками: location ~ /\. и запрет по расширениям, не забыв исключение ^~ /.well-known/acme-challenge/. autoindex включают осознанно и вместе с ограничением по адресу; server_tokens off убирает версию, но не слово nginx. Заголовки безопасности проверяют на нескольких маршрутах - свой add_header в дочернем блоке уносит их целиком. А найденную утечку считают состоявшейся: пароли меняют, ключи перевыпускают.

Комментарии

Пока нет комментариев. Будь первым!

Оставить комментарий

Комментарий появится после проверки. Email не публикуется. Войти, чтобы не вводить имя каждый раз.