Статья по безопасности 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 и обновления).

По умолчанию 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,
  • Атрибут SameSitelax или 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 с готовой аутентификацией:

  1. Laravel Breeze — минимальный набор: вход, регистрация, сброс пароля, подтверждение email и пароля.
  2. Laravel Fortify — headless-бэкенд аутентификации, плюс двухфакторная аутентификация.
  3. Laravel Jetstream — UI поверх Fortify.

Рекомендуется использовать один из kits для устойчивой и проверенной схемы входа.

Пакеты API-аутентификации

  1. Passport — OAuth2.
  2. Sanctum — токены для 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 ограничивает злоупотребление маршрутами.

Основные способы:

  1. Middleware throttle — на маршрут или группу.
  2. 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.