OWASP reference: OWASP Top
10:2025
Security note: The OWASP Top 10 is an awareness document, not a
complete security standard or a substitute for threat modeling,
security testing, and your organization's controls. The examples below
are starting points; validate them against your application's
architecture and supported .NET version.
Introduction
An ASP.NET Core Web API can have clean code, good test coverage, and a
scalable architecture---and still expose sensitive data or business
operations if its security controls are incomplete.
Security is not just adding JWT authentication or enabling HTTPS. A
production API must verify who is calling, what that caller is allowed
to do, which inputs are trusted, how secrets are managed, how failures
are handled, and whether suspicious activity can be detected.
The OWASP Top 10:2025 identifies ten major categories of web
application security risk. This article translates each category into
practical ASP.NET Core Web API guidance, with C# examples and
implementation checks.
Table of contents
- Security foundations for an ASP.NET Core API
- A01:2025 --- Broken Access Control
- A02:2025 --- Security Misconfiguration
- A03:2025 --- Software Supply Chain Failures
- A04:2025 --- Cryptographic Failures
- A05:2025 --- Injection
- A06:2025 --- Insecure Design
- A07:2025 --- Authentication Failures
- A08:2025 --- Software or Data Integrity Failures
- A09:2025 --- Security Logging and Alerting Failures
- A10:2025 --- Mishandling of Exceptional Conditions
- A practical API security baseline
- Security checklist for code reviews
- Conclusion
- References
Security foundations for an ASP.NET Core API
Before looking at individual risks, establish a few fundamentals:
- Authentication answers: "Who is calling?"
- Authorization answers: "What is this caller allowed to do?"
- Input validation checks whether data meets the API's expected shape and business rules.
- Output and error handling ensure that responses disclose only what the caller should see.
- Observability makes security-relevant events detectable and actionable.
For a typical API protected by an identity provider, use a standard
OAuth 2.0/OpenID Connect flow and validate access tokens with ASP.NET
Core authentication middleware. For service-to-service access in Azure,
consider Microsoft Entra ID and managed identities where supported.
A minimal JWT bearer setup can look like this:
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = builder.Configuration["Identity:Authority"];
options.Audience = builder.Configuration["Identity:Audience"];
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ClockSkew = TimeSpan.FromMinutes(1)
};
});
builder.Services.AddAuthorization();
builder.Services.AddControllers();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
Configure the authority and audience for your identity provider. Do not
disable issuer, audience, lifetime, or signing-key validation to "make
tokens work." In production, also confirm signing-key rollover,
scopes/roles, token lifetime, and identity-provider configuration.
A01:2025 --- Broken Access Control
Broken access control occurs when an authenticated caller can perform an
operation or access a resource they should not be allowed to access.
This is one of the most important risks for APIs because clients
frequently send object identifiers such as customerId, orderId, or
accountId.
Vulnerable pattern
[HttpGet("{id:int}")]
public async Task GetOrder(int id)
{
var order = await _db.Orders.FindAsync(id);
return order is null ? NotFound() : Ok(order);
}
The endpoint verifies neither ownership nor permission. A caller may
change /api/orders/100 to /api/orders/101 and access another
customer's order. This is commonly described as insecure direct object
reference (IDOR), a form of broken access control.
Safer approach: enforce access at the resource query
[Authorize]
[HttpGet("{id:int}")]
public async Task GetOrder(int id)
{
var subject = User.FindFirst("sub")?.Value;
if (string.IsNullOrWhiteSpace(subject))
return Forbid();
var order = await _db.Orders
.Where(o => o.Id == id && o.OwnerSubject == subject)
.Select(o => new OrderResponse(o.Id, o.Status, o.Total))
.SingleOrDefaultAsync();
return order is null ? NotFound() : Ok(order);
}
This assumes OwnerSubject stores a stable subject identifier from the
trusted identity provider. Adapt the claim and ownership model to your
identity design. For multi-tenant systems, include tenant boundaries in
the query and verify that the tenant claim is trusted and correctly
mapped.
Practical controls
- Apply
[Authorize]by default to protected controllers or endpoints. - Use policies for roles, scopes, and business permissions.
- Check object-level authorization on every operation that reads or changes a resource.
- Do not trust a tenant ID, owner ID, role, or price supplied by the client.
- Restrict mass assignment by using request DTOs instead of binding persistence entities.
- Test access using different users, tenants, roles, and resource owners.
For complex rules, ASP.NET Core resource-based authorization can
centralize policy decisions. A role check alone is not enough if
permission depends on the specific resource.
A02:2025 --- Security Misconfiguration
Security misconfiguration includes unsafe defaults, exposed diagnostics,
overly permissive CORS, unnecessary endpoints, and inconsistent security
settings across environments.
Common ASP.NET Core pitfalls
- Running a production environment with the Developer Exception Page.
- Allowing every origin, header, and method through CORS without a business need.
- Exposing Swagger/OpenAPI UI publicly when it should be restricted.
- Forgetting HTTPS redirection or secure proxy configuration.
- Leaving unused middleware, sample controllers, health details, or administrative endpoints exposed.
- Relying on a reverse proxy to enforce controls without confirming the API's own deployment assumptions.
Configure CORS deliberately
builder.Services.AddCors(options =>
{
options.AddPolicy("Frontend", policy =>
{
policy
.WithOrigins("https://app.example.com")
.WithMethods("GET", "POST", "PUT", "DELETE")
.WithHeaders("Authorization", "Content-Type");
});
});
Apply the policy explicitly:
app.UseCors("Frontend");
CORS is a browser-enforced cross-origin policy, not authentication or
authorization. Non-browser clients can call an API regardless of CORS
rules. If cookies are used, configure credentials and anti-forgery
protections carefully; never combine credentialed requests with a
wildcard origin.
Production configuration checklist
- Use environment-specific configuration and safe defaults.
- Keep detailed exception pages and developer tools out of production.
- Restrict API documentation to intended audiences and protect it when necessary.
- Trust forwarded headers only from known proxies or networks; incorrect proxy configuration can affect scheme, host, and client-IP interpretation.
- Set request body and upload limits appropriate to the endpoint.
- Configure security headers at the correct layer for your deployment.
- Review cloud network access, private endpoints, firewall rules, and ingress settings.
- Verify health endpoints disclose no secrets, connection strings, or internal topology.
A03:2025 --- Software Supply Chain Failures
Modern .NET applications depend on NuGet packages, SDKs, container base
images, build tasks, GitHub Actions or Azure DevOps tasks, and
deployment artifacts. A vulnerable or compromised dependency or build
pipeline can undermine otherwise secure application code.
Reduce dependency risk
Run dependency and vulnerability checks as part of CI:
dotnet list package --vulnerable --include-transitive
Use this command with a supported SDK and package source configuration.
For automated pipelines, combine it with dependency scanning, repository
alerts, and a policy that fails builds for vulnerabilities that exceed
your organization's risk threshold.
Practical controls
- Keep supported .NET SDKs, runtimes, NuGet packages, and container images patched.
- Pin or centrally manage dependency versions; review updates rather than blindly accepting them.
- Commit and review
packages.lock.jsonwhere deterministic package restore is part of your process. - Use trusted package feeds and protect feed credentials.
- Scan direct and transitive dependencies.
- Protect CI/CD identities with least privilege and short-lived credentials where possible.
- Restrict who can modify pipeline definitions, release environments, and production approvals.
- Generate a software bill of materials (SBOM) where required, and retain provenance for build artifacts.
- Sign and verify artifacts when supported by your release platform and threat model.
A clean local build does not prove the supply chain is trustworthy. The
build agent, dependencies, package feed, and artifact promotion path are
part of the security boundary.
A04:2025 --- Cryptographic Failures
Cryptographic failures happen when sensitive data is exposed because it
is not protected appropriately, weak algorithms or key handling are
used, or secrets are stored insecurely.
Common examples
- Sending credentials or tokens over plain HTTP.
- Storing passwords in plaintext or using a fast general-purpose hash.
- Committing database passwords or signing keys to source control.
- Logging bearer tokens, API keys, or personal data.
- Using custom encryption instead of established platform libraries.
- Keeping encryption keys next to the encrypted data without suitable access controls.
Protect secrets outside source code
For local development, use the .NET Secret Manager for development-only
secrets. It is not an encrypted production secret vault. In Azure,
prefer Key Vault and managed identity rather than storing long-lived
credentials in configuration files.
// Example: access a secret through configuration.
// The production configuration provider should retrieve it
// from an approved secret store such as Azure Key Vault.
var connectionString =
builder.Configuration.GetConnectionString("OrdersDatabase");
if (string.IsNullOrWhiteSpace(connectionString))
{
throw new InvalidOperationException(
"The OrdersDatabase connection string is not configured.");
}
The example does not itself connect to Key Vault; configure the
appropriate provider and identity for your environment.
Practical controls
- Enforce HTTPS and correctly configure TLS at the ingress and application layers.
- Use platform password-hashing facilities such as ASP.NET Core Identity rather than inventing a password hash scheme.
- Store keys and secrets in a dedicated secret-management service with access auditing and rotation.
- Encrypt sensitive data at rest when required by the data classification and threat model.
- Minimize the sensitive data you collect, return, and retain.
- Redact credentials and personal data from logs, traces, and exception messages.
- Use cryptographic libraries and algorithms recommended by current platform guidance.
Encryption is not a replacement for authorization. If an attacker can
legitimately retrieve decrypted data through an over-permissive API,
encryption at rest will not solve the access-control problem.
A05:2025 --- Injection
Injection occurs when untrusted input is interpreted as executable SQL,
a shell command, a query language expression, or another instruction.
SQL injection: unsafe string concatenation
// Do not do this.
var sql = $"SELECT * FROM Orders WHERE CustomerId = '{customerId}'";
If the input is incorporated into SQL syntax, an attacker may alter the
query's meaning.
Prefer parameterized queries
With EF Core, normal LINQ queries are parameterized:
var orders = await _db.Orders
.Where(o => o.CustomerId == customerId)
.ToListAsync();
For raw SQL, use parameterized APIs:
var orders = await _db.Orders
.FromSqlInterpolated(
$"SELECT * FROM Orders WHERE CustomerId = {customerId}")
.ToListAsync();
FromSqlInterpolated parameterizes interpolated values. Do not replace
it with FromSqlRaw plus string concatenation. SQL parameters cannot
safely stand in for identifiers such as table names or sort directions;
map those choices to a fixed allowlist.
Validate input as well
public sealed record CreateOrderRequest(
[property: Required] string CustomerReference,
[property: Range(1, 100)] int Quantity);
Use request DTOs and server-side validation, but remember that
validation does not replace parameterized queries. Also:
- Avoid building shell commands from request values.
- Avoid unsafe dynamic expression construction.
- Treat uploaded files, headers, query parameters, and message-queue payloads as untrusted.
- Encode output appropriately when returning data to HTML-rendering clients.
- Review XML parsers, template engines, and search/query DSLs for injection-specific settings.
ASP.NET Core APIs often return JSON rather than HTML, so classic
reflected XSS is less common in the API response itself. However, unsafe
content may later be rendered by a browser-based client. JSON does not
make untrusted content safe for every downstream consumer.
A06:2025 --- Insecure Design
Insecure design is a weakness in the system's requirements, workflows,
or trust assumptions---not merely a coding defect. A technically correct
endpoint can still implement an unsafe business process.
Example: trusting a client-supplied price
public sealed record CreateOrderRequest(
int ProductId,
int Quantity,
decimal UnitPrice); // Do not trust this price from the client.
If the API accepts the price as authoritative, a caller may submit a
lower value than the real product price.
Instead, accept only the data the client is allowed to choose and
calculate authoritative values on the server:
public sealed record CreateOrderRequest(int ProductId, int Quantity);
[Authorize]
[HttpPost]
public async Task CreateOrder(
CreateOrderRequest request,
CancellationToken cancellationToken)
{
if (request.Quantity is < 1 or > 100)
return BadRequest("Quantity is outside the permitted range.");
var product = await _db.Products
.SingleOrDefaultAsync(
p => p.Id == request.ProductId && p.IsActive,
cancellationToken);
if (product is null)
return NotFound();
var order = new Order
{
ProductId = product.Id,
Quantity = request.Quantity,
UnitPrice = product.CurrentPrice
};
_db.Orders.Add(order);
await _db.SaveChangesAsync(cancellationToken);
return CreatedAtAction(
nameof(GetOrder),
new { id = order.Id },
new { order.Id, order.Quantity, order.UnitPrice });
}
This is illustrative; production code should also validate inventory,
currency, discounts, tax, concurrency, customer eligibility, and
transaction boundaries.
Design controls before coding
- Threat-model important flows such as payments, account recovery, tenant administration, and bulk exports.
- Define authorization rules for every operation and resource.
- Set limits for pagination, batch size, file size, request rate, and expensive queries.
- Make state transitions explicit and reject invalid transitions.
- Use idempotency keys for operations where retries could create duplicate effects.
- Require additional verification for high-impact actions when appropriate.
- Design for abuse cases, not only expected user journeys.
- Document trust boundaries between the API, identity provider, database, queues, and external services.
Security requirements should be acceptance criteria, not a last-minute
checklist after the feature is built.
A07:2025 --- Authentication Failures
Authentication failures include weak credential handling, incorrect
token validation, insecure authentication flows, and gaps in session or
account lifecycle controls.
Validate bearer tokens properly
Use the framework's JWT bearer middleware or a supported
identity-provider integration. Do not manually decode a JWT and treat
its claims as trusted without verifying the signature and relevant
validation rules.
For access tokens, verify at least:
- Signature and signing key.
- Issuer.
- Audience.
- Expiration and not-before timestamps.
- Required scopes or roles for the API operation.
- Token type and accepted algorithms as appropriate to the identity provider.
A valid token proves neither that the caller owns a specific record nor
that every operation is permitted.
Practical controls
- Prefer established identity providers over custom authentication implementations.
- Use authorization-code flow with PKCE for appropriate interactive clients; use client credentials for service-to-service access where appropriate.
- Do not use the OAuth resource-owner-password credentials flow for new designs.
- Apply rate limits and monitoring to login, password reset, OTP, and token-related endpoints.
- Use MFA for privileged and high-risk accounts where applicable.
- Avoid revealing whether a username or email exists in authentication and recovery responses.
- Rotate and revoke credentials when compromise is suspected.
- Keep access tokens out of URLs, logs, analytics, and error reports.
- Use managed identities for supported Azure service-to-service access rather than embedded secrets.
Authorization is still separate
[Authorize(Policy = "Orders.Read")]
[HttpGet("{id:int}")]
public Task GetOrder(int id)
{
// The policy checks a permission; the implementation must still
// verify that the caller may access this particular order.
throw new NotImplementedException();
}
Define the policy in the service configuration and adapt the claim type
to your identity provider:
csharp
builder.
Top comments (0)