Securing a REST API: 7 Common Vulnerabilities and How to Prevent Them
Summary: Explore 7 common REST API vulnerabilities through practical C# and ASP.NET Core examples. Learn how to prevent broken access controls, SQL injection, improper input validation, sensitive data exposure, and request abuse using proven security best practices.
The Codanalyst team
7 min read
Contents
A REST API can work correctly, follow HTTP conventions, and return perfectly structured JSON responses while still exposing sensitive data or allowing unauthorized operations.
The problem does not always come from a complex vulnerability. Sometimes, a single missing authorization check in a controller is enough to compromise an entire feature.
Consider an example: an API allows users to view their orders. Authentication works, the tokens are valid, and the requests are processed correctly.
However, if a user can view another user's orders simply by changing an ID in the URL, the application has an access control vulnerability.
These are the types of problems we will examine in this article.
What you will learn: how to identify seven categories of vulnerabilities, understand how they appear in code, and discover the measures you can apply to better secure a REST API.
1. Broken Access Control: BOLA
BOLA stands for Broken Object Level Authorization. The term IDOR is also commonly used to describe similar issues.
This vulnerability occurs when an API verifies that a user is authenticated but does not properly verify whether that user is authorized to access the requested resource.
Vulnerable code example
Imagine an endpoint that retrieves an order by its ID:
[HttpGet("{id:int}")]
public async Task<IActionResult> GetOrder(int id)
{
var order = await _dbContext.Orders
.FindAsync(id);
if (order is null)
return NotFound();
return Ok(order);
}
The problem is simple: there is no check ensuring that the order belongs to the authenticated user.
If a user knows the ID of another order, they may be able to retrieve information they should not have access to.
How to fix it
If users should only be able to access their own orders, the query can enforce that restriction directly:
[Authorize]
[HttpGet("{id:int}")]
public async Task<IActionResult> GetOrder(int id)
{
var userId = User.FindFirst(
System.Security.Claims.ClaimTypes.NameIdentifier
)?.Value;
if (userId is null)
return Unauthorized();
var order = await _dbContext.Orders
.FirstOrDefaultAsync(o =>
o.Id == id &&
o.UserId == userId);
if (order is null)
return NotFound();
return Ok(order);
}
This version limits the query to orders belonging to the authenticated user. In a real-world application, adapt the logic to your business rules and any authorized roles.
Being authenticated does not mean being authorized.
A valid token does not automatically grant access to every resource in the application. Each operation must enforce the appropriate access control rules.
2. Improperly Configured Authentication
Authentication is used to determine the identity of a user or service. When it is poorly implemented, an API may accept requests from actors who should not have access.
In ASP.NET Core, a private endpoint can be protected with the [Authorize] attribute:
[Authorize]
[HttpGet("profile")]
public IActionResult GetProfile()
{
return Ok(new
{
Message = "Access granted"
});
}
However, the attribute alone is not enough. The authentication mechanism itself must be properly configured.
For an API using JWT, you should verify at least:
- the token signature;
- its expiration date;
- the issuer (
issuer); - the audience (
audience); - the allowed algorithms and signing keys;
- the roles and permissions actually granted.
You should also have appropriate policies for login attempts, account recovery, and session or token revocation when supported by your architecture.
A signed JWT is not necessarily encrypted.
Its contents can generally be decoded by whoever possesses the token. Never put passwords, secret keys, or sensitive information inside a JWT.
3. SQL Injection
SQL injection occurs when user-controlled data alters the structure of a SQL query.
The risk is particularly high when an application manually builds queries by concatenating strings.
Bad practice
var sql =
"SELECT * FROM Users WHERE Email = '" +
email + "'";
Here, the value of email is inserted directly into the query.
An unexpected input could alter the SQL logic executed by the database server.
Recommended approach
With Entity Framework Core, prefer LINQ queries:
var user = await _dbContext.Users
.FirstOrDefaultAsync(u => u.Email == email);
The ORM generates a parameterized query appropriate for the configured database provider.
If you use raw SQL, make sure values are passed as parameters rather than concatenated into the query.
Using an ORM does not guarantee complete protection against SQL injection.
Raw SQL queries, poorly designed stored procedures, and certain dynamic query constructions can still introduce vulnerabilities.
4. Insufficient Input Validation
An API should never assume that data received from a client is valid.
Even if the frontend enforces certain constraints, a client can send an HTTP request directly with completely different values.
Consider an endpoint for creating a user.
Define validation rules
using System.ComponentModel.DataAnnotations;
public class CreateUserRequest
{
[Required]
[EmailAddress]
public string Email { get; set; } = string.Empty;
[Required]
[StringLength(100, MinimumLength = 2)]
public string Name { get; set; } = string.Empty;
}
With an ASP.NET Core controller decorated with [ApiController], model validation errors normally result in an automatic HTTP 400 response.
[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
[HttpPost]
public IActionResult Create(CreateUserRequest request)
{
return Ok(new
{
request.Email,
request.Name
});
}
}
This example demonstrates structural validation such as required fields and string length. Business rules — for example, checking whether an email address is already registered — should be handled separately.
Input validation and authorization are two different security controls.
A value can be perfectly valid while the user sending it is still not authorized to use it to modify a particular resource.
5. Excessive Data Exposure
An API can return significantly more information than the client actually needs.
Imagine an endpoint that directly returns a database entity:
return Ok(user);
If that entity contains an internal or sensitive field, it may be exposed in the JSON response depending on the application's serialization configuration.
Use a dedicated DTO
public sealed record UserResponse(
int Id,
string Name,
string Email
);
Then explicitly construct the response:
var response = new UserResponse(
user.Id,
user.Name,
user.Email
);
return Ok(response);
The DTO explicitly defines which fields are intended for the client. This approach helps prevent accidentally exposing every property of an internal data model.
Never return passwords, password hashes, private keys, or internal secrets in an API response.
Each response should contain only the information required by the requested operation.
6. Missing Rate Limiting
Some endpoints are particularly sensitive to abuse, including login, password recovery, SMS delivery, code generation, and resource-intensive operations.
Without appropriate limits, an API may be exposed to excessive traffic, repeated attempts, abuse, or unexpected costs.
ASP.NET Core provides a built-in rate limiting middleware.
Example configuration
using System.Threading.RateLimiting;
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("api", limiter =>
{
limiter.PermitLimit = 60;
limiter.Window = TimeSpan.FromMinutes(1);
limiter.QueueLimit = 0;
limiter.AutoReplenishment = true;
});
});
Enable the middleware in the request pipeline:
app.UseRateLimiter();
You can then apply the policy to specific endpoints:
[EnableRateLimiting("api")]
[HttpGet("catalog")]
public IActionResult GetCatalog()
{
return Ok();
}
This example policy allows 60 requests per minute for each configured partition. The actual limits should be adapted to the expected traffic and characteristics of the service.
A single global rate limit is not always enough.
Login, SMS delivery, and account recovery endpoints may require stricter and more specific policies. In distributed environments, also verify whether limits need to be shared across multiple application instances.
7. Improper Error Handling
Errors are useful for diagnosing problems, but internal details should not be unnecessarily exposed to clients.
A response such as the following is inappropriate in production:
System.NullReferenceException
at MyApplication.Services.OrderService.GetOrder(...)
C:\Build\MyApplication\OrderService.cs
It may reveal details about the application's architecture and deployment environment.
Prefer a controlled response
{
"error": "An unexpected error occurred.",
"traceId": "example-trace-id"
}
The correlation ID should allow the corresponding event to be located in internal logs without exposing the stack trace to the client.
In production:
- disable detailed error pages;
- avoid logging tokens and secrets in plain text;
- restrict access to logs;
- retain the context required for troubleshooting;
- use consistent error responses.
Hiding technical details does not mean ignoring errors.
Errors should still be logged, monitored, and investigated, with information appropriate to the sensitivity of the system.
Reviewing an API Before Production
An effective security review is not just about looking for mistakes in the code. It should also verify how the API behaves in practice.
For every endpoint, ask yourself:
- Who can call this endpoint?
- Which resources can that user access or modify?
- Is incoming data validated on the server?
- Does the response contain only the required information?
- Are abuse scenarios and errors handled correctly?
- Do tests cover both authorized and unauthorized access?
To explore these topics further, consult the official resources:
- OWASP API Security Top 10
- ASP.NET Core authorization documentation
- ASP.NET Core rate limiting documentation
Conclusion
Securing a REST API is not simply about validating a token or adding a few attributes to controllers.
Security controls must be applied consistently across the entire application: authentication, authorization, input validation, data access, response handling, and traffic management.
The seven vulnerabilities covered in this article provide a practical starting point for improving the security of an existing API or designing a new one.
The key rule to remember: treat all data received from a client as untrusted, and explicitly authorize every sensitive operation on the server.
Security should be part of the design, development, testing, and deployment process — not something checked only at the end of a project.
Comments
Comments are reviewed before publication.