ASP.NET Core - How to Secure Public APIs

ASP.NET Core Secure Public APIs

 
ASP.NET Core is a popular framework. Key benefits include features like cross-platform execution, high-performance, built-in Dependency Injection and modular HTTP request pipeline.
 

The challenge

 
ASP.NET Core provides support for many authentication providers to secure the app through numerous authentication workflows. However, in many scenarios, we have to provide a web application/site that relies on unauthenticated APIs with anonymous access.
 
For example, we have a list of products in a database and we want to display these products on a web page. We may write an API to provide the list of products and have the front end (web site) retrieve this list via the API and display them on our public products web page.
 
Without applying a level of security, such architectures can be a security vulnerability open for exploitation.
 

Available Security Controls in ASP.NET

 
ASP.NET Core provides a solution for common vulnerabilities, including,
  • Cross-Site Scripting
  • SQL Injection,
  • Cross-Site Request Forgery (CSRF)
  • Open redirects

Going a step further

 
As a developer, we should also protect our apps from other common attack vectors, including,
  • Distributed denial-of-service (DDOS)
  • Denial-of-service (DOS)
  • Bulk data egress
  • Probe response
  • Scraping
Two steps we can take are referer header checks and rate-limiting, discussed in detail below.
 

Use an IP Based Request Limit Action Filter

 
We can limit clients to a number of request within the specified time span to prevent malicious bot attacks. I have created an IP Based Request Limit Action Filter in ASP.NET Core. Do be aware that multiple clients may sit behind one IP address so you may want to cater for this in your limits, or combine the IP address with other request data to make requests more unique.
 
In order to yse the filter, you just need to add ActionAttribute on top of Controller Action.
  1. [HttpGet()]  
  2. [ValidateReferrer]  
  3. [RequestLimit("Test-Action", NoOfRequest = 5, Seconds = 10)]  
  4. public async Task<ActionResult> GetAsync(CancellationToken ct)  
  5. {  
  6.    // code here  
  7. }  
Here is the implementation of the filter,
  1. namespace Security.Api.Filters {  
  2.     using System;  
  3.     using System.Net;  
  4.     using Microsoft.AspNetCore.Mvc;  
  5.     using Microsoft.AspNetCore.Mvc.Filters;  
  6.     using Microsoft.Extensions.Caching.Memory;  
  7.     /// <summary>    
  8.     /// Action filter to restrict limit on no. of requests per IP address.    
  9.     /// </summary>    
  10.     /// <seealso cref="Microsoft.AspNetCore.Mvc.Filters.ActionFilterAttribute" />    
  11.     [AttributeUsage(AttributeTargets.Method)]  
  12.     public class RequestLimitAttribute: ActionFilterAttribute {  
  13.         public RequestLimitAttribute(string name) {  
  14.             Name = name;  
  15.         }  
  16.         public string Name {  
  17.             get;  
  18.         }  
  19.         public int NoOfRequest {  
  20.             get;  
  21.             set;  
  22.         } = 1;  
  23.         public int Seconds {  
  24.             get;  
  25.             set;  
  26.         } = 1;  
  27.         private static MemoryCache Cache {  
  28.             get;  
  29.         } = new MemoryCache(new MemoryCacheOptions());  
  30.         public override void OnActionExecuting(ActionExecutingContext context) {  
  31.             var ipAddress = context.HttpContext.Request.HttpContext.Connection.RemoteIpAddress;  
  32.             var memoryCacheKey = $ "{Name}-{ipAddress}";  
  33.             Cache.TryGetValue(memoryCacheKey, out int prevReqCount);  
  34.             if (prevReqCount >= NoOfRequest) {  
  35.                 context.Result = new ContentResult {  
  36.                     Content = $ "Request limit is exceeded. Try again in {Seconds} seconds.",  
  37.                 };  
  38.                 context.HttpContext.Response.StatusCode = (int) HttpStatusCode.TooManyRequests;  
  39.             } else {  
  40.                 var cacheEntryOptions = new MemoryCacheEntryOptions().SetAbsoluteExpiration(TimeSpan.FromSeconds(Seconds));  
  41.                 Cache.Set(memoryCacheKey, (prevReqCount + 1), cacheEntryOptions);  
  42.             }  
  43.         }  
  44.     }  
  45. }   

