О разделе

Безопасность отличается от других слоёв тем, что цена ошибки асимметрична: один пропущенный validation на endpoint вашего приложения может стоить компании репутации, штрафов GDPR и судебных исков. Большинство разработчиков знают о SQL Injection и XSS на уровне определения, но не знают, как именно работает CSP, чем JWT отличается от session cookie, какие конкретно атаки лечит SameSite=Strict. Этот раздел даёт операционное понимание угроз и их митигаций — не теорию, а то, что нужно делать руками.

Принципы, на которых построен раздел:
  • Defense in depth: ни один слой защиты не считается достаточным;
  • Принцип наименьших привилегий: каждый компонент имеет минимальные права;
  • Fail securely: при ошибке система не открывает доступ;
  • Никогда не доверять клиенту: вся валидация на сервере;
  • Mark, not patch: security баги — не «технический долг», а инциденты.
Карта раздела
БлокЧто разбирается
ОбзорOWASP Top 10 2021 — что входит и почему
АутентификацияПароли, хеширование (bcrypt/argon2), tokens, OAuth 2.0/OIDC
АвторизацияRBAC, ABAC, Gates/Policies в Laravel
АтакиCSRF, XSS (3 типа), SQL Injection, CORS, brute-force
ТранспортTLS, HSTS, CSP, Security Headers
OperationsSecrets management, audit logging
OWASP Top 10 (2021)
Назначение

OWASP Top 10 — индустриальный стандарт классификации самых критичных уязвимостей веб-приложений, обновляемый раз в 3-4 года. Это не исчерпывающий список (есть OWASP Top 10 API, OWASP ASVS), но это первый чек-лист, который должен проходить любой production-проект. Знание Top 10 — маст-хэв для middle и senior.

Десять категорий (2021)
#КатегорияЧто внутри
A01Broken Access ControlОбход авторизации, IDOR, отсутствие проверки на endpoint
A02Cryptographic FailuresСлабое хеширование, MD5/SHA1 для паролей, HTTP вместо HTTPS
A03InjectionSQL, NoSQL, LDAP, OS command, ORM injection
A04Insecure DesignАрхитектурные дыры: race conditions, бизнес-логика без проверок
A05Security MisconfigurationДефолтные пароли, открытые admin-панели, debug=true в проде
A06Vulnerable ComponentsСтарые зависимости с известными CVE
A07Identification & Auth FailuresBrute-force без защиты, слабые сессии, отсутствие 2FA
A08Software & Data Integrity FailuresДоверие к unsigned-данным, supply chain атаки
A09Security Logging & Monitoring FailuresОтсутствие логов, поздно замеченные инциденты
A10Server-Side Request Forgery (SSRF)Сервер делает запросы к URL, контролируемому атакующим
Особые случаи
1. «Мы делаем код-ревью, OWASP не нужен.» Code review ловит логические ошибки, не системные угрозы. SSRF в обработчике картинок легко пропустить.
2. WAF как панацея. Web Application Firewall (CloudFlare, AWS WAF) фильтрует известные паттерны. Уникальные уязвимости вашего приложения WAF не видит.
3. «У нас закрытый API, инъекции не страшны.» Внутренний API через PostMessage из браузера — всё ещё HTTP-endpoint. Внутренний трафик не значит безопасный.
4. Пентест раз в год. Уязвимости появляются с каждым релизом. Нужен SAST в CI, регулярный dependency scan, не раз в год.
5. Top 10 покрывает 100%. Top 10 — верхушка. Полный список — OWASP ASVS (Application Security Verification Standard), сотни требований по уровням.
6. Старая редакция Top 10. 2017 → 2021: появились «Insecure Design», «Software Integrity», «SSRF». Старые туториалы могут не покрывать.
7. Бэкенд-разработчик игнорирует фронтенд-угрозы. XSS и CSP конфигурируются на бэкенде (заголовки, escape в шаблонах). Это совместная зона ответственности.
8. Уязвимости в dev-зависимостях. composer audit, npm audit — запускать в CI. Уязвимая dev-зависимость может попасть в build (особенно через build scripts).
Пароли: хеширование и хранение
Назначение

Хранение паролей — первый и самый частый источник катастрофических утечек. Все известные мега-утечки последних 10 лет (LinkedIn, Adobe, Yahoo) случились с базами, где пароли хранились в plain text или с устаревшим хешированием. Современный стандарт — adaptive hashing: bcrypt или argon2id, с медленностью, рассчитанной так, чтобы перебор стоил миллиарды лет.

Правила

Никогда: MD5, SHA1, SHA256 для паролей

Эти алгоритмы спроектированы быстрыми. На GPU перебор 100 миллионов SHA256-хешей в секунду. База из 1M юзеров с SHA256-паролями вскрывается за часы.

Используйте: bcrypt или argon2id

bcrypt — проверенный временем; argon2id — современнее, защищён от GPU/ASIC-атак. Оба медленные (100ms на хеш), что делает перебор экономически невыгодным.

Salt автоматический

bcrypt/argon2 включают salt в результат. Не нужно вручную «добавлять соль» — библиотека делает это за вас. Использование одного salt для всех (pepper) — антипаттерн.

Cost factor: подстраивайте под железо

bcrypt cost=12 в 2026 году = ~250 мс на современном CPU. Каждые 2-3 года увеличивайте cost на 1. Цель: вход юзера ≤ 500 мс, перебор — невозможен.

Практика в Laravel
// config/hashing.php — драйвер и cost
return [
    'driver' => 'bcrypt', // или 'argon2id'
    'bcrypt' => ['rounds' => env('BCRYPT_ROUNDS', 12)],
    'argon'  => ['memory' => 65536, 'threads' => 1, 'time' => 4],
];

// Хеширование при регистрации
$user->password = Hash::make($request->password);

// Проверка при логине
if (Hash::check($request->password, $user->password)) {
    // аутентифицирован
}

