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