Add Referer Check Action Filter

 
To protect APIs from abuse and to provide additional protection against Cross-Site Request Forgery (CSRF) attacks, a security check is performed on the request Referer header for every REST API request sent to the server.
 
This validates where the API request has come from. I have created a Referer Check Action Filter in ASP.NET Core. It also prevents access from tools like POSTMAN, REST Client etc.
 
You just need to add ActionAttribute on top of Controller Action.
  1. [HttpGet()]  
  2. [ValidateReferrer]  
  3. public async Task<ActionResult> GetAsync(CancellationToken ct)  
  4. {  
  5.    // your code here  
  6. }  
Here is the implementation of the filter,
  1. namespace Security.Api.Filters {  
  2.     using Microsoft.AspNetCore.Http;  
  3.     using Microsoft.AspNetCore.Mvc;  
  4.     using Microsoft.AspNetCore.Mvc.Filters;  
  5.     using Microsoft.Extensions.Configuration;  
  6.     using System;  
  7.     using System.Linq;  
  8.     using System.Net;  
  9.     /// <summary>    
  10.     /// ActionFilterAttribute to validate referrer url    
  11.     /// </summary>    
  12.     /// <seealso cref="Microsoft.AspNetCore.Mvc.Filters.ActionFilterAttribute" />    
  13.     [AttributeUsage(AttributeTargets.Method)]  
  14.     public sealed class ValidateReferrerAttribute: ActionFilterAttribute {  
  15.         private IConfiguration _configuration;  
  16.         /// <summary>    
  17.         /// Initializes a new instance of the <see cref="ValidateReferrerAttribute"/> class.    
  18.         /// </summary>    
  19.         public ValidateReferrerAttribute() {}  
  20.         /// <summary>    
  21.         /// Called when /[action executing].    
  22.         /// </summary>    
  23.         /// <param name="context">The action context.</param>    
  24.         public override void OnActionExecuting(ActionExecutingContext context) {  
  25.             _configuration = (IConfiguration) context.HttpContext.RequestServices.GetService(typeof(IConfiguration));  
  26.             base.OnActionExecuting(context);  
  27.             if (!IsValidRequest(context.HttpContext.Request)) {  
  28.                 context.Result = new ContentResult {  
  29.                     Content = $ "Invalid referer header"  
  30.                 };  
  31.                 context.HttpContext.Response.StatusCode = (int) HttpStatusCode.ExpectationFailed;  
  32.             }  
  33.         }  
  34.         /// <summary>    
  35.         /// Determines whether /[is valid request] [the specified request].    
  36.         /// </summary>    
  37.         /// <param name="request">The request.</param>    
  38.         /// <returns>    
  39.         /// <c>true</c> if [is valid request] [the specified request]; otherwise, <c>false</c>.    
  40.         /// </returns>    
  41.         private bool IsValidRequest(HttpRequest request) {  
  42.             string referrerURL = "";  
  43.             if (request.Headers.ContainsKey("Referer")) {  
  44.                 referrerURL = request.Headers["Referer"];  
  45.             }  
  46.             if (string.IsNullOrWhiteSpace(referrerURL)) return false;  
  47.             // get allowed client list to check    
  48.             var allowedUrls = _configuration.GetSection("CorsOrigin").Get < string[] > ()?.Select(url => new Uri(url).Authority).ToList();  
  49.             //add current host for swagger calls    
  50.             var host = request.Host.Value;  
  51.             allowedUrls.Add(host);  
  52.             bool isValidClient = allowedUrls.Contains(new Uri(referrerURL).Authority); // comapre with base uri    
  53.             return isValidClient;  
  54.         }  
  55.     }  
  56. }   

Add DoS Attack Middleware

 
DoS attacks overwhelm your APIs, making them unresponsive and/or expensive if you have autoscale configured.. There are different ways to avoid this problem through request throttling. Here is one option using middleware to restrict the number of request from a particular client IP address.
 
