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