// Rehash при изменении cost — после успешного входа
if (Hash::needsRehash($user->password)) {
    $user->update(['password' => Hash::make($request->password)]);
}
Особые случаи
1. Хранение пароля в логах. Log::info('login', $request->all()) запишет пароль в plain text. Используйте $request->except('password').
2. Возврат password_hash в API. Eloquent по умолчанию сериализует все поля. Добавьте protected $hidden = ['password', 'remember_token'].
3. Timing attack при сравнении. $password === $stored сравнивает посимвольно; время выполнения зависит от позиции расхождения. Hash::check использует hash_equals — constant time.
4. md5(password . salt) — самопальное хеширование. Любая самописная функция хеширования = катастрофа. Только проверенные библиотеки.
5. Минимальная длина 6 символов. Бессмысленно: 123456. Современная рекомендация NIST — min 8 без принудительной сложности (потому что пользователи всё равно ставят P@ssw0rd1!). Лучше — passphrase 12+ символов.
6. Принудительная смена пароля каждые 90 дней. NIST отказался от этого в 2017: пользователи делают Pass1, Pass2, Pass3. Меняйте только при подозрении на компрометацию.
7. Запрет паролей с известных утечек. Используйте Have I Been Pwned API (k-anonymity, не отправляет полный пароль). Запретите пароли из top-N утечек.
8. Password reset с предсказуемым токеном. Токен сброса = md5(email . time()) предсказуем. Используйте Str::random(60), храните хеш токена в БД.
Tokens, Sessions, JWT, Sanctum
Назначение

После проверки пароля приложение должно «помнить», что пользователь аутентифицирован. Способа три: server-side sessions с cookie, stateless JWT, opaque tokens. У каждого свой профиль безопасности, удобства, масштабируемости. Правильный выбор зависит от архитектуры (монолит/микросервисы), типа клиента (браузер/SPA/мобайл), требований к revocation.

Сравнение подходов
ПодходХранениеRevocationПрименение
Session cookieServer (Redis/DB) + cookie с session_idМгновенная (удаление из storage)Классический web, монолит
Opaque token (Sanctum)Hash в БДМгновенная (удаление токена)API для SPA, мобайл, OAuth-like
JWT (stateless)Только клиентСложная (blacklist + короткий TTL)Микросервисы, кросс-доменная аутентификация
JWT + refresh tokenRefresh в БД, access — statelessRevoke через refreshOAuth 2.0, мобайл-приложения
Session cookies: безопасные настройки
// config/session.php
return [
    'driver'         => 'redis',
    'lifetime'       => 120,           // 2 часа idle
    'expire_on_close'=> true,        // сессия умирает с закрытием вкладки
    'secure'         => true,         // cookie только по HTTPS
    'http_only'      => true,         // JS не имеет доступа
    'same_site'      => 'strict',      // защита от CSRF
];

Эти 5 настроек — обязательный минимум для session cookie в production. secure + http_only + same_site=strict покрывает 90% типичных атак на сессии.

JWT (JSON Web Token)

JWT — три части через точку: header.payload.signature. Header описывает алгоритм, payload содержит claims (sub, exp, iat и кастомные), signature защищает целостность. JWT читаем (base64, не зашифрован) — не кладите туда секреты.

// Структура JWT
{"alg":"RS256","typ":"JWT"}                          // header
{"sub":"user_42","exp":1735689600,"role":"admin"}  // payload
<signature>                                              // HMAC или RSA

Алгоритмы подписи: HS256 (HMAC, симметричный, для одного сервиса), RS256 (RSA, асимметричный, для распределённых систем). RS256 предпочтительнее в микросервисах: один сервис подписывает приватным ключом, другие проверяют публичным.

Laravel Sanctum (opaque tokens)
// Создание токена
$token = $user->createToken('mobile-app', ['orders:read', 'orders:write']);
return ['token' => $token->plainTextToken()];

// Защита маршрутов
Route::middleware('auth:sanctum')->group(function () {
    Route::get('/api/me', fn (Request $r) => $r->user());
});

// Проверка ability
if ($request->user()->tokenCan('orders:write')) { ... }

// Revoke всех токенов
$user->tokens()->delete();
Особые случаи
1. JWT с секретом в коде. Hardcoded HMAC-секрет в репо = компрометация всех токенов. Через env, секреты-менеджер.
2. JWT alg=none. Атака на старые библиотеки: атакующий подменяет alg на none, signature становится пустой, сервер «верит». Используйте библиотеки, явно блокирующие alg=none.
3. Долгоживущий JWT без revocation. JWT с TTL=24h украденный нельзя отозвать. Используйте короткий access token (5-15 мин) + refresh token, который можно revoke.
4. JWT в localStorage. XSS получает доступ к localStorage. httpOnly cookie защищена от JS.
5. Чувствительные данные в JWT payload. JWT не зашифрован, только подписан. Любой может декодировать base64 и прочитать.
6. Sanctum::actingAs в тестах с лишними abilities. Sanctum::actingAs($user, ['*']) даёт все права — тест не проверяет реальную авторизацию. Указывайте конкретные abilities.
7. Session fixation. Не регенерируется session_id после логина — атакующий, давший жертве свою сессию, входит как жертва. Laravel auth()->login делает regenerate автоматически.
8. Cookie без Domain и Path. По умолчанию cookie привязан к текущему host. Указывать Domain=.example.com только если действительно нужен на поддоменах — иначе расширение atak surface.
OAuth 2.0 + OIDC
Назначение

OAuth 2.0 — протокол делегированной авторизации: одно приложение получает разрешение действовать от имени пользователя в другом, без передачи пароля. OIDC (OpenID Connect) — надстройка над OAuth для аутентификации. Это два разных уровня: OAuth решает «может ли это приложение читать твоё API», OIDC — «кто этот человек».

Роли и термины
РольЧто делает
Resource OwnerПользователь, владеющий данными
ClientПриложение, запрашивающее доступ
Authorization ServerВыдаёт токены (Google, Auth0, Keycloak)
Resource ServerAPI, защищённое токеном
Основные flow

Authorization Code (с PKCE)

Стандарт для веб и мобайл с 2020. Юзер логинится на AS → получает authorization_code → клиент обменивает на access_token. PKCE (Proof Key for Code Exchange) защищает от перехвата code: client генерирует code_verifier, посылает code_challenge = SHA256(verifier), при обмене предъявляет verifier. Без знания verifier перехваченный code бесполезен.

Client Credentials

Машина-к-машине. Сервис аутентифицируется client_id + client_secret. Нет пользователя. Используется для бэкенд-к-бэкенд интеграций.

Refresh Token

Длинноживущий токен для получения новых access_token без повторного логина. Хранится только в защищённой части клиента (никогда в браузере без httpOnly).

Implicit (deprecated)

Старый flow для SPA, теперь deprecated. SPA должен использовать Authorization Code + PKCE.

Resource Owner Password Credentials (deprecated)

Клиент передаёт пароль на AS. Только для legacy миграций; современные системы — не используйте.

