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 < string, short > _IpAdresses = new Dictionary < string, short > ();
  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.