<? phpukraine СПІВБЕСІДИ
⌕Пошук по платформі
БЕЗПЕКА · JUNIOR

Як секрети витікають з працюючого застосунку і чому `APP_DEBUG=true` на продакшені небезпечний?

Git далеко не єдиний шлях, яким течуть секрети. `APP_DEBUG=true` показує будь-кому, хто спричинить помилку, заголовки запиту з cookies, тіло запиту з паролем, SQL з підставленими значеннями і код. Корінь сайту, налаштований на теку проєкту замість `public/`, віддає `.env` і `.git`. Логи і трекери помилок зберігають усе, що їм передали, а змінні з префіксом `VITE_` потрапляють у JavaScript-бандл. Захист: `APP_DEBUG=false`, корінь лише `public/`, явний перелік полів у логах, `#[\SensitiveParameter]` і жодних секретів у фронтенді.

Що побачить відвідувач, якщо на продакшені стоїть `APP_DEBUG=true` і щось упаде?
За адресою `https://example.com/.env` віддається файл з паролями. Як таке можливо в Laravel-проєкті?
Що не так з рядком `Log::info('Login attempt', $request->all())`?
Чому змінну `VITE_STRIPE_SECRET` не можна класти в `.env`?
APP_DEBUG .env секрети логи SensitiveParameter конфігурація

Розмова про секрети зазвичай зводиться до «не комітьте .env», і про це є окрема картка. Але в живому застосунку секрет найчастіше тече іншими шляхами, і перший з них це режим налагодження. З APP_DEBUG=true Laravel 13 на будь-який виняток віддає сторінку зі стеком і фрагментами коду, заголовками запиту, включно з Cookie і Authorization, тілом запиту у вигляді JSON і всіма SQL-запитами з уже підставленими значеннями. Впала форма входу, і пароль користувача видно на сторінці. Спричинити помилку зовні зазвичай нескладно: неочікуваний тип у параметрі, задовгий рядок, масив замість числа.

Другий шлях через вебсервер. У Laravel корінь сайту має вказувати на public/, де лежить лише index.php і статика. Якщо nginx налаштований на теку проєкту, https://сайт/.env віддає файл з усіма паролями, а /.git/ дозволяє відновити код разом з історією, зокрема секрети, які колись видалили комітом. .gitignore тут нічого не вирішує, він стосується лише git. Сканери перевіряють ці адреси на кожному новому домені, тож «ніхто не знає» не працює. Той самий корінь захищає storage/logs і bootstrap/cache/config.php, куди config:cache записує всі значення відкритим текстом.

Третій шлях: логи, трекери помилок і інструменти налагодження. Log::info('...', $request->all()) записує пароль у файл, який читає вся команда і сервіс збору логів. Стек-трейси містять аргументи функцій, тому в PHP 8.2+ параметри з паролями й ключами позначають #[\SensitiveParameter], і замість значення в трейсі опиняється SensitiveParameterValue. На продакшені лишають zend.exception_ignore_args=On, налаштовують очищення полів у Sentry, а Telescope і Debugbar або не вмикають, або закривають авторизацією. phpinfo() показує всі змінні оточення, тому маршрут з ним не має існувати.

Четвертий шлях на фронтенді. Vite вшиває в JavaScript-бандл змінні з префіксом VITE_, до яких звертається клієнтський код, тож туди можна класти лише те, що й так публічне: ключ для клієнтської частини платіжки, ідентифікатор аналітики. Секретний ключ, токен API з правами запису чи пароль після збірки лежить у файлі, який завантажує кожен відвідувач. І для всіх каналів одне правило реагування: секрет, що витік, відкликають і замінюють, а не просто прибирають з місця витоку.

// .env на продакшені
// APP_ENV=production
// APP_DEBUG=false  <- інакше помилка покаже заголовки, тіло запиту і SQL

// nginx: корінь лише public/, тоді /.env і /.git недоступні з вебу
// root /var/www/app/public;

// Було: у лог іде все, що прислав клієнт, разом із паролем.
Log::info('Login attempt', $request->all());

// Стало: явний перелік полів.
Log::info('Login attempt', ['email' => $request->input('email'), 'ip' => $request->ip()]);

// PHP 8.2+: значення не потрапить у стек-трейс і в трекер помилок.
function connect(string $dsn, #[\SensitiveParameter] string $password): PDO
{
    return new PDO($dsn, 'app', $password);
}
// У трейсі: connect('mysql:host=db', Object(SensitiveParameterValue))

// Фронтенд: усе з VITE_ публічне. Тут лише публічні ключі.
// VITE_STRIPE_PUBLISHABLE_KEY=pk_live_...   (можна)
// VITE_STRIPE_SECRET=sk_live_...            (ніколи)
Що debug-сторінка Laravel 13 показує стек, а разом з ним заголовки запиту, тіло запиту і SQL-запити з підставленими біндінгами.
Що `.env` і `.git` захищає не `.gitignore`, а корінь вебсервера: у Laravel він має вказувати лише на `public/`.
Що логи, Telescope, Sentry і стек-трейси є такими самими сховищами секретів, як база, тільки з ширшим доступом.
Що в PHP 8.2+ `#[\SensitiveParameter]` ховає значення параметра в стек-трейсі, а `zend.exception_ignore_args=On` прибирає аргументи взагалі.
Що все з префіксом `VITE_` вшивається в публічний JavaScript, і секрет там перестає бути секретом.
Увімкнути `APP_DEBUG=true` на продакшені «на пʼять хвилин», щоб подивитися помилку, і забути вимкнути: для цього є логи і трекер помилок.
Налаштувати корінь nginx на теку проєкту, а не на `public/`, і покладатися на `.htaccess` чи на те, що «ніхто не знає адресу»: `.env` і `.git/config` шукають сканери за кілька хвилин після появи домену.
Логувати `$request->all()` чи весь масив `$credentials`: пароль, токен картки і одноразовий код лягають у файл, який читає вся команда і сервіс збору логів.
Класти секретний ключ платіжки чи токен API в змінну `VITE_...`, бо «з фронтенду так зручніше викликати»: він опиниться у файлі `assets/app-*.js` для кожного відвідувача.
Після витоку ключа лише прибрати його з місця витоку: ключ треба відкликати і замінити, бо його вже могли скопіювати.
ПОРАДА

Назвіть канали витоку, які не повʼязані з git: сторінка помилки, корінь вебсервера, логи і трекери, фронтенд-бандл, `phpinfo()`. Інтервʼюер зазвичай чекає «не комітити `.env`», і перелік живих каналів одразу показує практичний досвід.

оновлено 8 жовтня 2026 · ліцензія CC-BY-SA-4.0 Знайшли неточність? Напишіть →
ПЕРЕВІРТЕ СЕБЕ

Рендерер винятків Laravel 13 виводить заголовки, тіло запиту і SQL з підставленими значеннями. `.gitignore` впливає лише на git, а доступ з вебу визначає корінь сервера. Vite вшиває змінні з `VITE_` у клієнтський код. `#[\SensitiveParameter]` нічого не шифрує, він лише замінює значення на `SensitiveParameterValue` у трасуванні стека.