Authorization Code + PKCE: шаги
1. Client → AS: GET /authorize?
     client_id=app&
     redirect_uri=https://app.com/callback&
     response_type=code&
     scope=openid profile email&
     code_challenge=<SHA256(verifier)>&
     code_challenge_method=S256&
     state=<random>

2. User логинится на AS, подтверждает scope

3. AS → Client: redirect to https://app.com/callback?code=<CODE>&state=<random>

4. Client → AS: POST /token
     grant_type=authorization_code&
     code=<CODE>&
     redirect_uri=...&
     client_id=app&
     code_verifier=<VERIFIER>

5. AS возвращает: {
     "access_token": "<JWT or opaque>",
     "refresh_token": "<opaque>",
     "id_token": "<JWT — OIDC only>",
     "expires_in": 3600
   }
Особые случаи
1. state параметр забыт. Без проверки state в callback — CSRF на OAuth: атакующий заставляет жертву привязать свой аккаунт. State генерируется клиентом, проверяется при возврате.
2. redirect_uri без whitelist. AS должен валидировать redirect_uri против заранее зарегистрированного списка. Иначе атакующий перенаправит code на свой URL.
3. PKCE для конфиденциальных клиентов «не нужен». Современные best practices: PKCE для ВСЕХ flow, даже для бэкенд-клиентов с secret.
4. Кеш access_token в браузере без httpOnly. XSS читает токен. Используйте httpOnly cookie с SameSite=Lax.
5. Скоупы «по умолчанию» широкие. Запрашивайте минимум нужного (openid profile email), не *. Пользователь видит ваш consent screen.
6. id_token для авторизации API. id_token — для аутентификации (кто это). access_token — для авторизации API (что может). Не путайте.
7. Refresh token rotation. При использовании refresh AS должен выдать новый refresh, инвалидировать старый. Защита от повторного использования украденного.
8. Не проверена подпись id_token. Доверять id_token без проверки сигнатуры — принять любой JWT от атакующего. Используйте JWKS endpoint AS для получения публичных ключей.
Authorization: RBAC / ABAC / ReBAC
Назначение

Аутентификация отвечает «кто», авторизация — «что может». Модели авторизации различаются гранулярностью: RBAC (роли) подходит для большинства SaaS, ABAC (атрибуты) — для сложной бизнес-логики, ReBAC (отношения) — для соц-сетей и доступа к объектам по графу связей.

Три модели

RBAC — Role-Based Access Control

Каждый пользователь имеет роли (admin, editor, viewer); роль имеет набор разрешений. Простая модель, проверка «можешь ли удалять» → «есть ли у тебя роль editor». Не подходит когда правила зависят от объекта (свой/чужой пост).

ABAC — Attribute-Based

Решение основано на атрибутах пользователя, объекта, действия и контекста. Пример: «менеджер региона X может видеть отчёты только своего региона». RBAC такое не выражает, ABAC — элементарно.

ReBAC — Relationship-Based

Доступ через граф отношений: «друг друга может видеть посты». Используется в Google Zanzibar, OpenFGA. Сложна в реализации, оправдана для соц-приложений.

RBAC в Laravel через spatie/laravel-permission
// composer require spatie/laravel-permission

// Назначить роль
$user->assignRole('editor');
$user->givePermissionTo('edit articles');

// Проверка
if ($user->can('edit articles')) { ... }
if ($user->hasRole('admin')) { ... }

// В route
Route::get('/admin', ...)->middleware('role:admin');
Route::get('/posts/edit', ...)->middleware('permission:edit articles');

// В Blade
@can('edit articles')
    <a href="...">Edit</a>
@endcan

Для ABAC-логики (свой/чужой пост) используйте Policies (см. KB_3 раздел Auth). Комбинация Spatie roles + Policies покрывает 95% задач.

Особые случаи
1. IDOR (Insecure Direct Object Reference). /orders/{id} без проверки владения. Атакующий перебирает id и читает чужие заказы. Используйте Policies или scoped bindings.
2. Проверка только на UI. Скрытая кнопка Edit в blade при @can — UI-фасад. Атакующий шлёт POST напрямую. Проверяйте на endpoint всегда.
3. is_admin в массовом присваивании. $user->update($request->all()) с is_admin в FormRequest — пользователь сделал себя админом. Whitelist через $fillable.
4. Privilege escalation через cookie. Роль хранится в cookie на клиенте. Атакующий редактирует cookie. Роль — только в БД, не в клиентском состоянии.
5. Race condition при назначении ролей. Два запроса одновременно назначают противоречивые роли. Транзакция + блокировка.
6. Дефолтная роль с лишними правами. Новый пользователь сразу получает editor, потому что «удобно». Должен быть viewer, с явным повышением.
7. Permissions кешируются неинвалидно. После revoke права в кеше старая роль ещё работает 10 минут. Инвалидируйте кеш в момент изменения.
8. Audit без перечисления попыток. Логируйте отказы в авторизации — это сигнал brute-force на привилегии.
CSRF (Cross-Site Request Forgery)
Назначение

CSRF — атака, при которой злонамеренный сайт заставляет браузер жертвы (уже авторизованной на целевом сайте) отправить запрос. Браузер автоматически прикрепляет cookie сессии — целевой сервер «верит» что это легитимный запрос пользователя. Защита — токен, который атакующий не может прочитать.

Механизмы защиты

CSRF token (synchronizer token pattern)

Сервер генерирует случайный токен на сессию, кладёт в форму. При submit браузер передаёт токен; сервер сравнивает с сохранённым. Атакующий сайт не может прочитать токен (CORS) — не подделает запрос.

SameSite cookie

Set-Cookie: session=...; SameSite=Strict — браузер не отправляет cookie при кросс-доменных запросах. Lax — отправляет только при top-level навигации. None — всегда отправляет (требует Secure). Strict/Lax почти полностью убирают CSRF.

Double-submit cookie

Альтернатива для stateless API: токен в cookie + тот же токен в заголовке (X-CSRF-TOKEN). Атакующий не читает cookie (XHR с другого домена не имеет доступа), не подставит заголовок.

Origin / Referer проверка

Сервер проверяет, что заголовок Origin (или Referer) совпадает с собственным доменом. Защита fallback, ненадёжна сама по себе.

Практика: Laravel CSRF
{{-- Blade: @csrf директива добавляет hidden input --}}
<form method="POST" action="/orders">
    @csrf
    <input name="product_id">
    <button>Buy</button>
</form>
// AJAX: токен из meta-тега + заголовок
<meta name="csrf-token" content="{{ csrf_token() }}">

