| name | dotnet-security-owasp |
| description | Hardens .NET apps per OWASP Top 10 -- injection, auth, XSS, deprecated security APIs. |
| allowed-tools | ["Read","Grep","Glob","Bash","Write","Edit"] |
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
builder.Services.AddAuthorizationBuilder()
.SetFallbackPolicy(new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build());
var app = builder.Build();
app.MapControllers();
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;
}
}
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();
});
builder.Services.AddCors(options =>
{
options.AddPolicy("Strict", policy =>
{
policy.WithOrigins("https://app.example.com")
.WithMethods("GET", "POST")
.WithHeaders("Content-Type", "Authorization");
});
});
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
builder.Services.AddHttpsRedirection(options =>
{
options.HttpsPort = 443;
});
var app = builder.Build();
app.UseHsts();
app.UseHttpsRedirection();
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);
}
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
var orders = await db.Orders
.Where(o => o.CustomerId == customerId)
.ToListAsync();
var results = await db.Orders
.FromSqlInterpolated($"SELECT * FROM Orders WHERE Status = {status}")
.ToListAsync();
app.MapGet("/greeting", (string name) =>
Results.Content($"<p>Hello, {HtmlEncoder.Default.Encode(name)}</p>",
"text/html"));
public static bool IsAllowedTool(string toolName) =>
toolName is "dotnet" or "git" or "nuget";
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
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");
builder.Services.AddAntiforgery();
var app = builder.Build();
app.UseAntiforgery();
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);
});
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();
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
builder.WebHost.ConfigureKestrel(options =>
{
options.AddServerHeader = false;
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/error");
app.UseHsts();
}
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();
});
builder.WebHost.ConfigureKestrel(options =>
{
options.Limits.MaxRequestBodySize = 10 * 1024 * 1024;
options.Limits.MaxRequestHeadersTotalSize = 32 * 1024;
options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30);
});
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
<PropertyGroup>
<NuGetAudit>true</NuGetAudit>
<NuGetAuditLevel>low</NuGetAuditLevel>
<NuGetAuditMode>all</NuGetAuditMode>
</PropertyGroup>
dotnet list package --vulnerable --include-transitive
dotnet outdated
dotnet --info
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
builder.Services.AddIdentity<ApplicationUser, IdentityRole>(options =>
{
options.Password.RequireDigit = true;
options.Password.RequiredLength = 12;
options.Password.RequireNonAlphanumeric = true;
options.Password.RequireUppercase = true;
options.Password.RequireLowercase = true;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(15);
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.AllowedForNewUsers = true;
options.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<AppDbContext>()
.AddDefaultTokenProviders();
builder.Services.ConfigureApplicationCookie(options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Strict;
options.ExpireTimeSpan = TimeSpan.FromHours(2);
options.SlidingExpiration = true;
});
Gotcha: CookieSecurePolicy.SameAsRequest allows cookies over HTTP in development, which is fine. But in production behind a reverse proxy terminating TLS, the app sees HTTP -- so cookies are sent insecurely. Always use CookieSecurePolicy.Always in production and configure forwarded headers.
A08: Software and Data Integrity Failures
Vulnerability: Code and infrastructure that does not protect against integrity violations -- unsigned packages, insecure CI/CD pipelines, deserialization of untrusted data.
Risk in .NET: Using BinaryFormatter for deserialization (arbitrary code execution), accepting unsigned NuGet packages from untrusted feeds, missing package source mapping.
Mitigation
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="internal" value="https://pkgs.example.com/nuget/v3/index.json" />
</packageSources>
<packageSourceMapping>
<packageSource key="nuget.org">
<package pattern="*" />
</packageSource>
<packageSource key="internal">
<package pattern="MyCompany.*" />
</packageSource>
</packageSourceMapping>
</configuration>
var data = JsonSerializer.Deserialize<OrderDto>(jsonString);
var bytes = MessagePackSerializer.Serialize(order);
var restored = MessagePackSerializer.Deserialize<Order>(bytes);
Gotcha: Package source mapping uses most-specific-pattern-wins: MyCompany.* beats the * wildcard. Always define specific patterns for internal packages to prevent dependency confusion attacks.
A09: Security Logging and Monitoring Failures
Vulnerability: Insufficient logging of security-relevant events, lack of monitoring for breaches, inability to detect and respond to active attacks.
Risk in .NET: Not logging authentication failures, missing audit trails for sensitive operations, logging sensitive data (passwords, tokens) in plaintext.
Mitigation
public sealed class AuditMiddleware(RequestDelegate next, ILogger<AuditMiddleware> logger)
{
public async Task InvokeAsync(HttpContext context)
{
var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier) ?? "anonymous";
var path = context.Request.Path.Value;
using (logger.BeginScope(new Dictionary<string, object?>
{
["UserId"] = userId,
["RequestPath"] = path,
["RemoteIp"] = context.Connection.RemoteIpAddress?.ToString()
}))
{
await next(context);
if (context.Response.StatusCode == StatusCodes.Status401Unauthorized)
{
logger.LogWarning("Authentication failed for {Path}", path);
}
if (context.Response.StatusCode == StatusCodes.Status403Forbidden)
{
logger.LogWarning("Authorization denied for {Path}", path);
}
}
}
}
builder.Logging.AddFilter("Microsoft.AspNetCore.Authentication", LogLevel.Warning);
builder.Services.AddHttpLogging(options =>
{
options.LoggingFields = HttpLoggingFields.RequestPath
| HttpLoggingFields.RequestMethod
| HttpLoggingFields.ResponseStatusCode
| HttpLoggingFields.Duration;
});
Gotcha: Structured logging with {Placeholder} syntax is safe, but string interpolation ($"User {userId}") in log calls bypasses structured logging and may leak PII into log sinks that do not support redaction.
A10: Server-Side Request Forgery (SSRF)
Vulnerability: Application fetches a remote resource based on user-supplied URL without validation, allowing attackers to reach internal services or metadata endpoints.
Risk in .NET: HttpClient calls with user-provided URLs, URL redirect following to internal networks, accessing cloud metadata endpoints (169.254.169.254).
Mitigation
public static class UrlValidator
{
private static readonly HashSet<string> AllowedHosts = new(StringComparer.OrdinalIgnoreCase)
{
"api.example.com",
"cdn.example.com"
};
public static bool IsAllowed(string url)
{
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))
return false;
if (uri.Scheme != Uri.UriSchemeHttps)
return false;
if (IPAddress.TryParse(uri.Host, out var ip))
{
if (IsPrivateOrReserved(ip))
return false;
}
return AllowedHosts.Contains(uri.Host);
}
private static bool IsPrivateOrReserved(IPAddress ip)
{
byte[] bytes = ip.GetAddressBytes();
return bytes[0] switch
{
10 => ,
=> ,
bytes[] == => ,
bytes[] >= && bytes[] <= => ,
bytes[] == => ,
_ =>
};
}
}
app.MapPost(, (FetchRequest request, IHttpClientFactory factory) =>
{
(!UrlValidator.IsAllowed(request.Url))
Results.BadRequest();
client = factory.CreateClient();
response = client.GetStringAsync(request.Url);
Results.Ok(response);
});
builder.Services.AddHttpClient("external", client =>
{
client.BaseAddress = new Uri("https://api.example.com");
})
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
AllowAutoRedirect = false
});
Gotcha: DNS rebinding can bypass IP allowlists -- an attacker's domain resolves to a public IP during validation but to an internal IP during the actual request. Pin DNS resolution or re-validate after connection.
Deprecated Security Patterns
This skill is the canonical owner of deprecated security pattern warnings. Other skills should cross-reference here rather than duplicating these warnings.
Code Access Security (CAS)
CAS is not supported in .NET Core/.NET 5+. Code that references System.Security.Permissions, SecurityPermission, or [SecurityCritical]/[SecuritySafeCritical] attributes for CAS purposes must be removed or replaced with OS-level security boundaries (containers, process isolation).
AllowPartiallyTrustedCallers (APTCA)
The [AllowPartiallyTrustedCallers] attribute has no effect in .NET Core/.NET 5+. The partial-trust model is gone. Remove APTCA attributes during migration. Use standard authorization and input validation instead.
.NET Remoting
.NET Remoting is not available in .NET Core/.NET 5+. It was inherently insecure due to unrestricted deserialization of remote objects. Replace with:
- gRPC for cross-process/cross-machine RPC (see [skill:dotnet-cryptography] for transport security)
- Named pipes for same-machine IPC
- HTTP APIs for service-to-service communication
DCOM
Distributed COM (DCOM) is Windows-only and not supported in .NET Core/.NET 5+. Replace with gRPC, REST APIs, or message queues for distributed communication.
BinaryFormatter
BinaryFormatter is obsolete as error (SYSLIB0011) in .NET 8 and removed in .NET 9+. It enables arbitrary code execution through deserialization attacks. Replace with:
System.Text.Json for JSON serialization
- MessagePack or Protocol Buffers for binary formats
XmlSerializer with strict type allowlists for XML scenarios
Do not set System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization to true as a workaround.
Agent Gotchas
- Do not use
[AllowAnonymous] without explicit justification -- it overrides the global fallback policy. Mark each anonymous endpoint with a comment explaining why.
- Do not disable HTTPS redirection for convenience -- use
dotnet dev-certs https --trust for local development instead.
- Do not log raw request bodies -- they may contain credentials, tokens, or PII. Use
HttpLoggingFields to select safe fields.
- Do not rely solely on client-side validation -- always validate on the server. Razor form validation is for UX, not security.
- Do not use
FromSqlRaw with string interpolation -- use FromSqlInterpolated which auto-parameterizes.
- Do not store secrets in
appsettings.json -- use user secrets for development and environment variables or managed identity for production. See [skill:dotnet-secrets-management].
- Do not generate security-sensitive code using deprecated patterns -- CAS, APTCA, .NET Remoting, DCOM, and BinaryFormatter are all unsupported in modern .NET. See the Deprecated Security Patterns section above.
Prerequisites
- .NET 8.0+ (LTS baseline)
- ASP.NET Core 8.0+ for security middleware, anti-forgery, and rate limiting
- Microsoft.AspNetCore.Identity for authentication/identity (if using A07 patterns)
References