Руководства по безопасности

Оставили файл с секретом в публичной директории? Проверьте свой webroot

Файл с токеном, .env или резервная копия в webroot скачивается по URL. Почему общий шаблон копирует его на каждый сайт и как проверить сервер и закрыть утечку.

Опубликовано 2026-06-11 Обновлено 2026-10-06 14 мин чтения
На этой странице

Для всех, кто держит несколько сайтов на общем/арендном хостинге или VPS, развёртывая в публичные директории (public/, public_html/, htdocs/ и т. д.). Без шагов атаки — только аудит своего сервера и закрытие дыр. О защите самого .env см. защита .env на общем хостинге.

Взгляд этого сайта: где вы это кладёте — ЭТО и есть контроль доступа

Секреты утекают больше из-за ошибок размещения, чем из-за хитрых атак. Webroot — это «место, откуда любой может скачать по URL». Положите секрет туда — и даже сильнейший пароль сервера бессмыслен. Базовое правило этого сайта — держать секреты вне и webroot, и git (→ безопасное хранение секретов). Идея проста: кладите в публичную директорию только то, что не жалко показать кому угодно.

public/ = скачивается любым через URL

Можно класть

изображения, CSS, JS — публичные ассеты

Никогда не класть

.env, token.json, .sql, .git, .bak

Выше корня приложения = не раздаётся

Кладите сюда

секреты, токены, учётные данные, резервные копии. Права 600 (чтение только владельцем). Недостижимо по URL.

Файлы в публичной директории любой может скачать по URL. Держите секреты вне её (не раздаваемыми, с правами 600).

Почему случаются «забытые файлы в публичной директории»

Распространённый паттерн — файл, положенный для удобства, или приехавший внутри заготовки, тихо остающийся публичным.

один URL
файл в webroot скачает любой
годы
он лежит незамеченным
каждый сайт
забытые файлы из шаблона попадают на каждый сайт
отозвать сейчас
считайте, что утёк, и ротируйте

Например, 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-ключи.

Аудит своего сервера

Сначала механически выявите «любые похожие на секрет файлы в моей собственной публичной директории». Это инспекция своих собственных активов, а не подглядывание в чужой сайт.

1

Поищите под публичными директориями похожие на секрет файлы

По всему своему домашнему каталогу выявите кандидаты-секреты, проскользнувшие в public/ (JSON с токеном/учётными данными, .env, ключи, временные файлы).
2

Исходите из того, что тот же файл есть и в других местах, — проверьте каждый хост и сайт

Сайты, построенные из той же заготовки, имеют то же самое в том же месте; прогоните ту же проверку по всем хостам и сайтам. Одно совпадение обычно означает, что есть и другие.
3

Решите по каждому файлу, действительно ли он должен быть публичным

Рассортируйте каждый результат на «безопасно публиковать» vs «секрет». Секреты проходят шаги удалить/отозвать/перенести ниже.
# 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 на общем хостинге.

1

Ищите также резервные копии и архивы

Названия публичных директорий зависят от хостинга, поэтому включите 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 и по каждому файлу убедитесь, что он действительно должен быть публичным.

2

Убедитесь, что листинг директорий выключен

Откройте в браузере директорию без index.html (например, /uploads/). Если видите список имён файлов, функция листинга директорий веб-сервера включена. Листинг позволяет находить забытые файлы, не зная их имён. Нужный результат — 403 или 404.

3

Поищите секреты в собранном JavaScript

Если вы собираете фронтенд, поищите в выходной папке, которую развёртываете (dist/, .next/static/ и т. д.). Используйте что-то вроде grep -rlE 'BEGIN [A-Z ]*PRIVATE KEY|secret|password' dist/, плюс префикс, который ваш сервис использует для ключей. Если что-то нашлось, проверьте, не имеет ли переменная имени для браузера (VITE_, NEXT_PUBLIC_).

4

Убедитесь, что секреты вне webroot не читаются другими пользователями

Вынос секрета из webroot не поможет, если из-за слабых прав его может прочитать другая учётная запись на том же сервере. find ~ -maxdepth 3 \( -name '.env*' -o -name '*.pem' -o -name '*token*.json' \) -perm /o+r 2>/dev/null выводит секреты, доступные для чтения кому-либо кроме владельца; исправьте их через chmod 600. Пустой вывод — всё в порядке.

