dotnet-security-owasp
OWASP Top 10 (2021) security guidance for .NET applications. Each category includes the vulnerability description,
.NET-specific risk, mitigation code examples, and common pitfalls. This skill is the canonical owner of deprecated
security pattern warnings (CAS, APTCA, .NET Remoting, DCOM, BinaryFormatter).
Scope
- OWASP Top 10 (2021) vulnerability categories with .NET-specific mitigations
- Injection, broken access control, XSS, SSRF prevention patterns
- Deprecated security API warnings (CAS, APTCA, BinaryFormatter, .NET Remoting)
- Security header configuration and CORS hardening
- Rate limiting and anti-forgery middleware patterns
- NuGet package audit and dependency vulnerability scanning
Out of scope
- Authentication/authorization implementation -- see [skill:dotnet-api-security]
- Blazor auth UI -- see [skill:dotnet-blazor-auth]
- Cryptographic algorithm selection -- see [skill:dotnet-cryptography]
- Configuration binding and Options pattern -- see [skill:dotnet-csharp-configuration]
- Secrets storage and management -- see [skill:dotnet-secrets-management]
Cross-references: [skill:dotnet-secrets-management] for secrets handling, [skill:dotnet-cryptography] for cryptographic
best practices, [skill:dotnet-csharp-coding-standards] for secure coding conventions.
A01: Broken Access Control
Vulnerability: Users act outside their intended permissions -- accessing other users' data, elevating privileges, or
bypassing access checks.
Risk in .NET: Missing [Authorize] attributes on controllers/endpoints, insecure direct object references (IDOR)
where user IDs are taken from route parameters without ownership validation, and CORS misconfiguration allowing
unintended origins.
Mitigation
// 1. Apply authorization globally, then opt out explicitly
builder.Services.AddAuthorizationBuilder()
.SetFallbackPolicy(new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build());
var app = builder.Build();
app.MapControllers(); // All endpoints require auth by default
// 2. Resource-based authorization to prevent IDOR
public sealed class DocumentAuthorizationHandler
: AuthorizationHandler<EditRequirement, Document>
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
EditRequirement requirement,
Document resource)
{
if (resource.OwnerId == context.User.FindFirstValue(ClaimTypes.NameIdentifier))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
// In the endpoint:
app.MapPut("/documents/{id}", async (
int id,
DocumentDto dto,
IAuthorizationService authService,
ClaimsPrincipal user,
AppDbContext db) =>
{
var document = await db.Documents.FindAsync(id);
if (document is null) return Results.NotFound();
var authResult = await authService.AuthorizeAsync(user, document, "Edit");
if (!authResult.Succeeded) return Results.Forbid();
document.Title = dto.Title;
await db.SaveChangesAsync();
return Results.NoContent();
});
```text
```csharp
// 3. Restrict CORS to known origins
builder.Services.AddCors(options =>
{
options.AddPolicy("Strict", policy =>
{
policy.WithOrigins("https://app.example.com")
.WithMethods("GET", "POST")
.WithHeaders("Content-Type", "Authorization");
});
});
```text
**Gotcha:** `AllowAnyOrigin()` combined with `AllowCredentials()` is rejected at runtime by ASP.NET Core, but `SetIsOriginAllowed(_ => true)` with `AllowCredentials()` silently allows all origins -- never use this pattern.
---
## A02: Cryptographic Failures
**Vulnerability:** Sensitive data exposed due to weak or missing encryption -- plaintext storage, deprecated algorithms, or improper key management.
**Risk in .NET:** Using MD5/SHA1 for hashing passwords, storing connection strings with plaintext passwords in `appsettings.json`, transmitting sensitive data over HTTP, or using `DES`/`RC2` for encryption.
### Mitigation
```csharp
// Enforce HTTPS and HSTS
builder.Services.AddHttpsRedirection(options =>
{
options.HttpsPort = 443;
});
var app = builder.Build();
app.UseHsts(); // Strict-Transport-Security header
app.UseHttpsRedirection();
// Never store secrets in appsettings.json -- use user secrets or env vars
// See [skill:dotnet-secrets-management] for proper secrets handling
```json
```csharp
// Use Data Protection API for symmetric encryption of application data
public sealed class TokenProtector(IDataProtectionProvider provider)
{
private readonly IDataProtector _protector =
provider.CreateProtector("Tokens.V1");
public string Protect(string plaintext) => _protector.Protect(plaintext);
public string Unprotect(string ciphertext) => _protector.Unprotect(ciphertext);
}
```text
See [skill:dotnet-cryptography] for algorithm selection (AES-GCM, RSA, ECDSA) and key derivation.
---
## A03: Injection
**Vulnerability:** Untrusted data sent to an interpreter as part of a command or query -- SQL injection, command injection, LDAP injection, and cross-site scripting (XSS).
**Risk in .NET:** String concatenation in SQL queries, `Process.Start` with unsanitized input, rendering user input as raw HTML in Razor pages.
### Mitigation
```csharp
// SQL injection prevention: always use parameterized queries
// EF Core is parameterized by default via LINQ
var orders = await db.Orders
.Where(o => o.CustomerId == customerId)
.ToListAsync();
// When raw SQL is needed, use parameterized interpolation
var results = await db.Orders
.FromSqlInterpolated($"SELECT * FROM Orders WHERE Status = {status}")
.ToListAsync();
// NEVER concatenate user input into SQL:
// var bad = db.Orders.FromSqlRaw("SELECT * FROM Orders WHERE Status = '" + status + "'");
```text
```csharp
// XSS prevention: Razor encodes output by default.
// Use @Html.Raw() ONLY for trusted, pre-sanitized HTML.
// In Minimal APIs, return typed results -- not raw strings:
app.MapGet("/greeting", (string name) =>
Results.Content($"<p>Hello, {HtmlEncoder.Default.Encode(name)}</p>",
"text/html"));
// Command injection prevention: avoid Process.Start with user input.
// If unavoidable, validate against an allowlist:
public static bool IsAllowedTool(string toolName) =>
toolName is "dotnet" or "git" or "nuget";
```bash
**Gotcha:** `FromSqlRaw` with string concatenation bypasses parameterization. Always use `FromSqlInterpolated` or pass `SqlParameter` objects to `FromSqlRaw`.
---
## A04: Insecure Design
**Vulnerability:** Flaws in design patterns that cannot be fixed by implementation alone -- missing rate limiting, lack of defense in depth, unrestricted resource consumption.
**Risk in .NET:** APIs without rate limiting, unbounded file uploads, missing anti-forgery tokens on state-changing operations.
### Mitigation
```csharp
// Rate limiting with built-in middleware (.NET 7+)
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("api", limiterOptions =>
{
limiterOptions.PermitLimit = 100;
limiterOptions.Window = TimeSpan.FromMinutes(1);
limiterOptions.QueueLimit = 0;
});
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
});
var app = builder.Build();
app.UseRateLimiter();
app.MapGet("/api/data", () => Results.Ok("data"))
.RequireRateLimiting("api");
```text
```csharp
// Anti-forgery for Minimal APIs (.NET 8+)
builder.Services.AddAntiforgery();
var app = builder.Build();
app.UseAntiforgery();
// Form-bound endpoint: antiforgery validated automatically
app.MapPost("/orders", async ([FromForm] string productId, AppDbContext db) =>
{
var order = new Order { ProductId = productId };
db.Orders.Add(order);
await db.SaveChangesAsync();
return Results.Created($"/orders/{order.Id}", order);
});
// JSON endpoint: opt in explicitly with RequireAntiforgery()
app.MapPost("/api/orders", async (CreateOrderDto dto, AppDbContext db) =>
{
var order = new Order { ProductId = dto.ProductId };
db.Orders.Add(order);
await db.SaveChangesAsync();
return Results.Created($"/api/orders/{order.Id}", order);
}).RequireAntiforgery();
```text
**Gotcha:** `UseRateLimiter()` must be called after `UseRouting()` and before `MapControllers()`/`MapGet()` to apply correctly.
---
## A05: Security Misconfiguration
**Vulnerability:** Insecure default configurations, incomplete configurations, open cloud storage, unnecessary features enabled, verbose error messages.
**Risk in .NET:** Detailed exception pages in production (`UseDeveloperExceptionPage`), default Kestrel settings exposing server headers, debug endpoints left enabled, or missing security headers.
### Mitigation
```csharp
// Remove server identity headers (configure BEFORE Build)
builder.WebHost.ConfigureKestrel(options =>
{
options.AddServerHeader = false;
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// Generic error handler in production -- no stack traces
app.UseExceptionHandler("/error");
app.UseHsts();
}
// Add security headers via middleware
app.Use(async (context, next) =>
{
context.Response.Headers.Append("X-Content-Type-Options", "nosniff");
context.Response.Headers.Append("X-Frame-Options", "DENY");
context.Response.Headers.Append("Referrer-Policy", "strict-origin-when-cross-origin");
context.Response.Headers.Append(
"Content-Security-Policy",
"default-src 'self'; script-src 'self'; style-src 'self'");
await next();
});
```text
```csharp
// Constrain request body size to prevent resource exhaustion (configure BEFORE Build)
builder.WebHost.ConfigureKestrel(options =>
{
options.Limits.MaxRequestBodySize = 10 * 1024 * 1024; // 10 MB
options.Limits.MaxRequestHeadersTotalSize = 32 * 1024; // 32 KB
options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30);
});
// Then: var app = builder.Build();
```text
**Gotcha:** `UseDeveloperExceptionPage()` leaks source code paths and stack traces. Ensure it is gated behind `IsDevelopment()` and never enabled in production or staging.
---
## A06: Vulnerable and Outdated Components
**Vulnerability:** Using components with known vulnerabilities, unsupported frameworks, or unpatched dependencies.
**Risk in .NET:** Running on out-of-support .NET versions, NuGet packages with known CVEs, transitive dependency vulnerabilities not audited.
### Mitigation
```xml
<!-- Enable NuGet audit in Directory.Build.props or csproj -->
<PropertyGroup>
<NuGetAudit>true</NuGetAudit>
<NuGetAuditLevel>low</NuGetAuditLevel>
<NuGetAuditMode>all</NuGetAuditMode> <!-- Audit direct + transitive -->
</PropertyGroup>
```text
```bash
# Audit NuGet packages for known vulnerabilities
dotnet list package --vulnerable --include-transitive
# Keep packages up to date
dotnet outdated # requires dotnet-outdated-tool
# Check .NET SDK/runtime support status
dotnet --info
```text
**Gotcha:** `NuGetAuditMode` defaults to `direct` -- transitive vulnerabilities are hidden unless you set `all`. Always use `all` in CI to catch deep dependency issues.
---
## A07: Identification and Authentication Failures
**Vulnerability:** Weak authentication mechanisms, credential stuffing, session fixation, missing multi-factor authentication.
**Risk in .NET:** Default Identity password policies that are too weak, session cookies without `Secure`/`SameSite` attributes, missing account lockout configuration.
### Mitigation
```csharp
// Configure strong Identity password and lockout policies
## Detailed Examples
See [references/detailed-examples.md](references/detailed-examples.md) for complete code samples and advanced patterns.
1---2name: dotnet-security-owasp3description: Hardens .NET apps per OWASP Top 10 -- injection, auth, XSS, deprecated security APIs.4license: MIT5targets: ['*']6category: security7subcategory: owasp8tags:9 - security10 - dotnet11 - skill12 - owasp13 - xss14 - injection15version: '1.0.0'16author: 'dotnet-agent-harness'17invocable: true18related_skills:19 - dotnet-api-security20 - dotnet-cryptography21 - dotnet-secrets-management22 - dotnet-input-validation23claudecode:24 allowed-tools: ['Read', 'Grep', 'Glob', 'Bash', 'Write', 'Edit']25codexcli:26 short-description: '.NET skill guidance for security tasks'27opencode:28 allowed-tools: ['Read', 'Grep', 'Glob', 'Bash', 'Write', 'Edit']29copilot: {}30geminicli: {}31antigravity: {}32---3334# dotnet-security-owasp3536OWASP Top 10 (2021) security guidance for .NET applications. Each category includes the vulnerability description,37.NET-specific risk, mitigation code examples, and common pitfalls. This skill is the canonical owner of deprecated38security pattern warnings (CAS, APTCA, .NET Remoting, DCOM, BinaryFormatter).3940## Scope4142- OWASP Top 10 (2021) vulnerability categories with .NET-specific mitigations43- Injection, broken access control, XSS, SSRF prevention patterns44- Deprecated security API warnings (CAS, APTCA, BinaryFormatter, .NET Remoting)45- Security header configuration and CORS hardening46- Rate limiting and anti-forgery middleware patterns47- NuGet package audit and dependency vulnerability scanning4849## Out of scope5051- Authentication/authorization implementation -- see [skill:dotnet-api-security]52- Blazor auth UI -- see [skill:dotnet-blazor-auth]53- Cryptographic algorithm selection -- see [skill:dotnet-cryptography]54- Configuration binding and Options pattern -- see [skill:dotnet-csharp-configuration]55- Secrets storage and management -- see [skill:dotnet-secrets-management]5657Cross-references: [skill:dotnet-secrets-management] for secrets handling, [skill:dotnet-cryptography] for cryptographic58best practices, [skill:dotnet-csharp-coding-standards] for secure coding conventions.5960---6162## A01: Broken Access Control6364**Vulnerability:** Users act outside their intended permissions -- accessing other users' data, elevating privileges, or65bypassing access checks.6667**Risk in .NET:** Missing `[Authorize]` attributes on controllers/endpoints, insecure direct object references (IDOR)68where user IDs are taken from route parameters without ownership validation, and CORS misconfiguration allowing69unintended origins.7071### Mitigation7273````csharp7475// 1. Apply authorization globally, then opt out explicitly76builder.Services.AddAuthorizationBuilder()77 .SetFallbackPolicy(new AuthorizationPolicyBuilder()78 .RequireAuthenticatedUser()79 .Build());8081var app = builder.Build();82app.MapControllers(); // All endpoints require auth by default8384// 2. Resource-based authorization to prevent IDOR85public sealed class DocumentAuthorizationHandler86 : AuthorizationHandler<EditRequirement, Document>87{88 protected override Task HandleRequirementAsync(89 AuthorizationHandlerContext context,90 EditRequirement requirement,91 Document resource)92 {93 if (resource.OwnerId == context.User.FindFirstValue(ClaimTypes.NameIdentifier))94 {95 context.Succeed(requirement);96 }97 return Task.CompletedTask;98 }99}100101// In the endpoint:102app.MapPut("/documents/{id}", async (103 int id,104 DocumentDto dto,105 IAuthorizationService authService,106 ClaimsPrincipal user,107 AppDbContext db) =>108{109 var document = await db.Documents.FindAsync(id);110 if (document is null) return Results.NotFound();111112 var authResult = await authService.AuthorizeAsync(user, document, "Edit");113 if (!authResult.Succeeded) return Results.Forbid();114115 document.Title = dto.Title;116 await db.SaveChangesAsync();117 return Results.NoContent();118});119120```text121122```csharp123124// 3. Restrict CORS to known origins125builder.Services.AddCors(options =>126{127 options.AddPolicy("Strict", policy =>128 {129 policy.WithOrigins("https://app.example.com")130 .WithMethods("GET", "POST")131 .WithHeaders("Content-Type", "Authorization");132 });133});134135```text136137**Gotcha:** `AllowAnyOrigin()` combined with `AllowCredentials()` is rejected at runtime by ASP.NET Core, but `SetIsOriginAllowed(_ => true)` with `AllowCredentials()` silently allows all origins -- never use this pattern.138139---140141## A02: Cryptographic Failures142143**Vulnerability:** Sensitive data exposed due to weak or missing encryption -- plaintext storage, deprecated algorithms, or improper key management.144145**Risk in .NET:** Using MD5/SHA1 for hashing passwords, storing connection strings with plaintext passwords in `appsettings.json`, transmitting sensitive data over HTTP, or using `DES`/`RC2` for encryption.146147### Mitigation148149```csharp150151// Enforce HTTPS and HSTS152builder.Services.AddHttpsRedirection(options =>153{154 options.HttpsPort = 443;155});156157var app = builder.Build();158app.UseHsts(); // Strict-Transport-Security header159app.UseHttpsRedirection();160161// Never store secrets in appsettings.json -- use user secrets or env vars162// See [skill:dotnet-secrets-management] for proper secrets handling163164```json165166```csharp167168// Use Data Protection API for symmetric encryption of application data169public sealed class TokenProtector(IDataProtectionProvider provider)170{171 private readonly IDataProtector _protector =172 provider.CreateProtector("Tokens.V1");173174 public string Protect(string plaintext) => _protector.Protect(plaintext);175176 public string Unprotect(string ciphertext) => _protector.Unprotect(ciphertext);177}178179```text180181See [skill:dotnet-cryptography] for algorithm selection (AES-GCM, RSA, ECDSA) and key derivation.182183---184185## A03: Injection186187**Vulnerability:** Untrusted data sent to an interpreter as part of a command or query -- SQL injection, command injection, LDAP injection, and cross-site scripting (XSS).188189**Risk in .NET:** String concatenation in SQL queries, `Process.Start` with unsanitized input, rendering user input as raw HTML in Razor pages.190191### Mitigation192193```csharp194195// SQL injection prevention: always use parameterized queries196// EF Core is parameterized by default via LINQ197var orders = await db.Orders198 .Where(o => o.CustomerId == customerId)199 .ToListAsync();200201// When raw SQL is needed, use parameterized interpolation202var results = await db.Orders203 .FromSqlInterpolated($"SELECT * FROM Orders WHERE Status = {status}")204 .ToListAsync();205206// NEVER concatenate user input into SQL:207// var bad = db.Orders.FromSqlRaw("SELECT * FROM Orders WHERE Status = '" + status + "'");208209```text210211```csharp212213// XSS prevention: Razor encodes output by default.214// Use @Html.Raw() ONLY for trusted, pre-sanitized HTML.215// In Minimal APIs, return typed results -- not raw strings:216app.MapGet("/greeting", (string name) =>217 Results.Content($"<p>Hello, {HtmlEncoder.Default.Encode(name)}</p>",218 "text/html"));219220// Command injection prevention: avoid Process.Start with user input.221// If unavoidable, validate against an allowlist:222public static bool IsAllowedTool(string toolName) =>223 toolName is "dotnet" or "git" or "nuget";224225```bash226227**Gotcha:** `FromSqlRaw` with string concatenation bypasses parameterization. Always use `FromSqlInterpolated` or pass `SqlParameter` objects to `FromSqlRaw`.228229---230231## A04: Insecure Design232233**Vulnerability:** Flaws in design patterns that cannot be fixed by implementation alone -- missing rate limiting, lack of defense in depth, unrestricted resource consumption.234235**Risk in .NET:** APIs without rate limiting, unbounded file uploads, missing anti-forgery tokens on state-changing operations.236237### Mitigation238239```csharp240241// Rate limiting with built-in middleware (.NET 7+)242builder.Services.AddRateLimiter(options =>243{244 options.AddFixedWindowLimiter("api", limiterOptions =>245 {246 limiterOptions.PermitLimit = 100;247 limiterOptions.Window = TimeSpan.FromMinutes(1);248 limiterOptions.QueueLimit = 0;249 });250 options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;251});252253var app = builder.Build();254app.UseRateLimiter();255256app.MapGet("/api/data", () => Results.Ok("data"))257 .RequireRateLimiting("api");258259```text260261```csharp262263// Anti-forgery for Minimal APIs (.NET 8+)264builder.Services.AddAntiforgery();265266var app = builder.Build();267app.UseAntiforgery();268269// Form-bound endpoint: antiforgery validated automatically270app.MapPost("/orders", async ([FromForm] string productId, AppDbContext db) =>271{272 var order = new Order { ProductId = productId };273 db.Orders.Add(order);274 await db.SaveChangesAsync();275 return Results.Created($"/orders/{order.Id}", order);276});277278// JSON endpoint: opt in explicitly with RequireAntiforgery()279app.MapPost("/api/orders", async (CreateOrderDto dto, AppDbContext db) =>280{281 var order = new Order { ProductId = dto.ProductId };282 db.Orders.Add(order);283 await db.SaveChangesAsync();284 return Results.Created($"/api/orders/{order.Id}", order);285}).RequireAntiforgery();286287```text288289**Gotcha:** `UseRateLimiter()` must be called after `UseRouting()` and before `MapControllers()`/`MapGet()` to apply correctly.290291---292293## A05: Security Misconfiguration294295**Vulnerability:** Insecure default configurations, incomplete configurations, open cloud storage, unnecessary features enabled, verbose error messages.296297**Risk in .NET:** Detailed exception pages in production (`UseDeveloperExceptionPage`), default Kestrel settings exposing server headers, debug endpoints left enabled, or missing security headers.298299### Mitigation300301```csharp302303// Remove server identity headers (configure BEFORE Build)304builder.WebHost.ConfigureKestrel(options =>305{306 options.AddServerHeader = false;307});308309var app = builder.Build();310311if (app.Environment.IsDevelopment())312{313 app.UseDeveloperExceptionPage();314}315else316{317 // Generic error handler in production -- no stack traces318 app.UseExceptionHandler("/error");319 app.UseHsts();320}321322// Add security headers via middleware323app.Use(async (context, next) =>324{325 context.Response.Headers.Append("X-Content-Type-Options", "nosniff");326 context.Response.Headers.Append("X-Frame-Options", "DENY");327 context.Response.Headers.Append("Referrer-Policy", "strict-origin-when-cross-origin");328 context.Response.Headers.Append(329 "Content-Security-Policy",330 "default-src 'self'; script-src 'self'; style-src 'self'");331 await next();332});333334```text335336```csharp337338// Constrain request body size to prevent resource exhaustion (configure BEFORE Build)339builder.WebHost.ConfigureKestrel(options =>340{341 options.Limits.MaxRequestBodySize = 10 * 1024 * 1024; // 10 MB342 options.Limits.MaxRequestHeadersTotalSize = 32 * 1024; // 32 KB343 options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30);344});345346// Then: var app = builder.Build();347348```text349350**Gotcha:** `UseDeveloperExceptionPage()` leaks source code paths and stack traces. Ensure it is gated behind `IsDevelopment()` and never enabled in production or staging.351352---353354## A06: Vulnerable and Outdated Components355356**Vulnerability:** Using components with known vulnerabilities, unsupported frameworks, or unpatched dependencies.357358**Risk in .NET:** Running on out-of-support .NET versions, NuGet packages with known CVEs, transitive dependency vulnerabilities not audited.359360### Mitigation361362```xml363364<!-- Enable NuGet audit in Directory.Build.props or csproj -->365<PropertyGroup>366 <NuGetAudit>true</NuGetAudit>367 <NuGetAuditLevel>low</NuGetAuditLevel>368 <NuGetAuditMode>all</NuGetAuditMode> <!-- Audit direct + transitive -->369</PropertyGroup>370371```text372373```bash374375# Audit NuGet packages for known vulnerabilities376dotnet list package --vulnerable --include-transitive377378# Keep packages up to date379dotnet outdated # requires dotnet-outdated-tool380381# Check .NET SDK/runtime support status382dotnet --info383384```text385386**Gotcha:** `NuGetAuditMode` defaults to `direct` -- transitive vulnerabilities are hidden unless you set `all`. Always use `all` in CI to catch deep dependency issues.387388---389390## A07: Identification and Authentication Failures391392**Vulnerability:** Weak authentication mechanisms, credential stuffing, session fixation, missing multi-factor authentication.393394**Risk in .NET:** Default Identity password policies that are too weak, session cookies without `Secure`/`SameSite` attributes, missing account lockout configuration.395396### Mitigation397398```csharp399400// Configure strong Identity password and lockout policies401402## Detailed Examples403404See [references/detailed-examples.md](references/detailed-examples.md) for complete code samples and advanced patterns.