Below is code for DosAttackMiddleware.cs
  1. namespace Security.Api.Middlewares {  
  2.     using Microsoft.AspNetCore.Http;  
  3.     using System.Collections.Generic;  
  4.     using System.Linq;  
  5.     using System.Net;  
  6.     using System.Threading.Tasks;  
  7.     using System.Timers;  
  8.     public sealed class DosAttackMiddleware {  
  9.         #region Private fields  
  10.         private static Dictionary < stringshort > _IpAdresses = new Dictionary < stringshort > ();  
  11.         private static Stack < string > _Banned = new Stack < string > ();  
  12.         private static Timer _Timer = CreateTimer();  
  13.         private static Timer _BannedTimer = CreateBanningTimer();  
  14.         #endregion  
  15.         private  
  16.         const int BANNED_REQUESTS = 10;  
  17.         private  
  18.         const int REDUCTION_INTERVAL = 1000; // 1 second    
  19.         private  
  20.         const int RELEASE_INTERVAL = 5 * 60 * 1000; // 5 minutes    
  21.         private RequestDelegate _next;  
  22.         public DosAttackMiddleware(RequestDelegate next) {  
  23.             _next = next;  
  24.         }  
  25.         public async Task InvokeAsync(HttpContext httpContext) {  
  26.             string ip = httpContext.Connection.RemoteIpAddress.ToString();  
  27.             if (_Banned.Contains(ip)) {  
  28.                 httpContext.Response.StatusCode = (int) HttpStatusCode.Forbidden;  
  29.             }  
  30.             CheckIpAddress(ip);  
  31.             await _next(httpContext);  
  32.         }  
  33.         /// <summary>    
  34.         /// Checks the requesting IP address in the collection    
  35.         /// and bannes the IP if required.    
  36.         /// </summary>    
  37.         private static void CheckIpAddress(string ip) {  
  38.             if (!_IpAdresses.ContainsKey(ip)) {  
  39.                 _IpAdresses[ip] = 1;  
  40.             } else if (_IpAdresses[ip] == BANNED_REQUESTS) {  
  41.                 _Banned.Push(ip);  
  42.                 _IpAdresses.Remove(ip);  
  43.             } else {  
  44.                 _IpAdresses[ip]++;  
  45.             }  
  46.         }  
  47.         #region Timers  
  48.         /// <summary>    
  49.         /// Creates the timer that substract a request    
  50.         /// from the _IpAddress dictionary.    
  51.         /// </summary>    
  52.         private static Timer CreateTimer() {  
  53.             Timer timer = GetTimer(REDUCTION_INTERVAL);  
  54.             timer.Elapsed += new ElapsedEventHandler(TimerElapsed);  
  55.             return timer;  
  56.         }  
  57.         /// <summary>    
  58.         /// Creates the timer that removes 1 banned IP address    
  59.         /// everytime the timer is elapsed.    
  60.         /// </summary>    
  61.         /// <returns></returns>    
  62.         private static Timer CreateBanningTimer() {  
  63.             Timer timer = GetTimer(RELEASE_INTERVAL);  
  64.             timer.Elapsed += delegate {  
  65.                 if (_Banned.Any()) _Banned.Pop();  
  66.             };  
  67.             return timer;  
  68.         }  
  69.         /// <summary>    
  70.         /// Creates a simple timer instance and starts it.    
  71.         /// </summary>    
  72.         /// <param name="interval">The interval in milliseconds.</param>    
  73.         private static Timer GetTimer(int interval) {  
  74.             Timer timer = new Timer();  
  75.             timer.Interval = interval;  
  76.             timer.Start();  
  77.             return timer;  
  78.         }  
  79.         /// <summary>    
  80.         /// Substracts a request from each IP address in the collection.    
  81.         /// </summary>    
  82.         private static void TimerElapsed(object sender, ElapsedEventArgs e) {  
  83.             foreach(string key in _IpAdresses.Keys.ToList()) {  
  84.                 _IpAdresses[key]--;  
  85.                 if (_IpAdresses[key] == 0) _IpAdresses.Remove(key);  
  86.             }  
  87.         }  
  88.         #endregion  
  89.     }  
  90. }   

Conclusion

 
Unauthenticated APIs are open to abuse. We must prevent obvious attack vectors by adding additional code. Hopefully this blog makes these controls easy to implement whilst making attacker’s lives more difficult.