Security Deep Dive for .NET APIs: OWASP, Rate Limiting, Headers, and CORS
Published:
This post covers practical security controls for ASP.NET Core APIs: OWASP API risks, rate limiting, security headers, CORS, authentication, authorization, and input handling. Security is not one feature. It is a set of controls that reduce the chance and impact of abuse.
OWASP API risks
The OWASP API Security Top 10 highlights common API failure modes. Examples include:
- broken object-level authorization
- broken authentication
- excessive data exposure
- unrestricted resource consumption
- broken function-level authorization
- unsafe consumption of third-party APIs
For .NET APIs, the practical response is:
- authorize access to each resource
- validate inputs
- avoid exposing entity models directly
- limit request sizes and rates
- log security-relevant failures
- keep dependencies updated
Object-level authorization
A common bug is checking that a user is authenticated but not checking that they can access the specific record.
Bad:
[Authorize]
[HttpGet("orders/{id:int}")]
public async Task<IActionResult> GetOrder(int id)
{
return Ok(await repository.GetByIdAsync(id));
}
Better:
var order = await repository.GetByIdAsync(id);
if (order is null)
return NotFound();
if (order.CustomerId != currentUser.CustomerId)
return Forbid();
return Ok(order);
Authentication is not enough. Resource access must be checked.
Rate limiting
Rate limiting protects APIs from abusive or accidental high-volume traffic.
Example:
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("api", limiter =>
{
limiter.PermitLimit = 100;
limiter.Window = TimeSpan.FromMinutes(1);
});
});
var app = builder.Build();
app.UseRateLimiter();
app.MapGet("/api/products", () => Results.Ok())
.RequireRateLimiting("api");
Rate limiting is especially important for:
- login endpoints
- expensive search endpoints
- public APIs
- endpoints that trigger external calls
CORS
CORS controls which browser origins can call your API. It is not an authentication system.
Example:
builder.Services.AddCors(options =>
{
options.AddPolicy("frontend", policy =>
{
policy
.WithOrigins("https://app.example.com")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
app.UseCors("frontend");
Avoid:
.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod()
That may be acceptable for a local prototype, but it is usually too broad for production.
Security headers
APIs and web apps benefit from security headers.
Common headers:
Strict-Transport-SecurityX-Content-Type-OptionsContent-Security-PolicyReferrer-Policy
Example middleware:
app.Use(async (context, next) =>
{
context.Response.Headers["X-Content-Type-Options"] = "nosniff";
context.Response.Headers["Referrer-Policy"] = "no-referrer";
await next();
});
For browser-facing apps, Content Security Policy deserves careful design and testing.
Input and output safety
Use request DTOs and response DTOs. Avoid binding directly to EF Core entities.
Why:
- prevents over-posting
- prevents leaking internal fields
- keeps public contracts stable
- makes validation explicit
Example:
public sealed record UpdateUserRequest(string DisplayName);
public sealed record UserResponse(int Id, string DisplayName);
Common mistakes to avoid
Watch for these issues:
- trusting client-side validation
- using CORS as a security boundary
- authorizing by role only when resource ownership matters
- logging tokens or passwords
- exposing internal entity fields in API responses
- leaving expensive endpoints without limits
API security is a set of repeated habits. Authentication gets users in the door, but authorization, validation, limits, and observability keep the system defensible.
Next Article: gRPC in .NET: Contracts, Streaming, and Interop