// fetch / axios
fetch('/api/orders', {
    method: 'POST',
    headers: {
        'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
        'Content-Type': 'application/json',
    },
    body: JSON.stringify({...}),
});

Laravel middleware VerifyCsrfToken в группе web автоматически проверяет POST/PUT/PATCH/DELETE. API-маршруты (через Sanctum/JWT) не нужны в CSRF-защите, поскольку используют stateless токены.

Особые случаи
1. CSRF для GET-запросов. GET не должен менять состояние. Если меняет (deprecated GET /logout) — атакующая <img src> разлогинит пользователя.
2. VerifyCsrfToken::$except для удобства. Webhook'и (Stripe, GitHub) приходят без CSRF — в $except. Но добавление случайного endpoint «временно» открывает дыру.
3. SameSite=None без Secure. Браузеры с 2020 блокируют такие cookie. Если нужен кросс-домен — всегда Secure и HTTPS.
4. CSRF в SPA с Sanctum. Sanctum для SPA использует session cookie + CSRF (это «stateful» режим). Для мобайл/external API — bearer токены без CSRF.
5. Long-lived CSRF token. Токен не должен пережить логаут. Регенерируйте при auth-операциях.
6. CSRF через JSON API без preflight. Простые запросы (form-encoded POST) идут без preflight CORS. Применяйте Content-Type: application/json — вызывает preflight, дополнительная защита.
7. CSRF на основе hidden form field в одном домене. Если same-origin policy уже защищает, токен дублирует. На multi-domain — обязателен.
8. Замена токена в каждом запросе. Излишне: один токен на сессию достаточен. Замена усложняет UX (несколько вкладок), не добавляет защиты.
XSS (Cross-Site Scripting)
Назначение

XSS — внедрение JavaScript в страницу, отображаемую другим пользователем. JS исполняется в его браузере с правами вашего домена: читает cookie, session, делает запросы. Защита — правильное экранирование при выводе + Content Security Policy.

Три типа XSS

Reflected XSS

Параметр запроса попадает в HTML без экранирования. /search?q=<script>...</script> — атакующий шлёт жертве ссылку, при клике запускается JS. Лечится экранированием.

Stored XSS

Вредоносный код сохраняется в БД (комментарий, профиль) и показывается всем пользователям. Самый опасный тип — не требует социального инжиниринга.

DOM-based XSS

Атака на клиентский JS: document.write(location.hash) без escape. Сервер не видит атаки — защита только в браузере.

Практика в Laravel
{{-- Безопасно: {{ }} экранирует HTML --}}
<div>{{ $comment->text }}</div>
{{-- <script>alert(1)</script> → &lt;script&gt;alert(1)&lt;/script&gt; --}}

{{-- Опасно: {!! !!} выводит как есть --}}
<div>{!! $comment->html !!}</div>

{{-- Если нужен HTML — санитизируйте через HtmlPurifier --}}
<div>{!! Purifier::clean($comment->html) !!}</div>

Content Security Policy — вторая линия. Заголовок указывает браузеру, откуда можно загружать скрипты:

Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com;
                          style-src  'self' 'unsafe-inline';
                          img-src    'self' data: https:;
                          object-src 'none'

Даже если XSS внедрился, без разрешения inline-скриптов он не выполнится. CSP меняет правила: атакующему нужно ещё обойти политику.

Особые случаи
1. JSON в HTML без escape. <script>var data = {{ $data }};</script> — если data содержит </script>, scope script ломается. Используйте @json($data).
2. URL атрибут без validation. <a href="{{ $link }}"> — если link = javascript:alert(1), клик запускает JS. Валидируйте схему: только http/https.
3. CSV-injection. Экспорт пользовательских данных в CSV. Если значение начинается с =, +, -, @ — Excel выполнит как формулу. Префиксируйте кавычкой.
4. v-html в Vue, dangerouslySetInnerHTML в React. Прямой аналог {!! !!}. Только после санитизации.
5. CSP с 'unsafe-inline' в script-src. Делает CSP бесполезной для XSS-защиты. Используйте nonces или hashes для legitimate inline скриптов.
6. SVG-файлы как картинки. SVG может содержать JS. Не используйте user-uploaded SVG как <img src>; конвертируйте в PNG.
7. X-XSS-Protection deprecated. Старый header игнорируется современными браузерами. Полагайтесь на CSP.
8. Sanitizer-библиотеки старой версии. HtmlPurifier, DOMPurify периодически находят bypass'ы. Обновляйте.
SQL Injection
Назначение

SQL Injection — внедрение SQL-кода через пользовательский ввод. Атакующий шлёт ' OR 1=1 -- в поле, оригинальный запрос становится SELECT * FROM users WHERE name = '' OR 1=1 --', возвращая всех пользователей. Защита одна и абсолютна: prepared statements. Конкатенация SQL с пользовательским вводом — всегда уязвимость.

Типы инъекций
ТипОписание
Classic / InbandАтакующий видит результат: UNION SELECT password FROM users
Blind booleanРезультат не виден, но атакующий смотрит на разницу в ответе (AND 1=1 vs AND 1=2)
Time-based blindАтакующий измеряет задержку (AND IF(..., SLEEP(5), 0))
Out-of-bandЭксфильтрация через DNS-запросы к контролируемому домену
Second-orderВредоносный ввод сохраняется в БД, эксплуатируется при последующем запросе
Защита: prepared statements
// Уязвимо — конкатенация
$users = DB::select("SELECT * FROM users WHERE email = '" . $_GET['email'] . "'");

// Безопасно — параметр через ?
$users = DB::select('SELECT * FROM users WHERE email = ?', [$_GET['email']]);

// Eloquent — bindings автоматически
User::where('email', $_GET['email'])->first();

// DB::raw с пользовательским вводом — снова уязвимо
DB::table('users')->whereRaw("email = '" . $_GET['email'] . "'");

// DB::raw с bindings — безопасно
DB::table('users')->whereRaw('email = ?', [$_GET['email']]);

Eloquent защищает 100% случаев, если использовать parametr binding. Опасные точки: DB::raw, whereRaw, orderByRaw — при ручной конкатенации.

