Vấn đề: Khi API trở thành cánh cửa mở
Tháng 9 năm 2025, một fintech startup ở HCM mất gần 5 tỷ đồng trong vòng 2 giờ. Không phải qua SQL injection hay ransomware-mà qua một API endpoint mà team quên yêu cầu authentication.
Endpoint đó gốc là GET /api/v1/health-check-dùng để kiểm tra máy chủ có sống không. Nhưng kỹ sư cũ để lọt logic transfer tiền vào file đó. Không ai ngờ. Attacker tìm được thông qua GitHub reconnaissance, gửi hàng triệu request, ăn cắp dữ liệu tài khoản. Xong.
Đó không phải trường hợp lẻ. Theo báo cáo 42Crunch 2026, 97% API vulnerabilities có thể bị khai thác với một request duy nhất. Gần 99% có thể access từ xa. Còn 59% không cần authentication gì hết.
Concept: OWASP API Top 10 2026 - Những gì chúng tôi phải chống lại
Chúng tôi không thể bảo vệ cái mình không hiểu. Vì vậy mở đầu từ top vulnerabilities:
1. Broken Object Level Authorization (BOLA / IDOR)
Khi bạn cho phép user truy cập vào object của user khác qua direct reference mà không validate quyền.
Ví dụ thực tế:
// ❌ VULNERABLE - Attacker đổi user_id trong URL
[HttpGet("/api/users/{userId}/profile")]
public ActionResult<UserProfile> GetUserProfile(int userId)
{
var profile = _db.Users.FirstOrDefault(u => u.Id == userId);
return Ok(profile); // Không kiểm tra current user có quyền xem không
}
// ✓ SECURE - Validate authorization trước
[HttpGet("/api/users/{userId}/profile")]
[Authorize]
public ActionResult<UserProfile> GetUserProfile(int userId)
{
var currentUser = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
// Chỉ user chính hoặc admin mới được xem
if (currentUser != userId.ToString() && !User.IsInRole("Admin"))
{
return Forbid("You don't have permission to access this profile");
}
var profile = _db.Users.FirstOrDefault(u => u.Id == userId);
return Ok(profile);
}
2. Broken Authentication
Không phải mỗi request đều có authentication token. Nhiều team để cho API endpoints hoạt động mà không kiểm tra:
- Health checks mà chứa logic thực
- Internal endpoints (kiểu như admin panel) nhưng exposed
- Legacy API versions vẫn chạy với old auth scheme (Basic Auth)
- Implicit grant flows cho OAuth (không có PKCE)
Thực tế 2026: Một fintech châu Á để lại endpoint /api/internal/calculate-interest-rate mà quên thêm [Authorize]. Attacker đổi tỷ lệ lãi suất trong database qua endpoint đó, gây lỗ hổng trên hàng chục khoản vay.
3. Unrestricted Resource Consumption (DoS)
Không có rate limit hoặc resource limit. Attacker spam API hàng triệu request, làm server crash hoặc tăng bill cloud.
4. Security Misconfiguration
Để debug mode bật ở production, return error messages chi tiết, không set CORS đúng, không validate HTTPS.
Checklist Hành Động: 10 Bước Bắt Buộc
Chúng tôi đã xem qua hàng trăm API audit. Đây là checklist mà team chúng tôi dùng cho mỗi API endpoint:
1. Authentication on Every Endpoint (Không ngoại lệ)
[ApiController]
[Route("api/[controller]")]
[Authorize] // ← Bắt buộc. Ngoại lệ phải có [AllowAnonymous]
public class OrdersController : ControllerBase
{
[HttpGet("{id}")]
public ActionResult<Order> GetOrder(int id)
{
return Ok(_db.Orders.Find(id));
}
[HttpPost]
[Authorize(Roles = "Admin")] // ← Role-based access khi cần
public ActionResult CreateOrder([FromBody] CreateOrderRequest req)
{
// ...
}
[HttpGet("health")]
[AllowAnonymous] // ← Ngoại lệ rõ ràng & không chứa logic thực
public ActionResult Health()
{
return Ok("OK");
}
}
Checklist:
- [ ] Mỗi endpoint có
[Authorize]hoặc rõ ràng explain tại sao[AllowAnonymous] - [ ] Ngoại lệ (health checks) không chứa bất kỳ business logic nào
- [ ] JWT token hoặc session được validate chặt chẽ
2. Implement Object Level Authorization
[HttpGet("orders/{orderId}")]
[Authorize]
public ActionResult<Order> GetOrder(int orderId)
{
var currentUserId = User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
var order = _db.Orders
.Include(o => o.Customer)
.FirstOrDefault(o => o.Id == orderId);
if (order == null) return NotFound();
// Kiểm tra: user này sở hữu order này không?
if (order.CustomerId.ToString() != currentUserId && !User.IsInRole("Admin"))
{
return Forbid();
}
return Ok(order);
}
Checklist:
- [ ] Mỗi query lấy data từ DB đều lọc theo current user (trừ Admin)
- [ ] Kiểm tra resource ownership trước khi return data
- [ ] Test case: verify user A không thể xem order của user B
3. Disable Debug Mode & Hide Version Headers
// Program.cs
var builder = WebApplication.CreateBuilder(args);
// Chỉ enable detail errors ở development
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/api/error");
app.UseHsts();
// Ẩn Server header (reveal framework version)
app.Use(async (context, next) =>
{
context.Response.Headers.Remove("Server");
context.Response.Headers.Remove("X-Powered-By");
context.Response.Headers.Remove("X-AspNet-Version");
await next();
});
}
app.UseHttpsRedirection();
app.UseCors();
Checklist:
- [ ]
clean - [ ] Error pages không leak stack traces ở production
- [ ] Debug symbols stripped from production binary
4. Implement Rate Limiting & Throttling
// Thêm vào Program.cs
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
// Global limit: 100 requests per minute
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? httpContext.Connection.RemoteIpAddress?.ToString() ?? "anonymous",
factory: partition => new FixedWindowRateLimiterOptions
{
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1)
}));
});
app.UseRateLimiter();
// Hoặc per-endpoint
[HttpGet("sensitive-operation")]
[Authorize]
[RequireRateLimiting("strict")] // ← 10 req/minute
public ActionResult SensitiveOp()
{
return Ok();
}
Checklist:
- [ ] Rate limit set trên tất cả public endpoints
- [ ] Sensitive operations (transfer, delete) có limit chặt hơn
- [ ] Monitor rate limit violations via logging
5. Validate Input Strictly (Allowlist, Not Denylist)
[HttpPost("orders")]
public ActionResult CreateOrder([FromBody] CreateOrderRequest req)
{
// ❌ Denylist (yếu)
// if (!req.Description.Contains("DROP TABLE")) ...
// ✓ Allowlist (mạnh)
var validation = new OrderValidator();
var result = validation.Validate(req);
if (!result.IsValid) return BadRequest(result.Errors);
// Parameterized query (EF Core handles this)
_db.Orders.Add(new Order
{
CustomerId = int.Parse(req.CustomerId), // Validate integer
Amount = decimal.Parse(req.Amount),
Description = req.Description[..100] // Truncate to max length
});
_db.SaveChanges();
return CreatedAtAction(nameof(GetOrder), new { id = order.Id });
}
FluentValidation example:
public class CreateOrderRequestValidator : AbstractValidator<CreateOrderRequest>
{
public CreateOrderRequestValidator()
{
RuleFor(x => x.CustomerId)
.NotEmpty()
.Matches(@"^\d+$"); // Only digits
RuleFor(x => x.Amount)
.GreaterThan(0)
.LessThanOrEqualTo(1000000);
RuleFor(x => x.Description)
.MaximumLength(500)
.Matches(@"^[a-zA-Z0-9\s\-.,]+$"); // Whitelist characters
}
}
Checklist:
- [ ] Input validation library (FluentValidation, DataAnnotations)
- [ ] Allowlist regex patterns, not blacklist
- [ ] Database queries use parameterized queries (EF Core default)
6. Use HTTPS Everywhere & Set Security Headers
// Program.cs
app.UseHttpsRedirection();
app.UseHsts(); // Add 'Strict-Transport-Security' header
app.Use(async (context, next) =>
{
context.Response.Headers.Add("X-Content-Type-Options", "nosniff");
context.Response.Headers.Add("X-Frame-Options", "DENY");
context.Response.Headers.Add("X-XSS-Protection", "1; mode=block");
context.Response.Headers.Add("Content-Security-Policy", "default-src 'self'");
await next();
});
Checklist:
- [ ] All HTTP redirects to HTTPS
- [ ] HSTS enabled (preload list recommended)
- [ ] CORS policy strict (không
Access-Control-Allow-Origin: *) - [ ] CSP header set
7. Audit & Log API Calls (Không log sensitive data)
app.Use(async (context, next) =>
{
var userId = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value ?? "anonymous";
var path = context.Request.Path;
var method = context.Request.Method;
var sw = System.Diagnostics.Stopwatch.StartNew();
await next();
sw.Stop();
// Log (nhưng không log body nếu chứa passwords, tokens)
_logger.LogInformation(
"API Call: {Method} {Path} | User: {UserId} | Status: {StatusCode} | Duration: {Duration}ms",
method, path, userId, context.Response.StatusCode, sw.ElapsedMilliseconds
);
// Alert if suspicious
if (context.Response.StatusCode >= 400 && context.Response.StatusCode != 404)
{
_logger.LogWarning("Failed API call: {Method} {Path} returned {Status}", method, path, context.Response.StatusCode);
}
});
Checklist:
- [ ] Logging middleware records auth failures
- [ ] No passwords, tokens, or PII in logs
- [ ] Centralized logging (ELK, Application Insights)
8. Manage API Versions Carefully
// ✓ Version endpoint explicitly
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
public class OrdersController : ControllerBase
{
[HttpGet("{id}"), MapToApiVersion("1.0")]
public ActionResult<OrderV1> GetOrderV1(int id) { /* legacy */ }
[HttpGet("{id}"), MapToApiVersion("2.0")]
public ActionResult<OrderV2> GetOrderV2(int id) { /* new */ }
}
// ❌ Don't leave old versions running indefinitely
// They often have older dependencies with CVEs
Checklist:
- [ ] API versioning strategy documented
- [ ] Deprecate old versions (e.g., v1 sunset date: 2026-12-31)
- [ ] Monitor traffic on deprecated versions
- [ ] Audit old versions for outdated libraries
9. Protect Against SSRF & Open Redirects
[HttpGet("fetch-external-data")]
[Authorize]
public async Task<ActionResult> FetchExternalData([FromQuery] string url)
{
// ❌ Vulnerable to SSRF
// var data = await _httpClient.GetStringAsync(url);
// ✓ Whitelist allowed domains
var allowedDomains = new[]
{
"https://api.trusted-partner.com",
"https://data.trusted-source.io"
};
var uri = new Uri(url);
if (!allowedDomains.Any(d => uri.Host == new Uri(d).Host))
{
return BadRequest("URL not in whitelist");
}
// Prevent redirect to malicious site
var handler = new HttpClientHandler
{
AllowAutoRedirect = false // Explicit control
};
using (var client = new HttpClient(handler))
{
var response = await client.GetAsync(url);
if (response.StatusCode == System.Net.HttpStatusCode.Redirect)
{
var location = response.Headers.Location?.ToString();
// Verify redirect target is safe
}
}
return Ok(await response.Content.ReadAsStringAsync());
}
Checklist:
- [ ] External URL requests whitelisted
- [ ] Redirect targets validated
- [ ] HTTP client library patched to latest version
10. Dependency & Supply Chain Security
// Program.cs
// Package.csproj - update regularly
// ❌ Old: <PackageReference Include="Newtonsoft.Json" Version="12.0.1" />
// ✓ New: <PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
// Monitor dependencies
// dotnet list package --outdated
// dotnet list package --vulnerable
Checklist:
- [ ]
dotnet list package --vulnerableruns clean in CI/CD - [ ] Dependency update policy (e.g., monthly security patches)
- [ ] NuGet package signature verification (nuget.config)
- [ ] SCA tool (Snyk, Dependabot, GitHub Advanced Security)
So Sánh: Good vs Bad API
| Tiêu chí | ❌ Bad | ✓ Good |
|---|---|---|
| Auth | Endpoints không [Authorize] |
Mỗi endpoint rõ ràng authorize |
| Object Access | User A xem dữ liệu User B qua ID | Object ownership validated |
| Rate Limit | Không limit | 100 req/min/user, 10 req/min sensitive ops |
| Errors | Return stack trace ở prod | Generic error, detailed logs internally |
| HTTPS | HTTP allowed | HTTPS only + HSTS |
| Dependencies | 2+ năm không update | Monthly security audit, patch trong 48h |
| Versions | 5 versions running, no sunset | Explicit versioning, deprecation timeline |
| Logging | Log passwords | Log user_id, timestamp, action (no secrets) |
Kết Luận & CTA
API là trái tim của hầu hết ứng dụng hiện đại. Nhưng theo báo cáo 2026, 97% API vulnerabilities vẫn khá dễ khai thác. Điều đó có nghĩa: basic security hygiene tiết kiệm hàng tỷ.
Team chúng tôi recommend:
- Ngay hôm nay: Chạy
dotnet list package --vulnerabletrên codebase. Fix ngay. - Tuần này: Audit 3-5 critical endpoints bằng checklist trên.
- Tháng này: Implement rate limiting + security header middleware.
- Q2 2026: SCA tool (Snyk hoặc Dependabot) vào CI/CD.
Chúng tôi không nói bảo mật API khó. Nó chỉ cần sự kiên trì. Một endpoint quên [Authorize] có thể cost hàng tỷ. Checklist này giúp team chúng tôi, và giờ giúp bạn.
Bạn đang làm gì với API security ngay bây giờ? Feedback ở dưới comment. Chúng tôi luôn lắng nghe.
Tham Khảo
- OWASP API Security Project
- 42Crunch State of API Security 2026 Report
- State of API Security 2026: Common Misconfigurations
- OWASP Top 10 API Vulnerabilities 2023
- ASP.NET Core Security Features - Microsoft Learn
- .NET Security Cheat Sheet - OWASP
BKGlobal Tech Team
Cập nhật: 2026-03-19