Для всех, кто держит несколько сайтов на общем/арендном хостинге или VPS, развёртывая в публичные директории (public/, public_html/, htdocs/ и т. д.). Без шагов атаки — только аудит своего сервера и закрытие дыр. О защите самого .env см. защита .env на общем хостинге.
Взгляд этого сайта: где вы это кладёте — ЭТО и есть контроль доступа
Секреты утекают больше из-за ошибок размещения, чем из-за хитрых атак. Webroot — это «место, откуда любой может скачать по URL». Положите секрет туда — и даже сильнейший пароль сервера бессмыслен. Базовое правило этого сайта — держать секреты вне и webroot, и git (→ безопасное хранение секретов). Идея проста: кладите в публичную директорию только то, что не жалко показать кому угодно.
public/ = скачивается любым через URL
Можно класть
изображения, CSS, JS — публичные ассеты
Никогда не класть
.env, token.json, .sql, .git, .bak
Выше корня приложения = не раздаётся
Кладите сюда
секреты, токены, учётные данные, резервные копии. Права 600 (чтение только владельцем). Недостижимо по URL.
Почему случаются «забытые файлы в публичной директории»
Распространённый паттерн — файл, положенный для удобства, или приехавший внутри заготовки, тихо остающийся публичным.
Например, JSON, хранящий токен доступа для сторонней интеграции, долго лежит под public/ — и если он из общего шаблона, тот же файл скопирован в то же место на многих сайтах, поэтому у них у всех одна и та же проблема. Исправить один сайт бессмысленно, если на остальных файл по-прежнему лежит. Так же опасны: забытый .env, резервные копии базы данных (.sql), директория .git, резервные копии редактора (*.bak) и временные файлы (связано: обход пути · весь .env наружу).
Какие файлы забывают и чем каждый опасен
«Файл с секретом» обычно вызывает в памяти .env, но на деле забывают гораздо больше. Руководство по тестированию от OWASP (некоммерческой организации, занимающейся веб-безопасностью) тоже называет старые версии файлов, резервные копии и файлы, на которые ничто не ссылается, среди того, что нужно проверять.
| Тип | Типичные имена | Чем рискуете | Где им место |
|---|---|---|---|
| Файлы конфигурации | .env, config.php.bak, settings.json | Пароли к базе данных и API-ключи, записанные открытым текстом | Выше корня приложения (не раздаётся) |
| Токены и учётные данные | token.json, credentials.json, *.pem, *.key | Прямой вход во внешний сервис или на другой сервер | Вне webroot, права 600 |
| Резервные копии, сделанные вручную или редактором | login.php.old, index.php~, .index.php.swp, *.orig | С изменённым расширением файл может не выполняться как код, и его исходник показывается простым текстом | Не создавайте их на продакшене; редактируйте локально |
| Дампы баз данных | backup.sql, dump.sql.gz | Персональные данные пользователей и хеши паролей, всё в одном файле | Отдельное место для резервных копий, лучше вне сервера |
| Архивы сайта | site.zip, www.tar.gz | Содержат .env и весь исходный код | Удаляйте сразу после распаковки |
| Данные системы контроля версий | .git/ | Можно восстановить секреты из прошлых коммитов | Вне публичной директории |
| Значения, встроенные при сборке | внутри assets/index-xxxx.js | Переменные, начинающиеся с VITE_ или NEXT_PUBLIC_, попадают в JavaScript для браузера | Держите секреты только в серверных переменных |
Последнюю строку легко упустить. Даже если вы никогда не клали файл, сборка может вытолкнуть секрет в публичную директорию. Официальная документация Vite прямо говорит, что переменные VITE_ не должны содержать чувствительную информацию, такую как API-ключи.
Аудит своего сервера
Сначала механически выявите «любые похожие на секрет файлы в моей собственной публичной директории». Это инспекция своих собственных активов, а не подглядывание в чужой сайт.
Поищите под публичными директориями похожие на секрет файлы
public/ (JSON с токеном/учётными данными, .env, ключи, временные файлы).Исходите из того, что тот же файл есть и в других местах, — проверьте каждый хост и сайт
Решите по каждому файлу, действительно ли он должен быть публичным
# audit under your own public directories (inspecting your own assets)
find ~ -path '*/public/*' \( -iname '*token*.json' -o -iname '*credential*.json' \
-o -iname '*.bak' -o -iname '*.sql' -o -iname '.env*' \) 2>/dev/null
# an exposed .git directory is dangerous too
find ~ -path '*/public/*/.git' -maxdepth 8 2>/dev/nullКак проверить свою среду
find выше — отправная точка. Пять проверок ниже ловят забытые файлы, которые один лишь поиск по имени пропускает. Выполняйте их только для своего сервера и своего домена. Проверки, специфичные для .env (посмотреть первые байты ответа, убедиться, что .htaccess действительно работает), описаны в статье защита .env на общем хостинге.
Ищите также резервные копии и архивы
Названия публичных директорий зависят от хостинга, поэтому включите public_html, htdocs и www. Например, выполните find ~/public_html \( -name '*~' -o -name '*.swp' -o -name '*.old' -o -name '*.orig' -o -name '*.zip' -o -name '*.tar.gz' -o -name '*.pem' -o -name '*.key' \) 2>/dev/null и по каждому файлу убедитесь, что он действительно должен быть публичным.
Убедитесь, что листинг директорий выключен
Откройте в браузере директорию без index.html (например, /uploads/). Если видите список имён файлов, функция листинга директорий веб-сервера включена. Листинг позволяет находить забытые файлы, не зная их имён. Нужный результат — 403 или 404.
Поищите секреты в собранном JavaScript
Если вы собираете фронтенд, поищите в выходной папке, которую развёртываете (dist/, .next/static/ и т. д.). Используйте что-то вроде grep -rlE 'BEGIN [A-Z ]*PRIVATE KEY|secret|password' dist/, плюс префикс, который ваш сервис использует для ключей. Если что-то нашлось, проверьте, не имеет ли переменная имени для браузера (VITE_, NEXT_PUBLIC_).
Убедитесь, что секреты вне webroot не читаются другими пользователями
Вынос секрета из webroot не поможет, если из-за слабых прав его может прочитать другая учётная запись на том же сервере. find ~ -maxdepth 3 \( -name '.env*' -o -name '*.pem' -o -name '*token*.json' \) -perm /o+r 2>/dev/null выводит секреты, доступные для чтения кому-либо кроме владельца; исправьте их через chmod 600. Пустой вывод — всё в порядке.
Проверьте, не проиндексировали ли что-нибудь поисковые системы
Поищите в Google site:yourdomain и посмотрите, нет ли незнакомых файлов (.sql, .zip, .txt и т. д.). Если пользуетесь Search Console, отчёт «Страницы» показывает проиндексированные URL. Что делать, если что-то всё ещё в результатах поиска, — в разделе о частых ошибках ниже.
Что делать, когда найдёте один
Сделайте три вещи как набор — удалить, отозвать, перенести. Пропустите любую — и исправление будет неполным.
Удалить из webroot, чтобы URL не мог его скачать
Отозвать/переиздать любой ключ или токен, который мог утечь
Держать секреты вне webroot с правами 600
По серверам: настройки, запрещающие раздачу таких файлов (как страховка)
Настоящее исправление — не класть туда файлы. Поверх этого добавьте на веб-сервере запрещающее правило, чтобы положенное по ошибке всё равно не раздавалось. Что можно использовать, зависит от вашей конфигурации.
| Конфигурация | Как запретить | Листинг директорий | На что обратить внимание |
|---|---|---|---|
| Apache (VPS) | <FilesMatch> + Require all denied в блоке <Directory> или .htaccess | Выключите через Options -Indexes | В Apache 2.4 AllowOverride по умолчанию None, поэтому .htaccess игнорируется, пока вы его не разрешите |
| Nginx (VPS) | deny all; внутри location | autoindex по умолчанию off | Не читает .htaccess. Блоки location с регулярными выражениями проверяются в порядке записи, и побеждает первое совпадение |
| Общий хостинг | Обычно .htaccess (совместим с Apache) | Через панель управления или .htaccess | Проверьте работу на фиктивном файле без секретов |
| Статический хостинг / CDN | Всё, что в выходной папке, публикуется как есть | Зависит от сервиса | Полагайтесь на то, что секретов нет в выходной папке, а не на запрещающие правила |
Пример для Apache (где .htaccess включён):
Options -Indexes
<FilesMatch "(^\.|~$|\.(bak|old|orig|swp|sql|zip|tar|gz|pem|key)$)">
Require all denied
</FilesMatch>Пример для Nginx (внутри блока server):
# Allow the path used for automatic certificate renewal (Let's Encrypt) first
location ^~ /.well-known/acme-challenge/ { }
# Deny files and directories starting with a dot (.env, .git, ...)
location ~ /\. { deny all; }
# Deny backups and archives
location ~* (~|\.(bak|old|orig|swp|sql|zip|tar|gz|pem|key))$ { deny all; }В Nginx, когда запрос совпадает с location, помеченным ^~, location с регулярными выражениями не проверяются. Так /.well-known/acme-challenge/, используемый для продления сертификата, по-прежнему раздаётся, несмотря на правило, запрещающее пути, начинающиеся с точки. После изменения конфигурации проверьте синтаксис через nginx -t, перезагрузите и убедитесь, что фиктивный файл возвращает 403. Если ваш сайт предлагает ZIP-файлы и подобное для скачивания, уберите это расширение из списка запрета.
Размещение, в которое попадают (опасно)
- JSON-токен для интеграции лежит в
public/ - файл-секрет, приехавший в заготовке, остаётся публичным
- резервные копии (
.sql/.bak) или.gitоставлены под публичной директорией - исправить один сайт и считать «решено»
Правильное размещение
- публичная директория держит только то, чем можно публично делиться
- секреты живут вне webroot с правами 600
- чувствительные расширения запрещены к раздаче
- найти один и проверить каждый хост и сайт
Сделайте «только публично-безопасное» постоянным правилом
Правила лучше разовых исправлений. Решите, что в публичной директории лежат только публично-безопасные файлы, и как правило никогда не кладите туда секреты, резервные копии, данные системы контроля версий (.git) или файлы конфигурации. Обновляя заготовку, уберите секрет из самого шаблона — иначе каждый сайт, который вы массово создадите дальше, продолжит копировать тот же файл с секретом.
Частые ошибки и как их исправить
- Полагаются на запрещающее правило и оставляют файл на месте: переезд сервера или переписывание конфигурации может потерять правило, и с этого дня файл публичен. Nginx вообще не читает
.htaccess. Считайте правило страховкой и выносите файл из публичной директории. - Редактируют файлы прямо на рабочем сервере: редакторы оставляют резервные копии, оканчивающиеся на
~или.swp. Руководство OWASP по тестированию тоже не советует редактировать файлы на месте на веб-сервере. Редактируйте локально и доставляйте изменения через развёртывание. - Копируют всю папку проекта в публичную директорию: вместе с ней уходят
.env,.gitи тестовые данные. Раздавайте только папку с результатом сборки и явно указывайте, что копируется. - Запрещают все пути с точкой и ломают продление сертификата: Let's Encrypt кладёт свой проверочный файл в
/.well-known/acme-challenge/. Как в примере для Nginx выше, сначала разрешите этот путь. - Кладут секреты в переменные
VITE_илиNEXT_PUBLIC_: они встраиваются в собранный JavaScript, и прочитать их может любой. Используйте их только в серверном коде и отзовите любой ключ, уже выпущенный таким образом. - Удалили файл и на этом остановились: если поисковая система его проиндексировала, он может задержаться в результатах. Убедитесь, что URL возвращает 404 или 410, затем временно (примерно на шесть месяцев) скройте его через инструмент «Удаления» в Google Search Console, пока повторное сканирование его не уберёт.
Реальный случай: скрытые персональные данные в опубликованном отчёте
Департамент финансирования здравоохранения (DHCF), который управляет Medicaid (государственным медицинским страхованием для людей с низким доходом) в Вашингтоне (округ Колумбия), сообщил, что два статистических отчёта, размещённые на его сайте с 2023 года до июля 2026 года, содержали исходные персональные данные в виде «скрытых полей». Отчёты были рассчитаны на показ только сводных цифр, но файлы по-прежнему хранили номера Medicaid получателей, даты рождения и другое. В уведомлении для Министерства здравоохранения и социальных служб США (HHS) число пострадавших указано как 399 086. DHCF узнал о проблеме 21 июля 2026 года и удалил отчёты.
Это не случай забытого файла с секретом, но корень тот же:
- Файл в публичной директории можно скачать целиком, а не только часть, показанную на экране
- Информацию, которую публикующий считает «невидимой», легче всего пропустить при проверке
- Она может оставаться опубликованной годами, прежде чем кто-то заметит
Шаг, который перед публикацией проверяет «что на самом деле внутри этого файла», предотвращает и забытые файлы, и скрытые данные. Полный разбор — в материале о раскрытии данных Medicaid в Вашингтоне (округ Колумбия) (на английском).
Что делает сам этот сайт
Базовое правило этого сайта — держать секреты — ключи, токены, строки подключения — вне и публичной директории, и репозитория кода. Артефакты развёртывания содержат только собранные публичные ассеты; секреты держатся как переменные окружения среды выполнения, в месте, которое не раздаётся. Причина: класс инцидента этой статьи — типичный случай, когда один забытый файл выдаёт всё любому, кто запросит его URL. Мы проектируем само размещение как контроль доступа и прогоняем этот аудит в том же ритме, что и инвентаризацию реагирования на уязвимости.
Читать дальше
- Стеки: защита .env на общем хостинге · руководство по реагированию на уязвимости (инвентаризация)
- Глоссарий: файлы .env и секреты · обход пути
- Инцидент: весь .env наружу
- Хранение: безопасное хранение секретов
- В облаке: публичные бакеты (на английском; та же проблема, но вне сервера)
- Когда взломан провайдер: что делать, если взломан ваш веб-хостинг (на английском; считайте, что всё, что там лежит, прочитано)
- Реальный случай: номера Medicaid, оставленные в опубликованных отчётах (Вашингтон, округ Колумбия) (на английском; опубликованный файл раскрыт целиком)
Источники
- IPA, «20 Checkpoints for Secure Website Operation and Management» (на японском; файлы, которые не должны быть публичными, кеш поисковых систем): ipa.go.jp
- OWASP, «Web Security Testing Guide: Review Old Backup and Unreferenced Files for Sensitive Information» (резервные копии .old, ~, .swp; не редактировать на месте на сервере): wstg.owasp.org
- Apache HTTP Server 2.4, «Upgrading to 2.4 from 2.2» (
AllowOverrideпо умолчаниюNone) и «mod_autoindex» (Options +Indexesсоздаёт листинги): httpd.apache.org, httpd.apache.org - nginx, «ngx_http_core_module: location» (порядок сопоставления и
^~) и «ngx_http_autoindex_module» (autoindexпо умолчаниюoff): nginx.org, nginx.org - Let's Encrypt, «Challenge Types» (HTTP-01 использует
/.well-known/acme-challenge/): letsencrypt.org - Vite, «Env Variables and Modes» (переменные
VITE_доступны клиентскому коду): vite.dev - Next.js, «Environment Variables» (переменные
NEXT_PUBLIC_встраиваются в JavaScript для браузера): nextjs.org - Справка Google Search Console, «Removals tool» (временное удаление действует около шести месяцев): support.google.com
- District of Columbia DHCF, «Notice of Data Incident»: dhcf.dc.gov; HIPAA Journal (число, сообщённое в HHS): hipaajournal.com