API security checklist 2026: 20 điểm kiểm tra trước khi go-live

99% doanh nghiệp gặp ít nhất một sự cố API security trong năm qua theo dữ liệu 2025. API là attack surface lớn nhất của web application hiện đại — xử lý 90%+ traffic. Bài này là checklist 20 điểm thực tế, có code, dùng để review API ASP.NET Core trước khi go-live hoặc trong security audit định kỳ. ---

Vấn đề

Mỗi khi tôi được giao review một API mới trước khi release, tôi đều dùng một checklist cố định. Không phải vì không tin developer viết code — mà vì security là thứ dễ bị sót khi đang tập trung hoàn thành feature.

Dưới đây là 20 điểm kiểm tra tôi dùng, chia thành 5 nhóm, kèm code mẫu cho ASP.NET Core.


Nhóm 1: Authentication & Authorization (6 điểm)

✅ 1. Token validation đầy đủ

Không chỉ validate signature — validate cả issuer, audience, expiry:

// Program.cs
options.TokenValidationParameters = new TokenValidationParameters
{
    ValidateIssuerSigningKey = true,
    IssuerSigningKey = GetSigningKey(),
    ValidateIssuer = true,
    ValidIssuer = configuration["Jwt:Issuer"],
    ValidateAudience = true,
    ValidAudience = configuration["Jwt:Audience"],
    ValidateLifetime = true,
    ClockSkew = TimeSpan.FromSeconds(30),         // Chặt — không dùng default 5 phút
    ValidAlgorithms = new[] { "RS256" },           // Whitelist algorithm
    RequireSignedTokens = true                     // Reject unsigned token
};

✅ 2. Object-level authorization (không chỉ route-level)

Mỗi endpoint trả về resource cụ thể phải verify ownership:

// ❌ CHỈ route auth — không đủ
[Authorize]
public async Task<IActionResult> GetDocument(int docId)
{
    return Ok(await _db.Documents.FindAsync(docId));
}

// ✅ Object-level auth
[Authorize]
public async Task<IActionResult> GetDocument(int docId)
{
    var userId = User.FindFirst(ClaimTypes.NameIdentifier)!.Value;
    var doc = await _db.Documents
        .FirstOrDefaultAsync(d => d.Id == docId && d.OwnerId == userId);
    if (doc == null) return Forbid();
    return Ok(doc);
}

✅ 3. Access token ngắn hạn + refresh token pattern

// Access token: 15 phút
var accessToken = GenerateJwt(userId, expiry: TimeSpan.FromMinutes(15));

// Refresh token: opaque, lưu server-side, có thể revoke
var refreshToken = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64));
await _db.RefreshTokens.AddAsync(new RefreshToken
{
    Token = Hash(refreshToken),  // Lưu hash, không plain text
    UserId = userId,
    ExpiresAt = DateTime.UtcNow.AddDays(30),
    CreatedAt = DateTime.UtcNow
});

✅ 4. Không accept credentials trong URL

// ❌ API key trong query string — bị log bởi server, proxy, browser history
GET /api/data?api_key=secret123

// ✅ Trong Authorization header
GET /api/data
Authorization: Bearer eyJ...

// Hoặc custom header
X-API-Key: secret123

✅ 5. Rate limiting per user, không chỉ per IP

// Program.cs
builder.Services.AddRateLimiter(options =>
{
    options.AddPolicy("per-user", context =>
    {
        // Rate limit theo userId — không phải IP (dễ bypass bằng proxy)
        var userId = context.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value
            ?? context.Connection.RemoteIpAddress?.ToString()
            ?? "anonymous";

        return RateLimitPartition.GetFixedWindowLimiter(userId, _ =>
            new FixedWindowRateLimiterOptions
            {
                PermitLimit = 100,
                Window = TimeSpan.FromMinutes(1),
                QueueProcessingOrder = QueueProcessingOrder.OldestFirst,
                QueueLimit = 0
            });
    });

    options.RejectionStatusCode = 429;
    options.OnRejected = async (context, token) =>
    {
        context.HttpContext.Response.Headers["Retry-After"] = "60";
        await context.HttpContext.Response.WriteAsync("Rate limit exceeded.", token);
    };
});

// Áp dụng lên sensitive endpoints
[EnableRateLimiting("per-user")]
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] LoginRequest request) { ... }

✅ 6. Phân quyền role/permission granular

// Không chỉ "authenticated" — verify quyền cụ thể
[Authorize(Policy = "CanDeleteInvoice")]
[HttpDelete("invoices/{id}")]
public async Task<IActionResult> DeleteInvoice(int id) { ... }

// Policy
options.AddPolicy("CanDeleteInvoice", policy =>
    policy.RequireClaim("permission", "invoice:delete"));

Nhóm 2: Input Validation & Output Safety (4 điểm)

