Безопасность
OWASP Top 10, аутентификация (пароли, токены, OAuth 2.0), авторизация (RBAC/ABAC), CSRF/XSS/SQL Injection, CORS, rate limiting, TLS, security headers, secrets management. Middle/senior уровень с практическими сценариями.
Безопасность отличается от других слоёв тем, что цена ошибки асимметрична: один пропущенный 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 |
| Operations | Secrets management, audit logging |
OWASP Top 10 — индустриальный стандарт классификации самых критичных уязвимостей веб-приложений, обновляемый раз в 3-4 года. Это не исчерпывающий список (есть OWASP Top 10 API, OWASP ASVS), но это первый чек-лист, который должен проходить любой production-проект. Знание Top 10 — маст-хэв для middle и senior.
| # | Категория | Что внутри |
|---|---|---|
| A01 | Broken Access Control | Обход авторизации, IDOR, отсутствие проверки на endpoint |
| A02 | Cryptographic Failures | Слабое хеширование, MD5/SHA1 для паролей, HTTP вместо HTTPS |
| A03 | Injection | SQL, NoSQL, LDAP, OS command, ORM injection |
| A04 | Insecure Design | Архитектурные дыры: race conditions, бизнес-логика без проверок |
| A05 | Security Misconfiguration | Дефолтные пароли, открытые admin-панели, debug=true в проде |
| A06 | Vulnerable Components | Старые зависимости с известными CVE |
| A07 | Identification & Auth Failures | Brute-force без защиты, слабые сессии, отсутствие 2FA |
| A08 | Software & Data Integrity Failures | Доверие к unsigned-данным, supply chain атаки |
| A09 | Security Logging & Monitoring Failures | Отсутствие логов, поздно замеченные инциденты |
| A10 | Server-Side Request Forgery (SSRF) | Сервер делает запросы к URL, контролируемому атакующим |
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 мс, перебор — невозможен.
// 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)]);
}
Log::info('login', $request->all()) запишет пароль в plain text. Используйте $request->except('password').protected $hidden = ['password', 'remember_token'].$password === $stored сравнивает посимвольно; время выполнения зависит от позиции расхождения. Hash::check использует hash_equals — constant time.md5(password . salt) — самопальное хеширование. Любая самописная функция хеширования = катастрофа. Только проверенные библиотеки.123456. Современная рекомендация NIST — min 8 без принудительной сложности (потому что пользователи всё равно ставят P@ssw0rd1!). Лучше — passphrase 12+ символов.Pass1, Pass2, Pass3. Меняйте только при подозрении на компрометацию.md5(email . time()) предсказуем. Используйте Str::random(60), храните хеш токена в БД.После проверки пароля приложение должно «помнить», что пользователь аутентифицирован. Способа три: server-side sessions с cookie, stateless JWT, opaque tokens. У каждого свой профиль безопасности, удобства, масштабируемости. Правильный выбор зависит от архитектуры (монолит/микросервисы), типа клиента (браузер/SPA/мобайл), требований к revocation.
| Подход | Хранение | Revocation | Применение |
|---|---|---|---|
| Session cookie | Server (Redis/DB) + cookie с session_id | Мгновенная (удаление из storage) | Классический web, монолит |
| Opaque token (Sanctum) | Hash в БД | Мгновенная (удаление токена) | API для SPA, мобайл, OAuth-like |
| JWT (stateless) | Только клиент | Сложная (blacklist + короткий TTL) | Микросервисы, кросс-доменная аутентификация |
| JWT + refresh token | Refresh в БД, access — stateless | Revoke через refresh | OAuth 2.0, мобайл-приложения |
// 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 — три части через точку: 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 предпочтительнее в микросервисах: один сервис подписывает приватным ключом, другие проверяют публичным.
// Создание токена
$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();
alg=none.Sanctum::actingAs в тестах с лишними abilities. Sanctum::actingAs($user, ['*']) даёт все права — тест не проверяет реальную авторизацию. Указывайте конкретные abilities.auth()->login делает regenerate автоматически.Domain и Path. По умолчанию cookie привязан к текущему host. Указывать Domain=.example.com только если действительно нужен на поддоменах — иначе расширение atak surface.OAuth 2.0 — протокол делегированной авторизации: одно приложение получает разрешение действовать от имени пользователя в другом, без передачи пароля. OIDC (OpenID Connect) — надстройка над OAuth для аутентификации. Это два разных уровня: OAuth решает «может ли это приложение читать твоё API», OIDC — «кто этот человек».
| Роль | Что делает |
|---|---|
| Resource Owner | Пользователь, владеющий данными |
| Client | Приложение, запрашивающее доступ |
| Authorization Server | Выдаёт токены (Google, Auth0, Keycloak) |
| Resource Server | API, защищённое токеном |
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 миграций; современные системы — не используйте.
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
}
state параметр забыт. Без проверки state в callback — CSRF на OAuth: атакующий заставляет жертву привязать свой аккаунт. State генерируется клиентом, проверяется при возврате.openid profile email), не *. Пользователь видит ваш consent screen.id_token для авторизации API. id_token — для аутентификации (кто это). access_token — для авторизации API (что может). Не путайте.Аутентификация отвечает «кто», авторизация — «что может». Модели авторизации различаются гранулярностью: 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. Сложна в реализации, оправдана для соц-приложений.
// 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% задач.
/orders/{id} без проверки владения. Атакующий перебирает id и читает чужие заказы. Используйте Policies или scoped bindings.Edit в blade при @can — UI-фасад. Атакующий шлёт POST напрямую. Проверяйте на endpoint всегда.is_admin в массовом присваивании. $user->update($request->all()) с is_admin в FormRequest — пользователь сделал себя админом. Whitelist через $fillable.editor, потому что «удобно». Должен быть viewer, с явным повышением.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, ненадёжна сама по себе.
{{-- 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 токены.
GET /logout) — атакующая <img src> разлогинит пользователя.VerifyCsrfToken::$except для удобства. Webhook'и (Stripe, GitHub) приходят без CSRF — в $except. Но добавление случайного endpoint «временно» открывает дыру.SameSite=None без Secure. Браузеры с 2020 блокируют такие cookie. Если нужен кросс-домен — всегда Secure и HTTPS.Content-Type: application/json — вызывает preflight, дополнительная защита.XSS — внедрение JavaScript в страницу, отображаемую другим пользователем. JS исполняется в его браузере с правами вашего домена: читает cookie, session, делает запросы. Защита — правильное экранирование при выводе + Content Security Policy.
Reflected XSS
Параметр запроса попадает в HTML без экранирования. /search?q=<script>...</script> — атакующий шлёт жертве ссылку, при клике запускается JS. Лечится экранированием.
Stored XSS
Вредоносный код сохраняется в БД (комментарий, профиль) и показывается всем пользователям. Самый опасный тип — не требует социального инжиниринга.
DOM-based XSS
Атака на клиентский JS: document.write(location.hash) без escape. Сервер не видит атаки — защита только в браузере.
{{-- Безопасно: {{ }} экранирует HTML --}}
<div>{{ $comment->text }}</div>
{{-- <script>alert(1)</script> → <script>alert(1)</script> --}}
{{-- Опасно: {!! !!} выводит как есть --}}
<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 меняет правила: атакующему нужно ещё обойти политику.
<script>var data = {{ $data }};</script> — если data содержит </script>, scope script ломается. Используйте @json($data).<a href="{{ $link }}"> — если link = javascript:alert(1), клик запускает JS. Валидируйте схему: только http/https.=, +, -, @ — Excel выполнит как формулу. Префиксируйте кавычкой.v-html в Vue, dangerouslySetInnerHTML в React. Прямой аналог {!! !!}. Только после санитизации.'unsafe-inline' в script-src. Делает CSP бесполезной для XSS-защиты. Используйте nonces или hashes для legitimate inline скриптов.<img src>; конвертируйте в PNG.X-XSS-Protection deprecated. Старый header игнорируется современными браузерами. Полагайтесь на CSP.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 | Вредоносный ввод сохраняется в БД, эксплуатируется при последующем запросе |
// Уязвимо — конкатенация
$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 — при ручной конкатенации.
orderBy($_GET['sort']) — SQL injection. Whitelist значений.%, _. Экранируйте или ограничьте.WHERE IN (?) с массивом. Laravel автоматически разворачивает массив. Без библиотеки — вручную: '?' . str_repeat(',?', count($ids)-1).db.users.find({email: req.body.email, password: req.body.password}) с {$ne: null} в password обходит проверку. Тоже валидируйте.EXECUTE с конкатенированным SQL — уязвима так же, как app-код.whereRaw, Doctrine ->andWhere('u.name = '. $name) — те же грабли.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 он должен быть точным доменом.
Классический пример: фронтенд на 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 |
Access-Control-* заголовки. CORS — только для JS в браузере.
* несовместимо с supports_credentials => true — при cookie-auth нужно указывать конкретные домены.
Access-Control-Allow-Origin: *. Открывает API для любого сайта. Допустимо только для публичных read-only API без credentials. Любой private API — явный whitelist.header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']) — все домены проходят. Атакующий получит доступ.Access-Control-Max-Age: 86400 — preflight кешируется на сутки. Меняешь CORS-настройки — пользователи не видят новые до конца кеша.credentials: include без обдумывания. Включение в fetch заставляет посылать cookie. Если эндпоинт не должен принимать аутентифицированные запросы — не включайте.http://localhost:3000 в production-config. Dev-origin остаётся в проде — локальные приложения других разработчиков могут читать ваше API.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'ов).
// 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 с одного источника.
TrustProxies Laravel видит IP балансировщика, не пользователя — все запросы «с одного IP».throttle:10,1 на autocomplete — пользователь набирает быстро, лимит исчерпан. Тюнингуйте по UX.Retry-After заголовок и человеческое сообщение.INCR и EXPIRE — on-disk counter создаёт hot row.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 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;
}
RC4, 3DES, MD5 ciphers. Слабые алгоритмы. Используйте Mozilla SSL config generator для актуального списка.<script src="http://..."> — браузер блокирует. Все ресурсы по HTTPS.preload + HSTS-список Chrome исключает первый запрос.*.example.com в любой системе — компрометация всех поддоменов. Лучше per-subdomain.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-Policy | Whitelist источников скриптов/стилей/картинок. Защита от XSS |
X-Frame-Options: DENY | Запрет iframe. Защита от clickjacking |
X-Content-Type-Options: nosniff | Браузер не угадывает MIME. Защита от MIME-confusion |
Referrer-Policy | Контроль того, что попадает в Referer. strict-origin-when-cross-origin — разумный дефолт |
Permissions-Policy | Whitelist API браузера: camera, microphone, geolocation |
Cross-Origin-Opener-Policy | Изоляция от других origin. Защита от Spectre-class |
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.
Content-Security-Policy-Report-Only — собирайте отчёты, потом включайте.X-Frame-Options: ALLOW-FROM. Не поддерживается Chrome. Используйте frame-ancestors в CSP.'unsafe-eval'. Многие фреймворки требуют. Делает CSP слабее. Постарайтесь обойтись без или используйте strict-dynamic.Server: nginx/1.18.0. Раскрытие версии помогает атакующему. Скройте через server_tokens off.Секреты (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 секретов после ухода каждого сотрудника, после инцидента;
- Принцип наименьших привилегий: каждый сервис имеет только свои ключи.
git filter-repo.config:cache с env-ссылками в коде. Если в коде env('STRIPE_KEY') вне config-файлов — после config:cache вернёт null. Только в config/, в коде — config('services.stripe.key')..env в Docker image. ADD .env в Dockerfile — секреты в image, который потом утечёт в registry. Передавайте через env vars при запуске.::add-mask:: в GitHub Actions).chmod 644 на .env. Доступно всем пользователям сервера. chmod 600 + правильный owner.Разработчик принёс на ревью 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}"];
}
}
user_id из запроса — любой может загрузить аватарку другому. Должно быть $request->user().getClientOriginalName() может вернуть ../../etc/passwd. Используйте Str::random() + расширение из mime.$request->validate(['avatar' => 'image|mimes:jpg,png|max:2048']).max:2048 — пользователь грузит 10 GB.public/avatars/ — нет контроля доступа. Используйте storage/app/private/avatars/ + signed URLs.Str::uuid() для уникальности.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 загрузок в час
false.$fillable. User::create($request->all()) с is_admin в payload./orders/{id} без проверки владения. Используйте Policies + scopeBindings.{!! $userInput !!} в Blade. Прямой XSS. Используйте {{ }}.* + credentials. Открыт для любого сайта.file_get_contents($_GET['url']) — атакующий шлёт http://169.254.169.254/ и читает AWS metadata.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' — браузер запрещает встраивание.
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. При выборе библиотеки убедитесь в защите.
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.