Особые случаи
1. Имена таблиц/колонок через переменные. Prepared statements не параметризируют идентификаторы. orderBy($_GET['sort']) — SQL injection. Whitelist значений.
2. LIKE с пользовательским вводом. Prepared statement защищает от SQL injection, но не от LIKE-метасимволов %, _. Экранируйте или ограничьте.
3. Динамический WHERE IN (?) с массивом. Laravel автоматически разворачивает массив. Без библиотеки — вручную: '?' . str_repeat(',?', count($ids)-1).
4. NoSQL injection. MongoDB: db.users.find({email: req.body.email, password: req.body.password}) с {$ne: null} в password обходит проверку. Тоже валидируйте.
5. Stored procedures с конкатенацией. SP, выполняющая EXECUTE с конкатенированным SQL — уязвима так же, как app-код.
6. ORM с разрешением сырых выражений. Eloquent whereRaw, Doctrine ->andWhere('u.name = '. $name) — те же грабли.
7. Логи запросов с user input. При компрометации логов атакующий видит структуру запросов, легче угадывает injection points.
8. Минимальные привилегии БД-пользователя. Web-приложение коннектится с правами на SELECT/INSERT/UPDATE. DROP, ALTER, GRANT — отдельный пользователь для миграций. Уменьшает blast radius успешной инъекции.
CORS (Cross-Origin Resource Sharing)
Назначение

CORS — механизм, через который сервер разрешает (или запрещает) браузерам с других доменов делать запросы. По умолчанию same-origin policy запрещает чтение ответа на кросс-доменный запрос. CORS — контролируемое исключение. Неправильная настройка CORS = открытый API для любого сайта в интернете.

Механизм

Simple request

GET / POST с Content-Type: application/x-www-form-urlencoded, multipart/form-data, text/plain. Браузер отправляет запрос с заголовком Origin. Сервер отвечает Access-Control-Allow-Origin. Если совпадает — JS получает ответ.

Preflight (OPTIONS)

Для сложных запросов (custom headers, Content-Type: application/json, методы кроме GET/POST/HEAD) браузер сначала шлёт OPTIONS. Сервер должен ответить с Access-Control-Allow-Methods, Access-Control-Allow-Headers. Только потом — реальный запрос.

Credentials

Access-Control-Allow-Credentials: true + клиент с credentials: include — cookie отправляются с кросс-доменным запросом. Origin не может быть *: при credentials = true он должен быть точным доменом.

Практика: config/cors.php + middleware

Классический пример: фронтенд на https://myfrontend.com, API на https://api.myapp.com. Без CORS браузер заблокирует AJAX-запрос. Чтобы разрешить, API должен вернуть Access-Control-Allow-Origin: https://myfrontend.com.

1. Конфиг — config/cors.php (в Laravel 7+ уже есть, для более старых нужен пакет fruitcake/laravel-cors):

return [
    'paths' => ['api/*', 'sanctum/csrf-cookie'],   // пути, для которых применяется CORS
    'allowed_methods'      => ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
    'allowed_origins'      => ['https://myfrontend.com', 'http://localhost:3000'],
    'allowed_origins_patterns' => [],
    'allowed_headers'      => ['*'],
    'exposed_headers'      => [],
    'max_age'             => 3600,   // кеш preflight
    'supports_credentials'=> true,   // для cookie-auth (Sanctum SPA)
];

2. Middleware — в глобальный стек. В Laravel 10 — App\Http\Kernel::$middleware. В Laravel 11+ — в bootstrap/app.php:

// bootstrap/app.php (Laravel 11+)
->withMiddleware(function (Middleware $middleware) {
    $middleware->api(prepend: [
        Illuminate\Http\Middleware\HandleCors::class,
    ]);
})

Middleware автоматически обрабатывает OPTIONS-запросы (preflight) и добавляет нужные заголовки в ответы.

Типичные ошибки в консоли браузера
Ошибка в консолиПричинаРешение
No 'Access-Control-Allow-Origin' header is present Сервер не вернул разрешающий заголовок Добавить allowed_origins с доменом фронтенда
Request header field X-Requested-With is not allowed by Access-Control-Allow-Headers В preflight сервер не разрешил кастомный заголовок Указать его в allowed_headers
Method DELETE is not allowed by Access-Control-Allow-Methods Метод не разрешён Добавить DELETE в allowed_methods
The value of the 'Access-Control-Allow-Origin' header must not be the wildcard '*' when the request's credentials mode is 'include' Клиент шлёт cookies (credentials: 'include') — * запрещён Указать конкретный домен в allowed_origins + supports_credentials => true
Preflight response is not successful Сервер не отвечает на OPTIONS (403/404/500) Проверить что HandleCors middleware реально в стеке + путь в paths
Важно помнить
CORS — это браузерная защита. Она не мешает запросам из Postman, curl, мобильных приложений или серверных скриптов. Эти клиенты игнорируют Access-Control-* заголовки. CORS — только для JS в браузере.
allowed_origins = ['*'] можно только для публичных read-only API. Для API с авторизацией — нельзя. Плюс * несовместимо с supports_credentials => true — при cookie-auth нужно указывать конкретные домены.
Особые случаи
1. Access-Control-Allow-Origin: *. Открывает API для любого сайта. Допустимо только для публичных read-only API без credentials. Любой private API — явный whitelist.
2. Reflecting Origin без проверки. header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']) — все домены проходят. Атакующий получит доступ.
3. CORS не защищает. CORS — о чтении ответа в браузере. Атакующий может слать запросы (POST формы), просто не видит ответа. CORS не заменяет CSRF.
4. Preflight кешируется надолго. Access-Control-Max-Age: 86400 — preflight кешируется на сутки. Меняешь CORS-настройки — пользователи не видят новые до конца кеша.
5. credentials: include без обдумывания. Включение в fetch заставляет посылать cookie. Если эндпоинт не должен принимать аутентифицированные запросы — не включайте.
6. CORS для public CDN. Если статика на CDN, и приложение делает fetch к этому CDN — нужен CORS на CDN-стороне. Часто упускается.
7. http://localhost:3000 в production-config. Dev-origin остаётся в проде — локальные приложения других разработчиков могут читать ваше API.
8. CORS не работает в WebView. Мобильные WebView могут отключать CORS или вести себя иначе. Тестируйте на реальных платформах.
Rate Limiting и защита от brute-force
Назначение

Rate limiting — ограничение числа запросов от одного источника за интервал времени. Защищает от: brute-force паролей, credential stuffing, scraping, DDoS уровня приложения, перегрузки API. Без rate limiting один скрипт с одной машины может потушить ваш сервис или перебрать миллионы паролей.

Стратегии

По IP

Самый простой: N запросов в минуту с одного IP. Минусы: NAT (один IP — много пользователей), IPv6 (легко получить новые IP). Подходит для unauthenticated endpoints.

По user_id (после auth)

