Auth

Безпека на практиці: CORS, HTTPS та захист від атак

Повна конфігурація безпеки Minimal API: CORS, HTTPS Redirection, HSTS, CSRF-захист, Rate Limiting, заголовки безпеки, зберігання секретів та production чеклист.

Безпека на практиці: CORS, HTTPS та захист від атак

Ваш API працює, аутентифікація налаштована, авторизація перевіряє ролі. Але це лише частина безпеки. У цій статті ми розглянемо практичні заходи захисту: від CORS та HTTPS до rate limiting та security headers. Наприкінці — чеклист, який потрібно пройти перед кожним деплоєм у production.

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 = протокол + домен + порт:

URLOrigin
https://myshop.com/pagehttps://myshop.com
https://api.myshop.com/v1https://api.myshop.com
http://myshop.comhttp://myshop.com (інший протокол!)
https://myshop.com:8080https://myshop.com:8080 (інший порт!)

Два URL мають різні origin, якщо відрізняється хоча б одна з трьох частин.

Налаштування CORS у Minimal API

Program.cs — CORS
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

CORS на рівні групи
// Публічний API — дозволяємо CORS
var publicApi = app.MapGroup("/api/v1")
    .RequireCors("Production");

// Внутрішній API — без CORS (server-to-server)
var internalApi = app.MapGroup("/internal");
// CORS не потрібен — запити не з браузера

Антипатерни CORS

Preflight-запити

Для «небезпечних» запитів (POST з Content-Type: application/json, PUT, DELETE) браузер спочатку надсилає preflight запит — OPTIONS:

Loading diagram...
sequenceDiagram
    participant B as 🌐 Браузер
    participant A as 🛡️ API

    B->>A: OPTIONS /api/v1/products<br/>Origin: https://myshop.com<br/>Access-Control-Request-Method: POST

    Note over A: CORS middleware перевіряє<br/>origin та метод

    A-->>B: 204 No Content<br/>Access-Control-Allow-Origin: https://myshop.com<br/>Access-Control-Allow-Methods: POST

    Note over B: Preflight пройшов ✅<br/>Тепер можна надіслати реальний запит

    B->>A: POST /api/v1/products<br/>Origin: https://myshop.com<br/>Content-Type: application/json

    A-->>B: 201 Created

2. HTTPS та HSTS

Чому HTTPS обов'язковий

Без HTTPS (TLS-шифрування) дані між клієнтом і сервером передаються відкритим текстом. JWT-токени, паролі, персональні дані — все видно зловмиснику.

Налаштування

Program.cs — HTTPS
var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    // Перенаправляє HTTP → HTTPS
    app.UseHttpsRedirection();

    // HSTS — каже браузеру:
    // "Наступні N секунд звертайся тільки по HTTPS"
    app.UseHsts();
}
UseHttpsRedirection()
middleware
Перехоплює HTTP-запити і повертає 307 Temporary Redirect на HTTPS-версію URL. Наприклад: http://api.myshop.com/productshttps://api.myshop.com/products.
UseHsts()
middleware
Додає заголовок 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 від імені залогіненого користувача.

Loading diagram...
sequenceDiagram
    participant V as 👤 Жертва
    participant E as 😈 evil-site.com
    participant A as 🛡️ Ваш API

    V->>A: Логін (cookie встановлено)
    V->>E: Жертва заходить на evil-site.com

    Note over E: Сторінка містить:<br/>form action="api.myshop.com/orders"<br/>method="POST"

    E->>A: POST /orders (cookie автоматично!)<br/>Cookie: session=abc123

    Note over A: API бачить валідну cookie<br/>і виконує запит! 😱

Коли CSRF актуальний?

АутентифікаціяCSRF-ризикЧому
JWT Bearer❌ НемаєТокен не передається автоматично
Cookie Auth⚠️ ЄCookie передається автоматично!
Cookie + SameSite=Strict✅ ЗахищеноБраузер не передає cookie cross-origin
Якщо ваш API використовує тільки JWT Bearer — CSRF вам не загрожує. Токен зберігається в пам'яті JavaScript і передається вручну через заголовок Authorization.Якщо ви використовуєте Cookie Auth — захистіться через SameSite=Strict (найпростіше) або Anti-Forgery Tokens.

Anti-Forgery у Minimal API

CSRF-захист для Cookie Auth
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+)

Program.cs — Rate Limiting
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();

Різні ліміти для різних ендпоінтів

Rate Limiting по групах
// 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 WindowN запитів за фіксований період (наприклад, 100/хв)Простий і передбачуваний
Sliding WindowЯк Fixed, але вікно «ковзає»Плавніший розподіл навантаження
Token BucketТокени накопичуються поступовоAPI з бурстами трафіку
ConcurrencyОбмежує одночасні запитиЗахист від тривалих операцій

5. Заголовки безпеки

Кожна HTTP-відповідь має містити заголовки, що захищають від поширених атак:

Middleware заголовків безпеки
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);
});

Пояснення заголовків

X-Frame-Options: DENY
header
Забороняє сторінці відображатися в <iframe>. Захищає від Clickjacking — атаки, де ваш сайт вбудований у прозорий iframe поверх фішингової сторінки.
X-Content-Type-Options: nosniff
header
Забороняє браузеру «вгадувати» MIME-тип. Без цього браузер може інтерпретувати JSON як HTML і виконати вбудований скрипт.
Strict-Transport-Security
header
Після отримання цього заголовка браузер протягом max-age секунд не робитиме HTTP-запити. Навіть якщо користувач введе http:// — браузер автоматично переключиться на https://.
Content-Security-Policy
header
Визначає, з яких джерел можна завантажувати скрипти, стилі, зображення. default-src 'self' — тільки з вашого домену.

6. Зберігання секретів

Ієрархія секретів

СередовищеДе зберігатиЯк доступити
Developmentdotnet user-secretsbuilder.Configuration["Key"]
Staging/CIEnvironment Variablesbuilder.Configuration["Key"]
ProductionAzure 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=...
Роздільник: у env-змінних вкладеність позначається __ (подвійне підкреслення), а не : як в JSON. Jwt:KeyJwt__Key.

Що НІКОЛИ не зберігати в коді

// appsettings.json — потрапить у Git!
{
    "Jwt": {
        "Key": "SuperSecretKey123!!!"
    },
    "ConnectionStrings": {
        "Default": "Server=prod;Password=admin123"
    }
}
// Хардкод у коді — ще гірше!
var key = "SuperSecretKey123!!!";

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: Базовий

Рівень 2: Проєктування

Рівень 3: Архітектура


9. Резюме

CORS — точковий дозвіл

Тільки конкретні origins, методи та заголовки. Ніколи AllowAnyOrigin() у production. Окрема політика для dev і prod.

HTTPS — обов'язковий

UseHttpsRedirection + UseHsts. Валідний сертифікат. Cookie: Secure=Always. Без HTTPS токени та паролі відкриті.

Rate Limiting — захист від brute-force

5 запитів/хвилина на login. 100/хвилина глобально. Повертайте 429 із Retry-After.

Security Headers

X-Frame-Options, HSTS, CSP, nosniff — мінімальний набір. Перевірка через securityheaders.com → A+.
Copyright © 2026