5

Проверьте, не проиндексировали ли что-нибудь поисковые системы

Поищите в Google site:yourdomain и посмотрите, нет ли незнакомых файлов (.sql, .zip, .txt и т. д.). Если пользуетесь Search Console, отчёт «Страницы» показывает проиндексированные URL. Что делать, если что-то всё ещё в результатах поиска, — в разделе о частых ошибках ниже.

Что делать, когда найдёте один

Сделайте три вещи как набор — удалить, отозвать, перенести. Пропустите любую — и исправление будет неполным.

1

Удалить из webroot, чтобы URL не мог его скачать

Переместите файл из публичной директории или удалите его и убедитесь, что URL теперь возвращает 404.
2

Отозвать/переиздать любой ключ или токен, который мог утечь

Не оставляйте на «вероятно, нормально». Поскольку он был открыт, считайте, что утёк, и обновите или отзовите этот токен/ключ.
3

Держать секреты вне webroot с правами 600

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

По серверам: настройки, запрещающие раздачу таких файлов (как страховка)

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

КонфигурацияКак запретитьЛистинг директорийНа что обратить внимание
Apache (VPS)<FilesMatch> + Require all denied в блоке <Directory> или .htaccessВыключите через Options -IndexesВ Apache 2.4 AllowOverride по умолчанию None, поэтому .htaccess игнорируется, пока вы его не разрешите
Nginx (VPS)deny all; внутри locationautoindex по умолчанию 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. Мы проектируем само размещение как контроль доступа и прогоняем этот аудит в том же ритме, что и инвентаризацию реагирования на уязвимости.

Читать дальше

Источники

  • 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

FAQ

QПочему опасно класть файлы в публичную директорию?
A

Всё в публичной веб-директории (webroot — public/, public_html/ и т. д.) может скачать любой, кто обратится к её URL. Забытый JSON с токеном/учётными данными, .env, резервная копия или файл ключа извлекаются раньше, чем кто-то заметит, что ведёт к немедленному вреду. В публичной директории должно лежать только то, что безопасно делать публичным.

QЕсли нашёл один на сайте, проверять ли остальные?
A

Да. Если вы массово создаёте сайты из общего шаблона или заготовки, тот же забытый файл обычно скопирован на все. Найдите один и проверьте каждый сайт и хост того же происхождения.

QЧто делать, когда найду один?
A

1) Удалить файл из webroot, чтобы он возвращал 404 по URL. 2) Обновить/отозвать любой токен или ключ, который мог утечь (считайте, что утёк). 3) Впредь держать секреты вне webroot с правами 600 (чтение только владельцем). Сделайте все три как набор.

QКаким файлам никогда не место в публичной веб-директории?
A

Обычные подозреваемые — файлы конфигурации вроде .env, JSON-файлы с токенами или учётными данными, дампы баз данных (.sql), ZIP- или tar.gz-архивы всего сайта, директория .git и резервные копии, сделанные вручную или редактором (.bak, .old, .orig, .swp или имя, оканчивающееся на ~). Руководство OWASP по тестированию объясняет: если login.php хранится как login.php.old, сервер может не выполнить его как код и вместо этого вернуть исходный код простым текстом.

QЯ удалил файл, но он всё ещё виден в результатах поиска. Что делать?
A

Сначала убедитесь, что URL возвращает 404 или 410, затем через инструмент «Удаления» в Google Search Console временно скройте его из результатов. По данным справки Google, временное удаление действует около шести месяцев и рассчитано на сочетание с постоянным исправлением, например удалением файла. Японское агентство IPA также советует просить поисковые системы удалить кешированные копии. Отзовите любой ключ или токен из этого файла независимо от того, исчез ли он из результатов поиска.

QМожно ли класть секреты в переменные окружения фронтенда (VITE_ или NEXT_PUBLIC_)?
A

Нет. Vite встраивает переменные, начинающиеся с VITE_, а Next.js — переменные, начинающиеся с NEXT_PUBLIC_, в JavaScript, отправляемый в браузер при сборке. Это то же самое, что положить файл в публичную директорию: прочитать его может кто угодно. Держите API-ключи и другие секреты в переменных, которые использует только серверный код.