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 в том же каталоге.

Главное

В корне сайта должны находиться только предназначенные для публикации файлы. Запреты на скрытые пути и резервные копии проверяй запросами, включая исключение для ACME. Если секрет уже был доступен, закрытие маршрута прекращает выдачу, но не отменяет необходимость заменить раскрытые данные.

Комментарии

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

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

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