Безопасность
CSRF, XSS, SQL-инъекции, mass assignment, rate limiting, секреты.
Middle Как Laravel защищает от SQL-инъекций и где защита может «протечь»?
Eloquent и Query Builder используют подготовленные выражения PDO — данные передаются биндингами, а не подстановкой в строку:
User::where('email', $request->email)->first(); // безопасно
DB::select('SELECT * FROM users WHERE email = ?', [$email]); // безопасно
Опасные места:
DB::select("SELECT * FROM users WHERE email = '$email'"); // ИНЪЕКЦИЯ!
->whereRaw("age > $age") // инъекция
->whereRaw('age > ?', [$age]) // ок
->orderBy($request->input('sort')) // имя колонки не биндится — whitelist!
DB::raw('...') // вся ответственность на вас
Правила:
raw-выражения — только с?-плейсхолдерами;- имена колонок/таблиц из пользовательского ввода — только через белый список (
in_array($sort, ['name', 'created_at'])); - валидация — не защита от инъекций сама по себе, но сужает поверхность атаки.
Middle XSS: как Blade защищает и когда защита не срабатывает?
{{ $value }} экранирует вывод через htmlspecialchars() — теги и кавычки становятся безобидным текстом. Это защита по умолчанию.
Когда XSS всё же возможен:
{!! $user->bio !!} {{-- сырой HTML пользователя — инъекция --}}
<a href="{{ $url }}"> {{-- javascript:alert(1) в $url — не фильтруется схема --}}
<script>let name = {{ $name }};</script> {{-- в JS-контексте нужен @json($name) / Js::from() --}}
Меры:
- никогда не выводить пользовательский ввод через
{!! !!}; если HTML нужен — санитайзер (HTMLPurifier) на входе или выходе; - в JS —
@json($data)илиIlluminate\Support\Js::from($data); - проверять схему URL (
https?://) для пользовательских ссылок; - заголовок Content-Security-Policy — второй эшелон обороны;
strip_tagsне экранирует атрибуты (<img onerror=...>) — это не санитайзер.
Middle CSRF и rate limiting: как устроены и как настраиваются?
CSRF: все POST/PUT/PATCH/DELETE через web-группу проходят middleware ValidateCsrfToken — сверка токена из _token / заголовка X-CSRF-TOKEN с сессией. Blade: @csrf. API (токены Bearer) CSRF не подвержены — там stateless. Исключения — $except / withoutMiddleware (для webhook'ов — но тогда верифицируйте подпись!).
Rate limiting — RateLimiter + middleware throttle:
// bootstrap/app.php или RouteServiceProvider
RateLimiter::for('api', fn (Request $request) =>
Limit::perMinute(60)->by($request->user()?->id ?: $request->ip()));
Route::middleware('throttle:api')->group(...);
Route::middleware('throttle:5,1')->post('/login', ...); // 5 попыток в минуту
// кастомные ответы:
Limit::perMinute(5)->response(fn () => response('Слишком много', 429));
Превышение → 429 Too Many Requests. Хранилище счётчиков — кэш (Redis на проде).
Дополнительно: логин защищают связкой throttle + задержки + CAPTCHA; пароли — Hash (bcrypt специально медленный).
Senior Какие ещё практики безопасности критичны в Laravel-проекте?
Секреты и конфигурация:
.envникогда в git;.env.example— без значений;APP_DEBUG=falseна проде — иначе стектрейс с паролями (Whoops/Flare);- ротация ключей;
APP_KEY— шифрует cookies, утечка = компрометация сессий.
Авторизация на каждом запросе: проверять владение ресурсом (policy), а не только «залогинен ли» — защита от IDOR (/orders/123 чужого заказа). Route Model Binding + scoped bindings помогают.
Файлы:
- валидация типа/размера; имя генерирует фреймворк (
store()), не доверятьgetClientOriginalName(); - публичные и приватные диски; приватные — через
Storage::download()с проверкой прав; - не выполнять загруженное: вне docroot или без execute-прав.
Прочее:
- HTTPS +
HSTS; cookiesSecure,HttpOnly,SameSite(в Laravel по умолчанию); Hash::checkдля паролей; шифрование данных —Crypt::encryptString;- зависимости:
composer audit, Dependabot; - security.txt, бэкапы, принцип наименьших привилегий для БД-пользователя.
Senior Как безопасно обрабатывать вебхуки от внешних сервисов?
Webhook — входящий POST без сессии, значит CSRF-мiddleware надо исключить, но доверие обеспечить иначе:
- Верификация подписи (Stripe, GitHub и др. подписывают тело запроса HMAC):
$signature = $request->header('Stripe-Signature');
$expected = hash_hmac('sha256', $payload, config('services.stripe.webhook_secret'));
abort_unless(hash_equals($expected, $signature), 403); // hash_equals — timing-safe
- Идемпотентность — провайдер шлёт повторы: хранить
event_idи игнорировать обработанные:
if (ProcessedEvent::where('event_id', $eventId)->exists()) return response('ok');
- Быстрый ответ — сразу
200 OK+ обработка в очереди, иначе таймауты и ретраи-шторм:
ProcessStripeWebhook::dispatch($payload);
return response('ok');
- Секрет — в
.env, не в коде; IP-allowlist как дополнительный слой, если провайдер публикует диапазоны.
Готовые решения: laravel/cashier для Stripe, spatie/laravel-webhook-client — универсальный.