All articles

C# Interview Guide: OWASP Top 10 for .NET APIs — Mitigations and Code

3 min read
#0
0 reads
0 reading now
0 shares

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 audit in 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.

ShareLinkedInX

Comments

Share your thoughts without signing in.