Лимит для аутентифицированных. Защищает от случая, когда легитимный пользователь злоупотребляет.

По API-ключу

Для публичных API: разные тарифы — разные лимиты. Stripe, GitHub.

Sliding window / Token bucket

Алгоритмы:
Fixed window — 100 запросов в минуту, счётчик ресетается каждую минуту (проблема: burst на границе).
Sliding window log — список таймстампов, точно но дорого.
Token bucket — бюджет токенов, регенерируется со временем (стандарт API limit'ов).

Laravel throttle
// routes/web.php
Route::post('/login', LoginController::class)
    ->middleware('throttle:5,1'); // 5 попыток в минуту с IP

// API с лимитом на user
Route::middleware(['auth:sanctum', 'throttle:60,1'])->group(function () {
    Route::apiResource('orders', OrderController::class);
});

// Кастомный лимит — RouteServiceProvider boot()
RateLimiter::for('login', function (Request $request) {
    return [
        Limit::perMinute(5)->by($request->input('email')),
        Limit::perMinute(30)->by($request->ip()),
    ];
});

Лучшая практика для login: лимит и по email, и по IP. Защищает от перебора паролей одного пользователя и от credential stuffing с одного источника.

Особые случаи
1. Только IP-throttle на login. Атакующий c ботнетом из 10к IP пройдёт. Throttle по email/user_id обязателен.
2. Лимит за прокси / load balancer. Без правильной настройки TrustProxies Laravel видит IP балансировщика, не пользователя — все запросы «с одного IP».
3. Слишком жёсткий лимит на legit-endpoint. throttle:10,1 на autocomplete — пользователь набирает быстро, лимит исчерпан. Тюнингуйте по UX.
4. Лимит без сообщения. 429 без понятного объяснения — пользователь не понимает. Возвращайте Retry-After заголовок и человеческое сообщение.
5. Хранение counter'ов в БД. На больших нагрузках лучше Redis с INCR и EXPIRE — on-disk counter создаёт hot row.
6. Account lockout без логирования. Lockout без записи в audit-лог — не видим атаки. Логируйте каждое срабатывание.
7. Lockout как DoS-вектор. Атакующий шлёт неудачные логины на email жертвы, блокирует её аккаунт. Используйте exponential backoff, не permanent lock.
8. Rate limit на CDN-уровне игнорирует app-аутентификацию. CloudFlare видит cookie, но не знает auth. Координируйте app-throttle и edge-throttle.
HTTPS, TLS, mTLS
Назначение

TLS — криптографический протокол, обеспечивающий три свойства: конфиденциальность (никто посередине не читает), целостность (никто не изменил), аутентичность сервера (это действительно тот, кем себя называет). Без TLS любой Wi-Fi в кафе видит ваши пароли. Современный стандарт — TLS 1.3, минимум приемлемо — TLS 1.2.

Базовые понятия

Сертификаты и CA

Сертификат сервера подписан Certificate Authority (Let's Encrypt, DigiCert). Браузер доверяет CA → верит сертификату. Сертификат содержит публичный ключ сервера, домен, expiry.

TLS Handshake

Клиент и сервер договариваются о cipher suite, обмениваются ключами через ECDHE (forward secrecy), сервер доказывает владение private key. TLS 1.3 — 1 round-trip; TLS 1.2 — 2 round-trip.

Forward Secrecy

Сессионный ключ не может быть восстановлен из private key сервера. Если private key утечёт завтра — старый трафик остаётся незашифрованным. Требует ECDHE/DHE.

mTLS (Mutual TLS)

И клиент, и сервер предъявляют сертификаты. Используется для bot-to-bot аутентификации, между микросервисами в service mesh.

Практика: nginx с Let's Encrypt
# nginx config
server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    ssl_protocols         TLSv1.2 TLSv1.3;
    ssl_ciphers           ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:...;
    ssl_prefer_server_ciphers on;
    ssl_session_cache     shared:SSL:10m;
    ssl_session_tickets   off;

    # HSTS
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
}

# HTTP → HTTPS редирект
server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}
Особые случаи
1. TLS 1.0/1.1 ещё включены. Старые версии уязвимы (BEAST, POODLE). Только 1.2+, лучше 1.3.
2. RC4, 3DES, MD5 ciphers. Слабые алгоритмы. Используйте Mozilla SSL config generator для актуального списка.
3. Expired certificate. Браузер показывает страшное предупреждение. Auto-renewal через certbot.
4. Mixed content. HTTPS-страница с <script src="http://..."> — браузер блокирует. Все ресурсы по HTTPS.
5. HSTS без preload. Первый визит уязвим. preload + HSTS-список Chrome исключает первый запрос.
6. Wildcard certificate утёк. Один *.example.com в любой системе — компрометация всех поддоменов. Лучше per-subdomain.
7. TLS termination на LB. Терминируется на balancer, дальше до app — HTTP. Промежуточный трафик в private network уязвим, если кто-то получит доступ.
8. Certificate transparency monitor. Не мониторите CT-логи — не узнаете, что для вашего домена кто-то выпустил неавторизованный сертификат.
Security Headers
Назначение

Security headers — HTTP-заголовки, инструктирующие браузер усиливать защиту. Это дешёвая защита: одна строка конфига даёт многократно сложнее атаки. Чек-лист, который должна пройти любая production-страница: HSTS, CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy.

Основные заголовки
HeaderЧто делает
Strict-Transport-SecurityБраузер ходит только по HTTPS. Защита от downgrade-attack
Content-Security-PolicyWhitelist источников скриптов/стилей/картинок. Защита от XSS
X-Frame-Options: DENYЗапрет iframe. Защита от clickjacking
X-Content-Type-Options: nosniffБраузер не угадывает MIME. Защита от MIME-confusion
Referrer-PolicyКонтроль того, что попадает в Referer. strict-origin-when-cross-origin — разумный дефолт
Permissions-PolicyWhitelist API браузера: camera, microphone, geolocation
Cross-Origin-Opener-PolicyИзоляция от других origin. Защита от Spectre-class
Middleware в Laravel
final class SecurityHeaders
{
    public function handle(Request $request, Closure $next): Response
    {
        $response = $next($request);

        $response->headers->add([
            'Strict-Transport-Security'   => 'max-age=31536000; includeSubDomains; preload',
            'X-Frame-Options'             => 'DENY',
            'X-Content-Type-Options'      => 'nosniff',
            'Referrer-Policy'             => 'strict-origin-when-cross-origin',
            'Permissions-Policy'          => 'camera=(), microphone=(), geolocation=()',
            'Content-Security-Policy'     => "default-src 'self'; script-src 'self' 'nonce-{$nonce}'; img-src 'self' data: https:;",
        ]);

        return $response;
    }
}