✅ 7. Validate tất cả input với Data Annotations hoặc FluentValidation

public record CreateOrderRequest
{
    [Required]
    [StringLength(200, MinimumLength = 1)]
    public string ProductName { get; init; } = string.Empty;

    [Range(1, 10000)]
    public decimal Quantity { get; init; }

    [Required]
    [RegularExpression(@"^[A-Z]{2}\d{8}$", ErrorMessage = "Invalid SKU format")]
    public string Sku { get; init; } = string.Empty;
}

// Controller: ModelState tự động validate
[HttpPost]
public async Task<IActionResult> CreateOrder([FromBody] CreateOrderRequest request)
{
    if (!ModelState.IsValid)
        return ValidationProblem(ModelState);
    // ...
}

✅ 8. Request size limit — ngăn payload bombing

// Program.cs — giới hạn toàn bộ API
builder.WebHost.ConfigureKestrel(options =>
{
    options.Limits.MaxRequestBodySize = 10 * 1024 * 1024; // 10MB tối đa
});

// Hoặc per-endpoint nếu cần fine-grained
[RequestSizeLimit(1 * 1024 * 1024)] // 1MB cho endpoint cụ thể
[HttpPost("upload")]
public async Task<IActionResult> Upload(IFormFile file) { ... }

✅ 9. Sanitize sensitive fields trong response

// Không return toàn bộ entity — dùng DTO để control fields
public record UserResponse(
    string Id,
    string DisplayName,
    string Email
    // Không include: PasswordHash, SecurityStamp, InternalNotes
);

// Dùng AutoMapper hoặc manual mapping
var response = new UserResponse(
    Id: user.Id,
    DisplayName: user.DisplayName,
    Email: user.Email
);

✅ 10. Content-Type validation

// Reject request có Content-Type sai — ngăn một số content-type confusion attack
[HttpPost]
[Consumes("application/json")]  // Chỉ chấp nhận JSON
public async Task<IActionResult> CreateResource([FromBody] CreateRequest request)
{
    // Framework tự reject nếu Content-Type không phải application/json
}

Nhóm 3: Transport & Secrets (4 điểm)

✅ 11. HTTPS bắt buộc, redirect HTTP

// Program.cs
app.UseHttpsRedirection();
app.UseHsts(); // Strict-Transport-Security header

// appsettings.json — Kestrel chỉ listen HTTPS trong production
"Kestrel": {
    "Endpoints": {
        "Https": { "Url": "https://*:443" }
    }
}

✅ 12. Secrets trong vault, không trong code hoặc appsettings

// ❌ NGUY HIỂM: hardcode hoặc appsettings (bị commit vào git)
var apiKey = "sk-prod-abc123def456";
var connString = configuration["ConnectionStrings:Default"]; // Nếu file bị expose

// ✅ Azure Key Vault
builder.Configuration.AddAzureKeyVault(
    new Uri($"https://{keyVaultName}.vault.azure.net/"),
    new DefaultAzureCredential()
);

// Hoặc environment variables inject qua CI/CD
var apiKey = Environment.GetEnvironmentVariable("EXTERNAL_API_KEY")
    ?? throw new InvalidOperationException("EXTERNAL_API_KEY not set");

✅ 13. CORS policy tường minh

// ❌ NGUY HIỂM: AllowAnyOrigin cho API có authentication
builder.Services.AddCors(options =>
{
    options.AddPolicy("Production", policy =>
        policy.AllowAnyOrigin()   // Bất kỳ website nào cũng gọi được API
              .AllowAnyMethod()
              .AllowAnyHeader());
});

// ✅ Whitelist explicit origins
builder.Services.AddCors(options =>
{
    options.AddPolicy("Production", policy =>
        policy.WithOrigins(
                "https://app.company.com",
                "https://admin.company.com"
              )
              .WithMethods("GET", "POST", "PUT", "DELETE")
              .WithHeaders("Authorization", "Content-Type")
              .AllowCredentials());
});

✅ 14. Rotate secrets định kỳ

Không phải code — là process. Checklist:

  • [ ] API keys: rotate mỗi 90 ngày hoặc khi nhân sự thay đổi
  • [ ] JWT signing keys: rotate mỗi 6-12 tháng với JWKS rotation
  • [ ] Database credentials: rotate mỗi 90 ngày
  • [ ] Third-party integration keys: review và rotate khi cần

Nhóm 4: Security Headers (3 điểm)

✅ 15. Security headers đầy đủ

// Middleware set headers cho mọi response
app.Use(async (context, next) =>
{
    var headers = context.Response.Headers;

    // Ngăn MIME type sniffing
    headers["X-Content-Type-Options"] = "nosniff";

    // Ngăn clickjacking
    headers["X-Frame-Options"] = "DENY";

    // Ngăn XSS (legacy browsers) — CSP là main protection
    headers["X-XSS-Protection"] = "1; mode=block";

    // Content Security Policy
    headers["Content-Security-Policy"] =
        "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:";

    // Không leak server info
    headers.Remove("Server");
    headers.Remove("X-Powered-By");
    headers.Remove("X-AspNet-Version");

    await next();
});

