Безопасность

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; cookies Secure, HttpOnly, SameSite (в Laravel по умолчанию);
  • Hash::check для паролей; шифрование данных — Crypt::encryptString;
  • зависимости: composer audit, Dependabot;
  • security.txt, бэкапы, принцип наименьших привилегий для БД-пользователя.
Senior Как безопасно обрабатывать вебхуки от внешних сервисов?

Webhook — входящий POST без сессии, значит CSRF-мiddleware надо исключить, но доверие обеспечить иначе:

  1. Верификация подписи (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
  1. Идемпотентность — провайдер шлёт повторы: хранить event_id и игнорировать обработанные:
if (ProcessedEvent::where('event_id', $eventId)->exists()) return response('ok');
  1. Быстрый ответ — сразу 200 OK + обработка в очереди, иначе таймауты и ретраи-шторм:
ProcessStripeWebhook::dispatch($payload);
return response('ok');
  1. Секрет — в .env, не в коде; IP-allowlist как дополнительный слой, если провайдер публикует диапазоны.

Готовые решения: laravel/cashier для Stripe, spatie/laravel-webhook-client — универсальный.