Обхід шляху - назва файлу від користувача містить ../ і виводить за межі дозволеного каталогу:
// вразливо
Route::get('/download', function (Request $request) {
return response()->download(storage_path('app/invoices/' . $request->query('file')));
});
// file=../../.env → storage/app/invoices/../../.env → .env застосунку
Так читають .env (ключі, паролі бази), конфігурацію, вихідний код, системні файли. У поєднанні із записом файлів (розпакування архівів, збереження за назвою від користувача) - перезапис коду й виконання команд.
Варіанти атаки, які наївна перевірка пропускає:
- закодовані послідовності:
%2e%2e%2f, подвійне кодування%252e; - зворотні слеші на Windows:
..\..\; - абсолютні шляхи:
/etc/passwd; - Zip Slip: архів з файлом
../../public/shell.php- під час розпакування він опиниться поза цільовим каталогом.
Захист:
1. Не приймати шлях від користувача взагалі. Найкраще - ідентифікатор запису в базі, а шлях до файлу береться з бази:
Route::get('/invoices/{invoice}/download', function (Invoice $invoice) {
Gate::authorize('view', $invoice);
return Storage::disk('invoices')->download($invoice->path);
});
Заодно це вирішує й авторизацію: користувач отримує лише свій файл.
2. Якщо назва з запиту неминуча - лише ім'я файлу без каталогів і білий список символів:
$name = basename($request->query('file'));
abort_unless(preg_match('/^[a-z0-9_-]+\.pdf$/i', $name), 404);
3. Перевірка канонічного шляху:
$base = realpath(storage_path('app/invoices'));
$path = realpath($base . DIRECTORY_SEPARATOR . $name);
abort_unless($path !== false && str_starts_with($path, $base . DIRECTORY_SEPARATOR), 404);
4. Розпакування архівів - перевіряти кожен шлях усередині до запису.
Laravel Storage нормалізує шляхи й не дає вийти за корінь диска через ../ (Flysystem кидає виняток для шляхів за межами кореня). Але це не замінює перевірку прав: файл іншого користувача всередині того самого диска - вже не path traversal, а помилка авторизації.