Готовый пакет: bepsvpt/secure-headers — конфиг + middleware. Тест собственных заголовков — securityheaders.com.

Особые случаи
1. CSP сразу строгая в проде. Включит панику и сломает legitimate скрипты (analytics, ads). Сначала Content-Security-Policy-Report-Only — собирайте отчёты, потом включайте.
2. HSTS preload без подготовки. Преinload — на годы. Если решите вернуться на HTTP — пользователи не смогут (страницы недоступны). Готовьтесь к долгосрочной HTTPS-only.
3. X-Frame-Options: ALLOW-FROM. Не поддерживается Chrome. Используйте frame-ancestors в CSP.
4. CSP с 'unsafe-eval'. Многие фреймворки требуют. Делает CSP слабее. Постарайтесь обойтись без или используйте strict-dynamic.
5. Кеширование заголовков на CDN. CDN может кешировать ответ с заголовками; изменение в коде не сразу видно пользователям. Vary headers, purge cache.
6. Server: nginx/1.18.0. Раскрытие версии помогает атакующему. Скройте через server_tokens off.
7. Заголовки только на HTML, не на API. API тоже должны иметь HSTS, CORS, CSP. Применяйте middleware ко всем маршрутам.
8. Не тестируется. securityheaders.com даёт оценку A+ до F. Должно быть в CI: автоматическая проверка после деплоя.
Secrets Management
Назначение

Секреты (API-ключи, DB-пароли, JWT-секреты, OAuth client_secret) не должны храниться в коде. Гит-история помнит навсегда; удалённые из последнего коммита секреты остаются в истории, доступны через git log -p. Современная практика: переменные окружения для dev, секреты-менеджер (Vault, AWS Secrets Manager) для prod.

Уровни хранения

.env (dev)

Переменные окружения в файле .env, исключённом из git. Подходит для локальной разработки. Не для production.

Container env (prod basic)

Docker secrets / Kubernetes secrets / системные env vars. Достаточно для маленьких проектов. Минусы: rotate означает рестарт; нет audit-trail.

Secrets manager (prod scale)

HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager. Динамическое получение секретов, auto-rotation, audit-логи. Сложнее в setup'е.

HSM (high security)

Hardware Security Module — private keys никогда не покидают железо. Для финансовых, медицинских данных.

Минимальные правила
  • .env в .gitignore и в .dockerignore;
  • .env.example со всеми ключами без значений — в git;
  • Pre-commit hook сканирует коммиты на secrets (git-secrets, gitleaks, trufflehog);
  • В CI — secrets через защищённые переменные (GitHub Secrets, GitLab CI variables);
  • На production — не .env, а env vars через secrets manager;
  • Rotate секретов после ухода каждого сотрудника, после инцидента;
  • Принцип наименьших привилегий: каждый сервис имеет только свои ключи.
Особые случаи
1. Случайно закоммитили секрет. Удаление из последующего коммита бесполезно — в истории остаётся. Rotate ключ немедленно, потом cleanup через git filter-repo.
2. config:cache с env-ссылками в коде. Если в коде env('STRIPE_KEY') вне config-файлов — после config:cache вернёт null. Только в config/, в коде — config('services.stripe.key').
3. Секреты в логах. Случайное логирование запроса с Authorization header. Маскируйте через middleware на уровне логгера.
4. .env в Docker image. ADD .env в Dockerfile — секреты в image, который потом утечёт в registry. Передавайте через env vars при запуске.
5. Одинаковые секреты везде. Один JWT-секрет в dev/staging/prod. Утечка с dev = компрометация prod. Уникальные значения на окружение.
6. CI artifacts хранят секреты. Build-логи в публичных PR могут содержать env vars. Маскируйте секреты в CI (::add-mask:: в GitHub Actions).
7. Long-lived OAuth tokens в файлах. Token live forever — утечка катастрофична. Refresh + revoke.
8. chmod 644 на .env. Доступно всем пользователям сервера. chmod 600 + правильный owner.
Практика: security review модуля
Постановка

Разработчик принёс на ревью endpoint загрузки аватарки. Сделаем поэтапный security-review, отмечая уязвимости.

// Множество проблем — найдите все
final class AvatarController
{
    public function upload(Request $request)
    {
        $user = User::find($request->input('user_id'));
        $file = $request->file('avatar');
        $name = $file->getClientOriginalName();
        $file->move(public_path('avatars'), $name);
        $user->update(['avatar' => "/avatars/{$name}"]);
        return ['url' => "/avatars/{$name}"];
    }
}
Найденные уязвимости
A01 Broken Access Control: user_id из запроса — любой может загрузить аватарку другому. Должно быть $request->user().
Path traversal: getClientOriginalName() может вернуть ../../etc/passwd. Используйте Str::random() + расширение из mime.
MIME spoofing / RCE: Нет проверки типа файла. PHP-файл может быть загружен и выполнен. $request->validate(['avatar' => 'image|mimes:jpg,png|max:2048']).
SVG XSS: Если разрешён SVG — внутри может быть JS. Конвертируйте в PNG через Intervention Image.
DoS через большой файл: Без max:2048 — пользователь грузит 10 GB.
Public folder: Файлы попадают в public/avatars/ — нет контроля доступа. Используйте storage/app/private/avatars/ + signed URLs.
Race condition: Два параллельных загрузки с одним именем — одна перезаписывается. Str::uuid() для уникальности.
Отсутствие rate limiting: Пользователь может загрузить 1000 файлов и забить storage.
Безопасная версия
final class AvatarController
{
    public function __construct(private AvatarUploader $uploader) {}

    public function upload(UploadAvatarRequest $request): JsonResponse
    {
        $path = $this->uploader->store($request->user(), $request->file('avatar'));

        return response()->json(['url' => route('avatar.show', ['path' => $path])]);
    }
}

// FormRequest решает: auth + validate
final class UploadAvatarRequest extends FormRequest
{
    public function authorize(): bool { return $this->user() !== null; }

    public function rules(): array
    {
        return [
            'avatar' => ['required', 'image', 'mimes:jpg,png,webp', 'max:2048', 'dimensions:max_width=2000,max_height=2000'],
        ];
    }
}

