Безпека на практиці: CORS, HTTPS та захист від атак
Безпека на практиці: CORS, HTTPS та захист від атак
1. CORS — Cross-Origin Resource Sharing
Проблема
Ваш API працює на https://api.myshop.com. Ваш SPA (React/Vue) — на https://myshop.com. Браузер блокує запити між різними доменами (origins) — це політика Same-Origin Policy.
Без CORS ваш фронтенд отримає помилку:
Access to fetch at 'https://api.myshop.com/products'
from origin 'https://myshop.com' has been blocked
by CORS policy.
Що таке Origin?
Origin = протокол + домен + порт:
| URL | Origin |
|---|---|
https://myshop.com/page | https://myshop.com |
https://api.myshop.com/v1 | https://api.myshop.com |
http://myshop.com | http://myshop.com (інший протокол!) |
https://myshop.com:8080 | https://myshop.com:8080 (інший порт!) |
Два URL мають різні origin, якщо відрізняється хоча б одна з трьох частин.
Налаштування CORS у Minimal API
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddCors(options =>
{
// Іменована політика для production
options.AddPolicy("Production", policy =>
{
policy
// ✅ Тільки ваші домени
.WithOrigins(
"https://myshop.com",
"https://admin.myshop.com")
// ✅ Тільки потрібні методи
.WithMethods(
"GET", "POST", "PUT", "DELETE")
// ✅ Тільки потрібні заголовки
.WithHeaders(
"Authorization",
"Content-Type",
"Accept")
// ✅ Дозволити credentials (cookies)
.AllowCredentials();
});
// Окрема політика для dev
options.AddPolicy("Development", policy =>
{
policy
.WithOrigins(
"http://localhost:3000",
"http://localhost:5173")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials();
});
});
var app = builder.Build();
// Обираємо політику залежно від середовища
if (app.Environment.IsDevelopment())
app.UseCors("Development");
else
app.UseCors("Production");
CORS для Route Groups
// Публічний API — дозволяємо CORS
var publicApi = app.MapGroup("/api/v1")
.RequireCors("Production");
// Внутрішній API — без CORS (server-to-server)
var internalApi = app.MapGroup("/internal");
// CORS не потрібен — запити не з браузера
Антипатерни CORS
// ❌ НІКОЛИ у production!
policy.AllowAnyOrigin();
Дозволяє будь-якому сайту робити запити до вашого API. Зловмисник може створити фішинговий сайт, який крадитиме дані ваших користувачів.
// 💥 Взагалі не скомпілюється!
policy.AllowAnyOrigin().AllowCredentials();
ASP.NET Core забороняє цю комбінацію. Якщо CORS дозволяє будь-який origin і credentials одночасно — це повний bypass аутентифікації.
// ❌ Не працює з credentials!
policy.WithOrigins("https://*.myshop.com");
ASP.NET Core не підтримує wildcard у WithOrigins. Кожен origin має бути вказаний повністю. Для динамічних origins — використовуйте SetIsOriginAllowed().
Preflight-запити
Для «небезпечних» запитів (POST з Content-Type: application/json, PUT, DELETE) браузер спочатку надсилає preflight запит — OPTIONS:
2. HTTPS та HSTS
Чому HTTPS обов'язковий
Без HTTPS (TLS-шифрування) дані між клієнтом і сервером передаються відкритим текстом. JWT-токени, паролі, персональні дані — все видно зловмиснику.
Налаштування
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
// Перенаправляє HTTP → HTTPS
app.UseHttpsRedirection();
// HSTS — каже браузеру:
// "Наступні N секунд звертайся тільки по HTTPS"
app.UseHsts();
}
307 Temporary Redirect на HTTPS-версію URL. Наприклад: http://api.myshop.com/products → https://api.myshop.com/products.Strict-Transport-Security до відповіді. Після отримання цього заголовка браузер ніколи не робитиме HTTP-запити до цього домену — навіть якщо користувач вручну введе http://.Dev-сертифікат
Для локальної розробки ASP.NET Core має вбудований dev-сертифікат:
# Генерувати та довірити dev-сертифікат
dotnet dev-certs https --trust
# Перевірити
dotnet dev-certs https --check
3. Захист від CSRF
Що таке CSRF?
CSRF (Cross-Site Request Forgery) — атака, де зловмисний сайт змушує браузер надіслати запит до вашого API від імені залогіненого користувача.
Коли CSRF актуальний?
| Аутентифікація | CSRF-ризик | Чому |
|---|---|---|
| JWT Bearer | ❌ Немає | Токен не передається автоматично |
| Cookie Auth | ⚠️ Є | Cookie передається автоматично! |
| Cookie + SameSite=Strict | ✅ Захищено | Браузер не передає cookie cross-origin |
Authorization.Якщо ви використовуєте Cookie Auth — захистіться через SameSite=Strict (найпростіше) або Anti-Forgery Tokens.Anti-Forgery у Minimal API
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAntiforgery();
var app = builder.Build();
app.UseAntiforgery();
// Ендпоінт, що отримує CSRF-токен
app.MapGet("/antiforgery/token",
(IAntiforgery antiforgery, HttpContext ctx) =>
{
var tokens = antiforgery.GetAndStoreTokens(ctx);
return Results.Ok(new
{
token = tokens.RequestToken,
headerName = tokens.HeaderName
});
}).AllowAnonymous();
// Захищеий ендпоінт — перевіряє CSRF
app.MapPost("/orders", (OrderRequest req) =>
Results.Created("/orders/1", req))
.RequireAuthorization()
.ValidateAntiforgery(); // ← Perевірка CSRF-токена
4. Rate Limiting
Навіщо обмежувати запити?
Без rate limiting зловмисник може:
- Brute-force паролі (мільйони спроб за хвилину)
- DDoS — перевантажити сервер запитами
- Scraping — вичитати всі дані з API
Вбудований Rate Limiting (.NET 7+)
using System.Threading.RateLimiting;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRateLimiter(options =>
{
// Глобальний ліміт: 100 запитів / хвилина
options.GlobalLimiter = PartitionedRateLimiter
.Create<HttpContext, string>(ctx =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey:
ctx.Connection.RemoteIpAddress?
.ToString() ?? "unknown",
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1)
}));
// Іменована політика для auth ендпоінтів
options.AddFixedWindowLimiter("auth", opt =>
{
opt.PermitLimit = 5;
opt.Window = TimeSpan.FromMinutes(1);
});
// Що повертати при перевищенні?
options.RejectionStatusCode = 429; // Too Many Requests
});
var app = builder.Build();
app.UseRateLimiter();
Різні ліміти для різних ендпоінтів
// Auth — максимально суворо (brute-force захист)
app.MapPost("/auth/login", Login)
.RequireRateLimiting("auth"); // 5/хв
// Публічне читання — вільніше
app.MapGet("/products", GetProducts);
// Глобальний ліміт: 100/хв
// Запис — помірно
app.MapPost("/orders", CreateOrder)
.RequireRateLimiting("write"); // 30/хв
Типи Rate Limiters
| Тип | Як працює | Коли використовувати |
|---|---|---|
| Fixed Window | N запитів за фіксований період (наприклад, 100/хв) | Простий і передбачуваний |
| Sliding Window | Як Fixed, але вікно «ковзає» | Плавніший розподіл навантаження |
| Token Bucket | Токени накопичуються поступово | API з бурстами трафіку |
| Concurrency | Обмежує одночасні запити | Захист від тривалих операцій |
5. Заголовки безпеки
Кожна HTTP-відповідь має містити заголовки, що захищають від поширених атак:
app.Use(async (ctx, next) =>
{
var headers = ctx.Response.Headers;
// Заборона відображення у iframe
// (захист від Clickjacking)
headers["X-Frame-Options"] = "DENY";
// Блокувати MIME-sniffing
// (браузер не «вгадує» тип контенту)
headers["X-Content-Type-Options"] = "nosniff";
// Суворий TLS (браузер запам'ятовує HTTPS)
headers["Strict-Transport-Security"] =
"max-age=31536000; includeSubDomains";
// Не передавати Referrer при переході
// на зовнішні сайти
headers["Referrer-Policy"] =
"strict-origin-when-cross-origin";
// Content Security Policy
// (захист від XSS)
headers["Content-Security-Policy"] =
"default-src 'self'";
// Вимкнути кеш для API-відповідей
// з персональними даними
headers["Cache-Control"] =
"no-store, no-cache";
await next(ctx);
});
Пояснення заголовків
<iframe>. Захищає від Clickjacking — атаки, де ваш сайт вбудований у прозорий iframe поверх фішингової сторінки.max-age секунд не робитиме HTTP-запити. Навіть якщо користувач введе http:// — браузер автоматично переключиться на https://.default-src 'self' — тільки з вашого домену.6. Зберігання секретів
Ієрархія секретів
| Середовище | Де зберігати | Як доступити |
|---|---|---|
| Development | dotnet user-secrets | builder.Configuration["Key"] |
| Staging/CI | Environment Variables | builder.Configuration["Key"] |
| Production | Azure Key Vault / AWS Secrets Manager | Спеціальний провайдер |
User Secrets (development)
# Ініціалізація
dotnet user-secrets init
# Додати секрет
dotnet user-secrets set "Jwt:Key" "MyDev..."
dotnet user-secrets set "Google:ClientId" "abc..."
dotnet user-secrets set "Google:ClientSecret" "xyz..."
# Переглянути всі секрети
dotnet user-secrets list
# Видалити
dotnet user-secrets remove "Jwt:Key"
Секрети зберігаються у:
- Windows:
%APPDATA%\Microsoft\UserSecrets\<UserSecretsId>\secrets.json - macOS/Linux:
~/.microsoft/usersecrets/<UserSecretsId>/secrets.json
Environment Variables (production)
# Linux / Docker
export Jwt__Key="ProductionSecret..."
export ConnectionStrings__Default="Server=..."
# Docker Compose
services:
api:
environment:
- Jwt__Key=ProductionSecret...
- ConnectionStrings__Default=Server=...
__ (подвійне підкреслення), а не : як в JSON. Jwt:Key → Jwt__Key.Що НІКОЛИ не зберігати в коді
// appsettings.json — потрапить у Git!
{
"Jwt": {
"Key": "SuperSecretKey123!!!"
},
"ConnectionStrings": {
"Default": "Server=prod;Password=admin123"
}
}
// Хардкод у коді — ще гірше!
var key = "SuperSecretKey123!!!";
// appsettings.json — тільки несекретні дані
{
"Jwt": {
"Issuer": "MyApi",
"Audience": "MyApp"
}
}
// Секрети — в User Secrets або env variables
7. Production чеклист
Перед кожним деплоєм перевірте цей чеклист:
Аутентифікація
- JWT-ключ зберігається поза кодом (User Secrets / env / vault)
- JWT-ключ ≥ 32 символи (для HS256)
-
ValidateIssuerSigningKey = true -
ValidateLifetime = true,ClockSkew = TimeSpan.Zero - Access Token ≤ 15 хвилин
- Refresh Token зберігається в БД та ротується
Авторизація
- Fallback Policy або явний
RequireAuthorization()на кожному ендпоінті - Публічні ендпоінти мають явний
.AllowAnonymous() - Resource-based auth для операцій із чужими ресурсами
HTTPS
-
UseHttpsRedirection()увімкнено -
UseHsts()увімкнено - Валідний SSL-сертифікат (Let's Encrypt або комерційний)
- Cookie:
SecurePolicy = Always
CORS
- Перертій origins (не
AllowAnyOrigin()) - Перелічені дозволені методи та заголовки
-
AllowCredentials()тільки з конкретними origins
Захист від атак
- Rate Limiting на auth-ендпоінтах (≤ 5-10 запитів/хвилину)
- Account Lockout після 5 невдалих спроб
- Security Headers (X-Frame-Options, X-Content-Type-Options, CSP, HSTS)
- Cookie:
HttpOnly = true,SameSite = Strict - Паролі хешуються (PBKDF2/BCrypt/Argon2), не plaintext
- Логи не містять паролів, токенів та персональних даних
Загальне
-
appsettings.jsonне містить секретів -
.gitignoreвключаєsecrets.json,.env - Swagger/OpenAPI вимкнений у production (або за авторизацією)
8. Практичні завдання
Рівень 1: Базовий
Налаштуйте CORS для SPA:
- Створіть політику
"Frontend"з originhttp://localhost:5173(Vite dev server) - Дозвольте методи GET, POST, PUT, DELETE
- Дозвольте заголовки
AuthorizationтаContent-Type - Протестуйте: запит з
http://localhost:5173→ працює. Запит зhttp://evil-site.com→ блокується - Спробуйте
AllowAnyOrigin()— яка помилка виникне при спробі додатиAllowCredentials()?
Додайте Rate Limiting:
- Глобальний ліміт: 60 запитів / хвилина (per IP)
- Окрема політика
"auth": 5 запитів / хвилина дляPOST /auth/login - Протестуйте: надішліть 6 запитів на login за 10 секунд — що повертає 6-й запит?
- Переконайтесь, що статус-код
429 Too Many Requestsповертається з правильним повідомленням
Рівень 2: Проєктування
Створіть middleware з усіма заголовками безпеки:
X-Frame-Options: DENYX-Content-Type-Options: nosniffStrict-Transport-Security: max-age=31536000; includeSubDomainsReferrer-Policy: strict-origin-when-cross-originContent-Security-Policy: default-src 'self'- Перевірте заголовки через DevTools → Network tab → Response Headers
- Протестуйте через securityheaders.com — досягніть оцінки A+
Рівень 3: Архітектура
Зберіть усе разом у production-ready API:
- CORS — тільки для вашого frontend URL
- HTTPS Redirection + HSTS
- Rate Limiting: 100/хв глобально, 5/хв для auth
- Security Headers middleware
- JWT з коротким access token (15 хв) та refresh token
- Identity з lockout (5 спроб)
- Fallback Policy — все закрито за замовчуванням
- User Secrets для dev, env variables для production
- Пройдіть production чеклист і переконайтесь, що всі пункти виконані
9. Резюме
CORS — точковий дозвіл
HTTPS — обов'язковий
Rate Limiting — захист від brute-force
Security Headers
OAuth 2.0 та зовнішні провайдери
Підключення «Увійти через Google/GitHub» в Minimal API: OAuth 2.0 Authorization Code Flow, OpenID Connect, Claims Transformation, зв'язка з локальною БД.
Теорія OAuth 2.0: Поняття, Аналогії та Флоу
Грунтовне, теоретичне пояснення OAuth 2.0 та OpenID Connect через життєві аналогії. Розбір учасників, токенів та всіх основних Authorization Flows (Grant Types) без єдиного рядка коду.