LaravelSeniorTechnical
Каковы лучшие практики безопасности в приложении Laravel (CSRF, XSS, SQL injection)?
Laravel предоставляет встроенную защиту от CSRF (middleware VerifyCsrfToken), экранирование XSS через Blade {{ }}, параметризованные запросы Eloquent/QueryBuilder против SQL-инъекций. Дополнительно — rate limiting, строгий валидатор и CSP-заголовки.
CSRF-защита
Laravel автоматически генерирует CSRF-токен для каждой сессии. Middleware \App\Http\Middleware\VerifyCsrfToken сверяет токен при каждом POST, PUT, PATCH, DELETE-запросе.
// В Blade-форме
<form method="POST" action="/profile">
@csrf {{-- генерирует <input type="hidden" name="_token" value="..."> --}}
@method('PUT')
</form>
// Для SPA/API: исключить маршруты из CSRF-проверки
class VerifyCsrfToken extends Middleware
{
protected $except = [
'stripe/*', // вебхуки платёжных систем
'api/webhooks/*',
];
}
// Для SPA с Sanctum: cookie X-XSRF-TOKEN обрабатывается автоматически
// axios / fetch должны читать cookie 'XSRF-TOKEN' и передавать в заголовке
XSS-защита
Blade автоматически экранирует вывод через {{ }}. Тройные скобки {!! !!} выводят сырой HTML — использовать только для доверенных данных.
// БЕЗОПАСНО: экранирует < > " ' &
{{ $user->name }}
// ОПАСНО: используйте только для своего очищенного контента
{!! $article->html_content !!}
// Очистка HTML от пользователя перед сохранением:
composer require mews/purifier
$clean = clean($request->input('content')); // HTMLPurifier под капотом
// Content-Security-Policy заголовок (через middleware):
class SecurityHeaders
{
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$response->headers->set(
'Content-Security-Policy',
"default-src 'self'; script-src 'self' 'nonce-" . csp_nonce() . "'"
);
$response->headers->set('X-Content-Type-Options', 'nosniff');
$response->headers->set('X-Frame-Options', 'DENY');
return $response;
}
}
SQL Injection
Eloquent и Query Builder используют PDO prepared statements — все привязанные значения параметризуются автоматически.
// БЕЗОПАСНО: параметры привязаны
User::where('email', $request->email)->first();
DB::table('users')->where('id', $id)->get();
// ОПАСНО: никогда не интерполируйте пользовательский ввод
// BAD:
DB::select("SELECT * FROM users WHERE name = '$name'");
// Если нужен динамический столбец — валидируйте через allowlist:
$column = in_array($request->sort, ['name', 'email', 'created_at'])
? $request->sort
: 'created_at';
User::orderBy($column)->get();
// Raw выражения — только с привязкой:
DB::select('SELECT * FROM users WHERE id = ?', [$id]);
User::whereRaw('votes > ?', [100])->get();
Mass Assignment защита
// ПРАВИЛО: всегда указывайте $fillable или $guarded
class User extends Model
{
// Явный allowlist — предпочтительно
protected $fillable = ['name', 'email', 'password'];
// Или явный blocklist (осторожно: новые поля автоматически открыты)
// protected $guarded = ['id', 'is_admin'];
}
// Никогда в production:
// protected $guarded = []; // открывает все поля
Rate Limiting и Throttle
// routes/api.php
Route::middleware('throttle:60,1')->group(function () {
Route::post('/login', [AuthController::class, 'login']);
});
// Кастомный лимитер (AppServiceProvider или RouteServiceProvider)
RateLimiter::for('login', function (Request $request) {
return Limit::perMinute(5)->by($request->ip());
});
Route::middleware('throttle:login')->post('/login', ...);
Валидация и файловые загрузки
// Form Request с жёсткими правилами
public function rules(): array
{
return [
'avatar' => [
'required',
'file',
'mimes:jpg,jpeg,png,webp',
'max:2048', // 2 МБ
'dimensions:max_width=2000,max_height=2000',
],
'role' => ['required', Rule::in(['user', 'editor'])], // enum allowlist
];
}
// Хранить загруженные файлы вне public/
$path = $request->file('avatar')->store('avatars', 'private');
// Раздавать через временные URL:
$url = Storage::disk('private')->temporaryUrl($path, now()->addMinutes(30));
Подводные камни
- Отключённый CSRF для всего API без Sanctum SPA: некоторые добавляют весь
api/*в$except, не осознавая, что stateful SPA-запросы остаются незащищены. - Использование
{!! !!}для пользовательского контента: достаточно одного такого места, чтобы злоумышленник внедрил вредоносный<script>. $guarded = []в модели: часто встречается в примерах учебников — в production это открывает все поля для массового присвоения, включаяis_admin.- Динамические имена столбцов без allowlist:
orderBy($request->sort)без проверки позволяет перебирать любые столбцы, включая чувствительные. - Хранение файлов в
public/storageнапрямую: файлы становятся доступны без аутентификации по прямой ссылке; используйтеprivate-диск и временные URL. - Секреты в
.envзакоммичены в git:.gitignoreдолжен исключать.env; используйте.env.exampleбез реальных значений. - Отсутствие заголовков безопасности: Laravel не устанавливает
Content-Security-Policy,X-Frame-OptionsиStrict-Transport-Securityпо умолчанию — добавьте middleware или пакетspatie/laravel-csp. - Verbose error messages в production: убедитесь, что
APP_DEBUG=falseиAPP_ENV=production— иначе stack trace с путями и конфигом виден всем.
Common mistakes
- Сводить security best practices к названию метода без lifecycle и failure path.
- Игнорировать модель runtime: Laravel 13 поверх PHP: request проходит через HTTP kernel, middleware, routing, controller/action и response pipeline.
- Не отделять validation, authorization, transaction boundary и business logic.
What the interviewer is testing
- Объясняет security best practices через конкретную точку lifecycle в Laravel.
- Приводит корректный минимальный пример без вымышленных методов или callbacks.
- Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.
- Идёт от входных данных к БД/кешу/логам, а не предлагает случайные исправления.