Статья по безопасности Laravel (Laravel Cheat Sheet)
Введение
Эта Статья даёт советы по безопасности для разработчиков на Laravel. Охвачены распространённые уязвимости и способы сделать приложение защищённым.
Laravel по умолчанию включает встроенные механизмы безопасности и рассчитан на безопасную конфигурацию «из коробки». При этом фреймворк даёт гибкость для сложных сценариев — и разработчик, не зная деталей, может использовать возможности небезопасно. Цель гида — помочь избежать типичных ошибок.
Основы
- В продакшене отключите режим отладки. В переменной окружения
APP_DEBUGукажитеfalse:
APP_DEBUG=false- Сгенерируйте ключ приложения. Он нужен для симметричного шифрования и SHA256: шифрование cookie, подписанные URL, токены сброса пароля, шифрование сессии. Команда:
php artisan key:generate-
Настройте PHP безопасно. См. статью по конфигурации PHP.
-
Выставьте безопасные права на файлы и каталоги: каталоги Laravel — не шире
775, неисполняемые файлы — не шире664, исполняемые (Artisan, скрипты деплоя) — не шире775. -
Следите за уязвимостями в зависимостях (
composer auditи обновления).
Безопасность cookie и управление сессией
По умолчанию Laravel настроен безопасно. Если меняете cookie или сессию, проверьте:
- Включите middleware шифрования cookie, если используете драйвер сессии
cookieили храните в cookie данные, которые клиент не должен читать или подменять. Обычно шифрование нужно включать. ДобавьтеEncryptCookiesв группуwebвApp\Http\Kernel:
/**
* Группы route-middleware приложения.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
...
],
...
];- Включите
HttpOnlyдля cookie сессии вconfig/session.php, чтобы JavaScript не читал cookie сессии:
'http_only' => true,- Если не используете маршруты по поддоменам, задайте
domainдля cookie вnull, чтобы cookie выставлялась только для того же origin (без поддоменов):
'domain' => null,- Атрибут
SameSite—laxилиstrictвconfig/session.php, чтобы ограничить контекст first-party / same-site:
'same_site' => 'lax',- Для приложения только по HTTPS установите
secureвconfig/session.phpвtrue(защита от MITM). Если есть и HTTP, и HTTPS, можноnull— атрибутSecureвыставится автоматически на HTTPS-запросах:
'secure' => null,- Задайте короткий таймаут простоя сессии. OWASP рекомендует 2–5 минут для высокоценных приложений и 15–30 минут для низкого риска. В
config/session.php:
'lifetime' => 15,Дополнительно: доклад о безопасности cookie.
Аутентификация
Guards и providers
В основе аутентификации Laravel — guards и providers. Guard определяет, как пользователь аутентифицируется на каждый запрос. Provider — как пользователь загружается из хранилища.
Из коробки: guard session (состояние через сессию и cookie) и guard token для API-токенов.
Providers: eloquent (Eloquent ORM) и database (query builder).
Настройка в config/auth.php; можно реализовать свои guards и providers.
Стартовые наборы
Официальные starter kits с готовой аутентификацией:
- Laravel Breeze — минимальный набор: вход, регистрация, сброс пароля, подтверждение email и пароля.
- Laravel Fortify — headless-бэкенд аутентификации, плюс двухфакторная аутентификация.
- Laravel Jetstream — UI поверх Fortify.
Рекомендуется использовать один из kits для устойчивой и проверенной схемы входа.
Пакеты API-аутентификации
Fortify и Jetstream поддерживают Sanctum из коробки.
Массовое присваивание (mass assignment)
Mass assignment — типичная проблема приложений с ORM, в том числе Eloquent.
Суть: через массовое заполнение модифицируются поля, которые пользователю менять нельзя.
Пример:
Route::any('/profile', function (Request $request) {
$request->user()->forceFill($request->all())->save();
$user = $request->user()->fresh();
return response()->json(compact('user'));
})->middleware('auth');Маршрут меняет профиль текущего пользователя. Но если в таблице users есть колонка is_admin, пользователь не должен менять её сам — приведённый код разрешает менять любые колонки строки: это уязвимость mass assignment.
Laravel по умолчанию защищает от этого. Соблюдайте:
- Разрешайте только нужные поля:
$request->only(...)или$request->validated(), а не$request->all(). - Не вызывайте
unguard()и не задавайте$guarded = []— это отключает встроенную защиту. - Избегайте
forceFill/forceCreateбез строго проверенного массива данных.
SQL-инъекции
SQL-инъекции остаются частой угрозой. Ниже — специфика Laravel; общие рекомендации — в статье по предотвращению SQL-инъекций.
Eloquent и защита от SQL-инъекций
Eloquent по умолчанию параметризует запросы и использует привязки. Пример:
use App\Models\User;
User::where('email', $email)->get();Фактически выполняется:
select * from `users` where `email` = ?Даже недоверенный $email не встраивается в SQL как текст запроса.
Сырые запросы
whereRaw и аналоги дают гибкость, но для данных из запроса обязательно используйте привязки. Небезопасно:
use Illuminate\Support\Facades\DB;
use App\Models\User;
User::whereRaw('email = "'.$request->input('email').'"')->get();
DB::table('users')->whereRaw('email = "'.$request->input('email').'"')->get();Исправление — плейсхолдеры:
use App\Models\User;
User::whereRaw('email = ?', [$request->input('email')])->get();Именованные привязки:
use App\Models\User;
User::whereRaw('email = :email', ['email' => $request->input('email')])->get();Имена колонок
Нельзя брать имена колонок из недоверенного ввода.
Потенциально опасно:
use App\Models\User;
User::where($request->input('colname'), 'somedata')->get();
User::query()->orderBy($request->input('sortBy'))->get();Частичная защита со стороны Laravel не заменяет валидацию: в ряде СУБД привязка имён колонок невозможна. В худшем случае это перерастает в mass assignment, если колонки не белые.
Валидируйте ввод, например:
use App\Models\User;
$request->validate(['sortBy' => 'in:price,updated_at']);
User::query()->orderBy($request->validated()['sortBy'])->get();Правила валидации и имена колонок
Некоторые правила принимают имя колонки — та же угроза, что и при динамических колонках в запросах. Пример:
use Illuminate\Validation\Rule;
$request->validate([
'id' => Rule::unique('users')->ignore($id, $request->input('colname'))
]);Упрощённо под капотом может получиться эквивалент с колонкой из ввода:
use App\Models\User;
$colname = $request->input('colname');
User::where($colname, $request->input('id'))->where($colname, '<>', $id)->count();Так как имя колонки задаёт пользователь, это тот же класс риска, что и при динамических именах колонок в запросах.
Межсайтовый скриптинг (XSS)
XSS — внедрение скриптов в доверенные страницы.
В Blade конструкция {{ }} экранирует вывод через htmlspecialchars.
Синтаксис {!! !!} выводит без экранирования — для недоверенных данных нельзя использовать.
Уязвимо:
{!! request()->input('somedata') !!}Безопасно:
{{ request()->input('somedata') }}Общие меры — в статье по предотвращению XSS.
Неконтролируемая загрузка файлов
Загрузка вредоносных файлов может привести к RCE или DoS. Дополнительно см. статью по загрузке файлов.
Тип и размер файла
Всегда проверяйте тип (расширение / MIME) и размер — против DoS по диску и RCE через исполняемые файлы:
$request->validate([
'photo' => 'file|size:100|mimes:jpg,bmp,png'
]);DoS: огромные файлы без лимита заполняют диск. RCE: загрузка PHP и вызов по URL (если файл публичен). Валидация снимает оба класса риска.
Не доверяйте пути и имени файла из ввода
Если путь или имя файла строятся из ввода пользователя, возможна перезапись критичных файлов или запись «чужой» директории.
Route::post('/upload', function (Request $request) {
$request->file('file')->storeAs(auth()->id(), $request->input('filename'));
return back();
});Имя вроде ../2/filename.pdf может положить файл в каталог другого пользователя. Используйте basename():
Route::post('/upload', function (Request $request) {
$request->file('file')->storeAs(auth()->id(), basename($request->input('filename')));
return back();
});ZIP и XML
XML открывает XXE, «billion laughs» и др. ZIP — zip bomb. См. XML Security и File Upload.
Обход каталога (path traversal)
Цель атаки — чтение файлов через ../ и вариации или абсолютные пути.
Небезопасная выдача файла по имени из запроса:
Route::get('/download', function(Request $request) {
return response()->download(storage_path('content/').$request->input('filename'));
});Имя ../../.env может утечь конфиг. Снова basename():
Route::get('/download', function(Request $request) {
return response()->download(storage_path('content/').basename($request->input('filename')));
});Открытое перенаправление
Само по себе часто низкий риск, но усиливает фишинг.
Route::get('/redirect', function (Request $request) {
return redirect($request->input('url'));
});Пользователь уходит на произвольный внешний URL. Такой же параметр в письмах сброса пароля может вести на поддельный сайт.
Межсайтовая подделка запроса (CSRF)
CSRF — нежелательное действие на доверенном сайте от имени авторизованного пользователя.
Laravel включает middleware VerifyCsrfToken. При подключении к группе web в App\Http\Kernel защита активна:
/**
* Группы route-middleware приложения.
*
* @var array
*/
protected $middlewareGroups = [
'web' => [
...
\App\Http\Middleware\VerifyCsrfToken::class,
...
],
];В POST-формах используйте директиву @csrf:
<form method="POST" action="/profile">
@csrf
<!-- Эквивалентно: -->
<input type="hidden" name="_token" value="{{ csrf_token() }}" />
</form>Для AJAX настройте заголовок X-CSRF-Token.
Исключения маршрутов из CSRF через $except в middleware — только для безсостоятельных API или вебхуков. Лишние исключения создают дыры в защите.
Инъекция команд
Выполнение shell-команд со вставкой неэкранированного ввода.
public function verifyDomain(Request $request)
{
exec('whois '.$request->input('domain'));
}Экранируйте через escapeshellcmd и/или escapeshellarg.
Другие инъекции
Инъекция объектов, eval, extract на недоверенных данных — избегайте.
unserialize($request->input('data'));
eval($request->input('data'));
extract($request->all());Не передавайте недоверенный ввод в опасные функции.
Ограничение частоты запросов
Laravel ограничивает злоупотребление маршрутами.
Основные способы:
- Middleware
throttle— на маршрут или группу. RateLimiter::for()— свои правила.
1. На один маршрут
Route::get('/profile', function () {
return 'User profile';
})->middleware('throttle:10,1'); // 10 запросов в минуту2. На группу маршрутов
Route::middleware('throttle:20,1')->group(function () {
Route::get('/posts', fn () => 'Posts');
Route::get('/comments', fn () => 'Comments');
});3. Свой лимитер
В RouteServiceProvider через RateLimiter::for():
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
RateLimiter::for('custom-limit', function ($request) {
return Limit::perMinute(5)->by($request->user()?->id ?: $request->ip());
});Подключение:
Route::middleware('throttle:custom-limit')->get('/dashboard', fn () => 'Dashboard');4. Глобально для API / web
Можно добавить throttle в группы api или web в Kernel.php (группа api по умолчанию уже с лимитом).
protected $middlewareGroups = [
'api' => [
'throttle:60,1', // 60 запросов в минуту для API
// ...
],
'web' => [
'throttle:30,1', // 30 запросов в минуту для web
// ...
],
];Подробнее в документации Laravel по rate limiting.
Заголовки безопасности
Имеет смысл добавить (в веб-сервере или middleware):
- X-Frame-Options
- X-Content-Type-Options
- Strict-Transport-Security (для только HTTPS)
- Content-Security-Policy
См. проект OWASP Secure Headers.
Ссылки
© Перевод на русский язык. Оригинальные материалы: OWASP Cheat Sheet Series.
Этот проект использует материалы OWASP, распространяемые по лицензии CC BY-SA 4.0.