Middle: питання на співбесіді з теми «Безпека»
Питання з реальних співбесід з відповідями: Laravel і PHP, бази даних, JavaScript і фронтенд, Git, Docker, API, безпека й архітектура. Тими самими темами, що й тести.
4 питання
Signed URL - посилання з криптографічним підписом у query-рядку. Якщо хтось змінить URL, підпис стане недійсним - підробка неможлива без APP_KEY.
// згенерувати
URL::signedRoute('unsubscribe', ['user' => $id]);
URL::temporarySignedRoute('download', now()->addMinutes(30), ['file' => $id]);
// перевірити (middleware або вручну)
Route::get('/download/{file}', ...)->middleware('signed');
Застосування: посилання-відписки в листах, тимчасові посилання на скачування, підтвердження email - там, де потрібен захищений доступ без автентифікації. temporarySignedRoute додає термін дії.
APP_KEY - ключ, яким Laravel шифрує й підписує дані.
Що від нього залежить:
- шифровані cookie, зокрема сесійна;
Crypt::encrypt()і кастencryptedу моделях;- підписані URL (
URL::signedRoute()); - зашифровані завдання черги (
ShouldBeEncrypted).
Паролі від нього не залежать - вони хешуються bcrypt чи argon2, а не шифруються.
Якщо ключ змінити: усі сесії завершаться (cookie не розшифруються), підписані посилання перестануть працювати, а зашифровані колонки в базі стане неможливо прочитати.
Як змінити без цього: старий ключ переносять у APP_PREVIOUS_KEYS. Laravel шифрує новим, а розшифровує по черзі новим і попередніми.
Якщо ключ витік - зловмисник може підробляти cookie й підписані URL, розшифрувати дані з бази. Тому:
- згенерувати новий ключ, старий - тимчасово в
APP_PREVIOUS_KEYS; - перешифрувати дані з кастом
encryptedновим ключем; - прибрати старий ключ з
APP_PREVIOUS_KEYS.
Ключ не комітять у репозиторій і не логують; для .env у репозиторії є php artisan env:encrypt.
Завантаження файлів - одна з найчастіших дір: файл може виявитися скриптом, перезаписати чужий файл чи покласти сервер розміром.
Валідація:
$request->validate([
'avatar' => ['required', File::image()->max('2mb')->dimensions(Rule::dimensions()->maxWidth(4000))],
'contract' => ['required', 'file', 'mimes:pdf', 'max:10240'],
]);
mimesвизначає тип за вмістом, а не за назвою;extensionsперевіряє розширення, яке дав користувач - корисно разом зmimes;max- у кілобайтах; ліміт розміру тіла запиту ще й на вебсервері.
Збереження:
- ім'я -
hashName()(випадкове) і розширення -extension()(за вмістом).getClientOriginalName()можна підробити, зокрема з../у шляху; - приватні документи - на приватний диск, віддавати через контролер з перевіркою прав чи
temporaryUrl(); - публічний диск ніколи не має виконувати PHP: файли віддає вебсервер як статику.
Обробка:
- зображення перекодувати (Laravel 13:
$request->image('avatar')->toWebp()) - нове кодування зазвичай відкидає вбудований у файл сторонній вміст і метадані на кшталт геолокації (результат варто перевірити для свого драйвера); - великі файли обробляти в черзі.
Оригінальне ім'я файлу, якщо воно потрібне людям, зберігають окремо в базі й екранують при виводі.
Шифрування в Laravel - симетричне, з ключем APP_KEY, алгоритм за замовчуванням AES-256-CBC з підписом HMAC (чи AES-GCM з вбудованою автентифікацією). Зашифроване значення не можна ні прочитати, ні непомітно змінити без ключа.
use Illuminate\Support\Facades\Crypt;
$token = Crypt::encryptString($apiToken);
$apiToken = Crypt::decryptString($token);
encryptString чи encrypt:
encryptString()/decryptString()- шифрує рядок як є;encrypt()/decrypt()(і хелпериencrypt(),decrypt()) спершу серіалізують значення черезserialize()- можна шифрувати масиви й об'єкти, але при розшифруванні викликаєтьсяunserialize(). Для рядків кращеencryptString.
Результат - base64 від JSON з полями iv, value, mac (чи tag для GCM). Він довший за вихідний рядок: для колонки бази потрібен text, а не короткий varchar.
Помилки розшифрування:
try {
$value = Crypt::decryptString($payload);
} catch (DecryptException $e) {
// значення пошкоджено, підроблено чи зашифровано іншим ключем
}
Ротація ключа. Якщо просто замінити APP_KEY, усе зашифроване старим ключем стане нечитабельним: cookie, сесії, поля з cast encrypted, збережені токени. Для плавної заміни:
APP_KEY="base64:NEW..."
APP_PREVIOUS_KEYS="base64:OLD..."
- шифрування - завжди новим ключем;
- розшифрування - спершу новим, потім по черзі старими з
APP_PREVIOUS_KEYS; - користувачі не розлогінюються, дані читаються.
Але старі ключі треба колись прибрати: дані в базі, зашифровані старим ключем, лишаються такими, доки їх не перезаписати. Після ротації - команда чи задача, що читає й зберігає ці записи заново (вони перешифровуються новим ключем), і лише потім видалення старого ключа з APP_PREVIOUS_KEYS.
Що не шифрувати через Crypt:
- паролі - їх хешують (
Hash::make), а не шифрують: розшифрувати пароль неможливо має бути навіть для вас; - поля, за якими шукаєте: зашифроване значення щоразу різне (випадковий IV), тому
where('email', ...)не спрацює. Для пошуку - окрема колонка з хешем (HMAC) значення.
Ключ - єдиний секрет усієї схеми. Витік APP_KEY разом з дампом бази - це витік усіх зашифрованих даних.