Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама

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 додає термін дії.

Докладніше в документації: Підписані URL

APP_KEY - ключ, яким Laravel шифрує й підписує дані.

Що від нього залежить:

  • шифровані cookie, зокрема сесійна;
  • Crypt::encrypt() і каст encrypted у моделях;
  • підписані URL (URL::signedRoute());
  • зашифровані завдання черги (ShouldBeEncrypted).

Паролі від нього не залежать - вони хешуються bcrypt чи argon2, а не шифруються.

Якщо ключ змінити: усі сесії завершаться (cookie не розшифруються), підписані посилання перестануть працювати, а зашифровані колонки в базі стане неможливо прочитати.

Як змінити без цього: старий ключ переносять у APP_PREVIOUS_KEYS. Laravel шифрує новим, а розшифровує по черзі новим і попередніми.

Якщо ключ витік - зловмисник може підробляти cookie й підписані URL, розшифрувати дані з бази. Тому:

  1. згенерувати новий ключ, старий - тимчасово в APP_PREVIOUS_KEYS;
  2. перешифрувати дані з кастом encrypted новим ключем;
  3. прибрати старий ключ з 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 разом з дампом бази - це витік усіх зашифрованих даних.

Докладніше в документації: Плавна ротація ключів шифрування