✅ 16. HSTS với preload

app.UseHsts();

// appsettings.json
"Hsts": {
    "MaxAge": 31536000,      // 1 năm
    "IncludeSubDomains": true,
    "Preload": true           // Submit vào browser preload list
}

✅ 17. Referrer Policy

headers["Referrer-Policy"] = "strict-origin-when-cross-origin";
// Không gửi full URL làm Referer khi cross-origin — ngăn data leak qua referrer header

Nhóm 5: Monitoring & Logging (3 điểm)

✅ 18. Log failed authentication — không log credentials

// ✅ Log đủ để investigate, không log sensitive data
_logger.LogWarning(
    "Authentication failed for user {Email} from IP {IpAddress} at {Timestamp}",
    request.Email,      // Email OK (identifier)
    GetClientIp(context),
    DateTime.UtcNow
);

// ❌ Không log
_logger.LogWarning("Login failed: email={Email}, password={Password}", ...);
_logger.LogWarning("Token: {Token}", jwtToken);  // Token là credential

✅ 19. Alert trên access pattern bất thường

Thiết lập alert (qua Azure Monitor, Datadog, hoặc ELK) cho:

# Ví dụ alert rules
alerts:
  - name: "Rapid sequential ID access"
    condition: "same user, >50 different resource IDs trong 1 phút"
    severity: High

  - name: "Mass auth failures"
    condition: ">10 failed auth trong 1 phút từ cùng IP"
    severity: High

  - name: "Unexpected admin access"
    condition: "admin endpoint access outside business hours"
    severity: Medium

  - name: "Large data export"
    condition: "single response >10MB hoặc >1000 records"
    severity: Medium

✅ 20. Health check không expose internal state

// ❌ Health check tiết lộ connection strings, versions, internal topology
app.MapHealthChecks("/health", new HealthCheckOptions
{
    ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse
    // Response có thể bao gồm: DB host, version, internal service names
});

// ✅ Health check cho public: minimal, không sensitive data
app.MapHealthChecks("/health/live", new HealthCheckOptions
{
    Predicate = _ => false, // Chỉ liveness — không check dependencies
    ResponseWriter = (context, report) =>
    {
        context.Response.ContentType = "application/json";
        return context.Response.WriteAsync(
            report.Status == HealthStatus.Healthy ? "{\"status\":\"ok\"}" : "{\"status\":\"degraded\"}"
        );
    }
});

// Health check đầy đủ (DB, external services) chỉ cho internal monitoring
app.MapHealthChecks("/health/ready")
   .RequireAuthorization("InternalMonitoring");

Checklist tổng hợp — copy-paste cho PR review

## API Security Review Checklist

### Authentication & Authorization
- [ ] JWT validation đầy đủ (issuer, audience, algorithm whitelist, expiry)
- [ ] Object-level authorization cho mọi resource endpoint
- [ ] Access token < 15 phút + refresh token pattern
- [ ] Credentials không trong URL (query string)
- [ ] Rate limiting per user trên sensitive endpoints
- [ ] Role/permission granular (không chỉ "authenticated")

### Input & Output
- [ ] Input validation với Data Annotations hoặc FluentValidation
- [ ] Request body size limit
- [ ] Response DTO — không return raw entity với sensitive fields
- [ ] Content-Type validation (Consumes attribute)

### Transport & Secrets
- [ ] HTTPS bắt buộc, HTTP redirect
- [ ] Secrets trong vault/environment — không trong code
- [ ] CORS whitelist explicit origins
- [ ] Secret rotation schedule documented

### Headers
- [ ] Security headers (X-Content-Type-Options, X-Frame-Options, CSP)
- [ ] HSTS enabled
- [ ] Referrer-Policy set
- [ ] Server header removed

### Monitoring
- [ ] Failed auth events logged (không log credentials)
- [ ] Alert rules cho anomalous access pattern
- [ ] Health check public endpoint không expose internal state

Kết

20 điểm này không phải lý thuyết — đây là thứ tôi check trong mọi API audit. Không cần implement đủ 20 trong sprint đầu tiên, nhưng 6 điểm đầu tiên (Authentication & Authorization) là non-negotiable trước khi bất kỳ API nào đi vào production với real user data.

Bài tiếp theo: Security Misconfiguration trong ASP.NET Core — OWASP A02 đã nhảy từ #5 lên #2, cụ thể là debug endpoints, CORS, security headers, và error handling trong production environment.


Thiết Kiếm — BKGlobal Tech Team

#BKGlobal #appsec #security #api #owasp #securecoding #dotnet

Bảo mật & AppSec

Xem tất cả