Q22 — OWASP Top 10 — How Each Applies to .NET APIs with Mitigations
Level: Senior | Topic: Security
The Most Critical Risks
- A01 — Broken Access Control: Never trust client-sent IDs. Always load the resource then verify ownership with resource-based authorization (
IAuthorizationService). Return 403 (not 404) to avoid leaking resource existence to attackers. - A02 — Cryptographic Failures: Use HTTPS everywhere. Hash passwords with BCrypt/Argon2 (never MD5/SHA1). Encrypt PII at rest with AES-256. Store keys in Azure Key Vault, never in config files.
- A03 — Injection: Use EF Core LINQ (always parameterized) or
FromSqlRaw()with parameters. Never concatenate user input into SQL strings. - A05 — Security Misconfiguration: Disable Swagger in production. Harden CORS to explicit origins. Never return detailed error messages to clients.
- A06 — Vulnerable Components: Run
dotnet auditin CI. Use Dependabot or Renovate to auto-update packages. Pin package versions. - A09 — Logging Failures: Use structured logging (Serilog). Never log PII, passwords, or secrets. Include correlation IDs. Ship logs to a centralised SIEM.
Code Example
// A03 — SQL Injection Prevention
// ❌ VULNERABLE — string concatenation
// var sql = $"SELECT * FROM Users WHERE Name = '{name}'"; // INJECTABLE!
// ✅ SAFE — LINQ (always parameterized by EF Core)
public async Task<User?> FindUserSafeAsync(string name, CancellationToken ct)
=> await _db.Users.FirstOrDefaultAsync(u => u.Name == name, ct);
// ✅ SAFE — parameterized raw SQL
public async Task<List<User>> SearchUsersAsync(string term, CancellationToken ct)
=> await _db.Users
.FromSqlRaw("SELECT * FROM Users WHERE Name LIKE {0}", $"%{term}%")
.AsNoTracking().ToListAsync(ct);
// A01 — Resource-based Access Control
[HttpGet("{id}")]
public async Task<IActionResult> GetOrder(int id, CancellationToken ct)
{
var order = await _orders.GetByIdAsync(id, ct);
if (order is null) return NotFound();
// Check ownership AFTER loading — never trust the client-sent ID alone
var authResult = await _authz.AuthorizeAsync(User, order, "OrderOwnerPolicy");
if (!authResult.Succeeded)
return Forbid(); // 403 — don't return 404 (that leaks resource existence)
return Ok(order);
}
// A02 — Password hashing with BCrypt (work factor 12)
public static string HashPassword(string password)
=> BCrypt.Net.BCrypt.HashPassword(password, workFactor: 12);
public static bool VerifyPassword(string password, string hash)
=> BCrypt.Net.BCrypt.Verify(password, hash);
// A02 — AES-256 encryption at rest
public static (byte[] Ciphertext, byte[] IV) Encrypt(string plaintext, byte[] key)
{
using var aes = Aes.Create();
aes.Key = key; // 256-bit key from Key Vault
aes.GenerateIV();
using var encryptor = aes.CreateEncryptor();
var bytes = Encoding.UTF8.GetBytes(plaintext);
return (encryptor.TransformFinalBlock(bytes, 0, bytes.Length), aes.IV);
}
// A05 — Restrictive CORS (never wildcard in production)
builder.Services.AddCors(opts => opts.AddPolicy("production", p =>
p.WithOrigins("https://app.example.com") // explicit origin only
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders("Authorization", "Content-Type")
.AllowCredentials()));
Senior Insight
Never log PII or secrets — structured logging captures everything in scope automatically. Use [LogMasked] attributes or custom Serilog destructuring policies to scrub sensitive fields before they reach your log sink. Run dotnet audit in your CI pipeline and fail the build on high-severity CVEs. The OWASP Top 10 rarely changes year-over-year — broken access control and injection have been #1 and #3 for a decade.