OWASP Top 10 in ASP.NET Core
Key Points
- OWASP Top 10 (2021): A01 Broken Access Control · A02 Cryptographic Failures · A03 Injection · A04 Insecure Design · A05 Security Misconfiguration · A06 Vulnerable Components · A07 Auth Failures · A08 Software/Data Integrity · A09 Logging/Monitoring · A10 SSRF.
- ASP.NET Core covers many by default (anti-forgery, parameterized queries via EF, DPAPI, HTTPS redirection) — but defaults aren't enough; senior reviews each.
- Top hit list for .NET: missing authorization on endpoints, JWT misconfig, EF queries built with string concat, secrets in config files, outdated NuGet packages, no security headers, no SSRF defense in
HttpClientcalls. - Tools: GitHub Dependabot, NuGet vulnerability alerts (
dotnet list package --vulnerable), Snyk, OWASP ZAP, security-code-scan analyzer.
Concepts (deep dive)
A01: Broken Access Control
The #1 web vulnerability. Examples: - IDOR (Insecure Direct Object Reference): /orders/123 returns my order regardless of ownership. - Missing [Authorize] on a sensitive endpoint. - Privilege escalation: client passes role=admin and server trusts it.
// ❌ IDOR
app.MapGet("/orders/{id}", (int id, AppDb db) => db.Orders.Find(id));
// ✅ Filter by current user
app.MapGet("/orders/{id}", async (int id, AppDb db, ClaimsPrincipal u) =>
{
var userId = u.FindFirstValue("sub");
var order = await db.Orders.FirstOrDefaultAsync(o => o.Id == id && o.UserId == userId);
return order is null ? Results.NotFound() : Results.Ok(order);
}).RequireAuthorization();
Senior practice: FallbackPolicy = RequireAuthenticatedUser; resource-based authorization for entity ownership.
A02: Cryptographic Failures
- Weak hashes (MD5, SHA1) for passwords.
- Hard-coded keys.
- TLS 1.0/1.1 enabled.
- Sensitive data in URLs (query strings logged everywhere).
// ❌ MD5 password
var hash = MD5.HashData(Encoding.UTF8.GetBytes(pwd));
// ✅ ASP.NET Identity (PBKDF2/Argon2id)
var hash = passwordHasher.HashPassword(user, pwd);
Senior practice: PBKDF2 minimum, Argon2id preferred. TLS 1.2+; HSTS; secure cookies.
A03: Injection
- SQL injection: string-concat queries.
- Command injection:
Process.Start(userInput). - LDAP, XPath, NoSQL injection.
// ❌ EF raw SQL with concat
db.Database.ExecuteSqlRaw($"SELECT * FROM Users WHERE Email='{email}'");
// ✅ Parameterized
db.Database.ExecuteSqlInterpolated($"SELECT * FROM Users WHERE Email={email}");
// or
db.Users.Where(u => u.Email == email);
EF Core's LINQ queries are parameterized by default. FromSqlInterpolated and ExecuteSqlInterpolated parameterize the interpolated values.
A04: Insecure Design
Architectural flaws — e.g., a password reset that emails the password (it's stored hashed; can't be recovered → must be redesigned).
Senior practice: threat-model new features; design for least privilege.
A05: Security Misconfiguration
- Default credentials.
- Verbose error pages in production (stack traces leaked).
- Unnecessary services enabled (e.g., default web server pages).
- Missing security headers.
// ❌ Verbose errors in prod
if (app.Environment.IsDevelopment()) app.UseDeveloperExceptionPage();
// ✅ Generic page in prod
else app.UseExceptionHandler("/error");
Security headers:
app.Use(async (ctx, next) =>
{
ctx.Response.Headers["X-Content-Type-Options"] = "nosniff";
ctx.Response.Headers["X-Frame-Options"] = "DENY";
ctx.Response.Headers["Referrer-Policy"] = "strict-origin-when-cross-origin";
ctx.Response.Headers["Permissions-Policy"] = "geolocation=(), camera=()";
ctx.Response.Headers["Content-Security-Policy"] = "default-src 'self'";
await next();
});
app.UseHsts();
app.UseHttpsRedirection();
Or use NWebsec library.
A06: Vulnerable & Outdated Components
Run in CI. Block builds on critical CVEs. Use Dependabot / Renovate for automated PRs. NuGet has built-in vulnerability metadata since 2022.
A07: Identification & Authentication Failures
- Brute-forceable login (no lockout).
- Weak passwords accepted.
- Predictable session IDs (DPAPI handles this in .NET).
- No MFA.
// Identity has lockout built in:
o.Lockout.MaxFailedAccessAttempts = 5;
o.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
o.Password.RequiredLength = 12;
o.Password.RequireDigit = true;
A08: Software & Data Integrity Failures
- Insecure deserialization (
BinaryFormatter— banned in .NET 9+). - Untrusted CI/CD pipelines.
- Auto-update mechanisms without signature verification.
// ❌ banned
var formatter = new BinaryFormatter();
formatter.Deserialize(stream);
// ✅ JSON / MessagePack with strict typing
JsonSerializer.Deserialize<Order>(stream);
A09: Logging & Monitoring Failures
- No security event logging.
- Logs written but not monitored.
- No alerts on auth failures, anomalies.
Pipe security events to SIEM (Sentinel, Splunk). Alert on auth failure spikes, privileged operations, failed authorization.
A10: Server-Side Request Forgery (SSRF)
// ❌ User-controlled URL
public async Task<IActionResult> Fetch(string url)
{
var data = await _http.GetStringAsync(url); // attacker → http://169.254.169.254/ (AWS metadata!)
return Ok(data);
}
Defense: - Validate URLs against an allowlist (not denylist). - Block private IPs (RFC1918, link-local 169.254/16, IPv6 ULA). - Use a proxy / network policy that blocks egress to internal addresses. - Prefer ID-based references over URLs (let server resolve to allowed origin).
public async Task<IActionResult> Fetch(string id)
{
if (!_allowed.TryGetValue(id, out var url)) return BadRequest();
var data = await _http.GetStringAsync(url);
return Ok(data);
}
Cross-cutting defenses
| Defense | Where |
|---|---|
[Authorize] everywhere | endpoints |
| Parameterized queries | data layer |
| Anti-forgery | server-rendered forms |
| HTTPS + HSTS | hosting |
| Security headers | middleware |
| Secrets in Key Vault | config |
| Audit logging | cross-cutting |
| Dependency scanning | CI/CD |
| Threat modeling | design |
Anti-forgery (CSRF)
Built into ASP.NET. Each form gets a token; server validates on submit.
// Razor automatically:
<form method="post">
<!-- @Html.AntiForgeryToken() injected -->
</form>
[ValidateAntiForgeryToken]
public IActionResult Post() { /* ... */ }
// Minimal API + JS:
app.UseAntiforgery(); // .NET 8+
app.MapPost("/api", () => { }).RequireAntiforgery();
For SPAs using cookie auth, send the token in a header.
CORS
builder.Services.AddCors(o => o.AddPolicy("api", p =>
p.WithOrigins("https://app.contoso.com")
.AllowAnyMethod()
.AllowAnyHeader()
.AllowCredentials()));
app.UseCors("api");
❌ WithOrigins("*").AllowCredentials() is invalid — browsers reject it.
Secrets management
- Local dev: user secrets (
dotnet user-secrets). - Cloud: Azure Key Vault with managed identity. Never put secrets in
appsettings.jsoncommitted to git.
Sanitization for HTML output
Razor auto-encodes by default; @Html.Raw(...) is the escape hatch and must be guarded. For user-generated HTML, use HtmlSanitizer library.
Code: correct vs wrong
❌ Wrong: SQL string concat
✅ Correct: parameterized
❌ Wrong: trusting client role
✅ Correct: server-authoritative
❌ Wrong: open redirect
✅ Correct: validate
Design patterns for this topic
Pattern 1 — "Threat-model every new feature"
- Intent: STRIDE — Spoofing, Tampering, Repudiation, Info Disclosure, DoS, Elevation.
Pattern 2 — "Defense in depth"
- Intent: every control assumes another fails.
Pattern 3 — "Allowlist over denylist"
- Intent: for SSRF, redirects, file types — explicit allow.
Pattern 4 — "Secure-by-default middleware stack"
- Intent: HTTPS redirection + HSTS + security headers + auth fallback policy + anti-forgery.
Pattern 5 — "Automated dependency scanning"
- Intent: Dependabot/Renovate +
dotnet list package --vulnerablein CI.
Pros & cons / trade-offs
| Defense | Pros | Cons |
|---|---|---|
FallbackPolicy | Secure-by-default | Risk of breaking existing public endpoints |
| Strict CSP | XSS containment | App breakage; tuning needed |
| Anti-forgery | CSRF defense | SPA token plumbing |
| Dependency scanning | Catches CVEs | False positives |
| HSTS | Forces HTTPS | Hard to roll back if misconfigured |
When to use / when to avoid
- Always: HTTPS, HSTS, security headers, anti-forgery for cookies.
- Always: parameterized queries; never raw concat.
- Avoid:
BinaryFormatter; user-controlled URLs inHttpClient.
Interview Q&A
Q1. Top OWASP risk in 2021? Broken Access Control (A01).
Q2. How does EF Core prevent SQL injection? LINQ queries are parameterized. FromSqlInterpolated parameterizes interpolated values. FromSqlRaw with concat is the trapdoor.
Q3. Why is BinaryFormatter removed? Insecure deserialization — attacker-controlled blob can run arbitrary types. Replaced by JSON/MessagePack with strict typing.
Q4. CSRF defense in ASP.NET Core? Anti-forgery tokens automatically embedded in Razor forms; validated on POST. SPAs send the token in a header.
Q5. SSRF defense? Allowlist of permitted destinations; block private IP ranges; ID-based references. Network egress policies as defense in depth.
Q6. Why HSTS? Forces browsers to use HTTPS even on the first visit (after first valid response). Mitigates downgrade attacks.
Q7. Why use Key Vault? Centralized secret store; access controlled by managed identity; audit log; rotation. No secrets in source.
Q8. What's CSP? Content-Security-Policy header. Restricts resource origins (scripts, styles, frames). Single biggest XSS containment.
Q9. How detect outdated NuGet packages? dotnet list package --vulnerable --include-transitive. NuGet has built-in CVE metadata. Dependabot/Renovate automate.
Q10. Open redirect — what and how prevent? Attacker-controlled returnUrl redirects user to phishing site. Prevent with Url.IsLocalUrl(...).
Gotchas / common mistakes
- ⚠️ Missing
[Authorize]on a sensitive endpoint. - ⚠️ Trusting
X-Forwarded-ForwithoutUseForwardedHeadersconfig. - ⚠️ Verbose errors in production.
- ⚠️ Default CORS too permissive (
AllowAnyOrigin). - ⚠️ Logging PII.
- ⚠️ Open redirects in login/return-URL flows.