// Маршрут с throttle
Route::post('/avatar', [AvatarController::class, 'upload'])
    ->middleware(['auth:sanctum', 'throttle:5,60']); // 5 загрузок в час
Сводные подводные камни безопасности
Топ-12
1. APP_DEBUG=true в production. Раскрывает stack traces со структурой кода, конфигом, секретами. Всегда false.
2. Mass assignment без $fillable. User::create($request->all()) с is_admin в payload.
3. IDOR. /orders/{id} без проверки владения. Используйте Policies + scopeBindings.
4. Хардкод секрета. API-ключ в коде — навсегда в git-истории.
5. {!! $userInput !!} в Blade. Прямой XSS. Используйте {{ }}.
6. Загрузка файлов без MIME-проверки. RCE через php-файл с расширением jpg.
7. CORS с * + credentials. Открыт для любого сайта.
8. JWT в localStorage. Любой XSS читает. Используйте httpOnly cookie.
9. Brute-force только по IP. Ботнет обходит. Throttle по email/user.
10. Server-Side Request Forgery (SSRF). file_get_contents($_GET['url']) — атакующий шлёт http://169.254.169.254/ и читает AWS metadata.
11. Сертификат на 10 лет. Long-lived сертификаты — больше окно компрометации. Let's Encrypt — 90 дней с auto-rotate.
12. Отсутствие audit-логов. Инцидент произошёл — не знаем когда, кем, что. Логируйте успешные/неуспешные логины, изменения прав, удаление данных.
Вопросы для самопроверки
Базовые

1. Чем аутентификация отличается от авторизации?

Аутентификация — «кто этот пользователь» (проверка identity). Авторизация — «что он может сделать» (проверка permissions). Без аутентификации нет авторизации (никто = нет прав).

2. Почему MD5 нельзя использовать для паролей?

MD5 спроектирован быстрым — на GPU перебор 100M+ хешей в секунду. База с MD5-паролями вскрывается за часы. Для паролей нужны медленные адаптивные функции: bcrypt (~250мс на хеш), argon2id.

3. Что такое OWASP Top 10 и почему важно?

Индустриальный список 10 самых критичных уязвимостей веба, обновляемый каждые 3-4 года OWASP Foundation. Обязательный чек-лист для любого production. Версия 2021 включает Broken Access Control (#1), Cryptographic Failures, Injection, Insecure Design.

Атаки на сессии

4. Что такое CSRF и какие механизмы защиты?

Cross-Site Request Forgery — злонамеренный сайт заставляет браузер жертвы с активной сессией отправить запрос. Защита: (1) CSRF token в формах + проверка сервером; (2) SameSite=Strict cookie — браузер не отправляет cookie кросс-доменно; (3) double-submit cookie для stateless API; (4) проверка Origin/Referer.

5. Чем httpOnly cookie защищён от XSS, а localStorage нет?

httpOnly cookie недоступен из JavaScript — даже если XSS-payload выполнился, он не прочитает cookie. localStorage и обычные cookie читаются через JS (localStorage.getItem, document.cookie). При выборе хранения токена httpOnly cookie предпочтительнее для защиты от XSS.

6. Что такое clickjacking и как защититься?

Атакующий встраивает ваш сайт в iframe на своём сайте, накладывает прозрачные элементы, обманывает пользователя кликнуть на ваш интерфейс. Защита — заголовок X-Frame-Options: DENY или CSP frame-ancestors 'none' — браузер запрещает встраивание.

OAuth / JWT

7. Объясните Authorization Code Flow с PKCE

(1) Client генерирует code_verifier (random string) и code_challenge = SHA256(verifier). (2) Перенаправляет user на AS с challenge. (3) User логинится, AS возвращает code. (4) Client обменивает code на access_token, предъявляя verifier — AS проверяет, что SHA256(verifier) = challenge. (5) AS возвращает токены. PKCE защищает от перехвата code: без verifier code бесполезен.

8. Какая разница между JWT и opaque token?

JWT — самодостаточный токен; payload + signature, проверяется без обращения к серверу аутентификации (stateless). Opaque token — случайная строка; чтобы получить данные, нужно запросить auth server (stateful). JWT масштабируется лучше (нет round-trip), но revocation сложнее. Opaque revoke мгновенно (удаление из БД).

9. Почему JWT с alg=none опасен?

Старые JWT-библиотеки принимали алгоритм none — signature не проверялась. Атакующий менял header на {"alg":"none"}, ставил пустой signature, сервер «верил» payload. Современные библиотеки явно блокируют none. При выборе библиотеки убедитесь в защите.

SQL Injection

10. Защищает ли ORM от SQL injection полностью?

Eloquent через параметризированные запросы защищает 100%, если использовать стандартные методы (where, insert, update). Опасные точки: DB::raw(), whereRaw, orderByRaw с конкатенацией пользовательского ввода. Имена таблиц/колонок не параметризируются — для динамических нужен whitelist.

11. Что такое second-order SQL injection?

Вредоносный ввод проходит через первый запрос (сохраняется в БД), и эксплуатируется во втором запросе, который использует сохранённое значение в небезопасной конкатенации. Защита: prepared statements везде, не только на входе пользователя.

Транспорт

12. Что такое HSTS и зачем preload?

HTTP Strict Transport Security — заголовок, инструктирующий браузер всегда ходить по HTTPS. Защита от downgrade-атаки (man-in-the-middle принуждает HTTP). preload добавляет домен в встроенный список Chrome — даже первый запрос идёт по HTTPS, нет окна для атаки.

13. Что такое forward secrecy?

Свойство TLS: сессионный ключ не может быть восстановлен из private key сервера. Если private key утечёт, старый записанный трафик остаётся незашифрованным. Достигается через ECDHE (Ephemeral Diffie-Hellman) — новый ключ для каждой сессии.

14. Что делает Content Security Policy?

CSP — заголовок с whitelist источников скриптов, стилей, изображений. Защита от XSS: даже если payload внедрён в HTML, браузер откажется выполнить inline-скрипт без разрешения. Лучшая практика: default-src 'self'; script-src 'self' 'nonce-XXX' — nonce для legitimate inline скриптов.

15. Что такое SSRF и пример?

Server-Side Request Forgery — атакующий заставляет сервер делать HTTP-запросы по контролируемому URL. Классический пример: file_get_contents($_GET['url']) — атакующий шлёт http://169.254.169.254/latest/meta-data/iam/ (AWS instance metadata) и получает IAM credentials сервера. Защита: whitelist разрешённых доменов, запрет приватных IP-диапазонов, отдельный сервис для outbound requests.