Prevent Repeated Requests using ActionFilters in ASP.NET MVC

Wherever there is a form or page that allows a user to post up information, there is an opportunity for repeat postings and spam. No one really enjoys being spammed or seeing hundreds of the same comments strewn across their forum, blog or other areas of discussion and this article aims to help curb that.

Using a Custom ActionFilter within ASP.NET MVC, we can create a reusable and flexible solution that will allow you to place time-limit constraints on the Requests being sent to your controllers that will assist in deterring spammers (and those that aren’t very tech-savy) from bombarding you with duplicate items.

The Problem

A user with malicious intent decides that they want to spam your form and that you should have hundreds of duplicate messages decorating your forum, database (or whatever else you decide to do with posted data) as fast as the spammer can submit your form.

The Solution

To help prevent these multiple submission attempts, we will need to create a Custom ActionFilter that will keep track of the source of the Request (to ensure that it isn’t the same person), a delay value (to indicate the duration between attempts) and possibly some additional information such as error handling.

But first, let’s start with the ActionFilter itself :

public class PreventSpamAttribute : ActionFilterAttribute
{
       public override void OnActionExecuting(ActionExecutingContext filterContext)
       {
              base.OnActionExecuting(filterContext);
       }
}

This is a very basic implementation of an ActionFilter that will override the OnActionExecuting method and allow the addition of our extra spam-deterrent features. Now we can begin adding some of the features that we will need to help accomplish our mission, which includes :

  • A property to handle the delay between Requests.
  • A mechanism to uniquely identify the user making the Request (and their target).
  • A mechanism to store this information so that it is accessible when a Request occurs.
  • Properties to handle the output of ModelState information to display errors.

Let’s begin with adding the delay, which will just be an integer value that will indicate (in seconds) the minimum delay allowed between making Requests to a specific Controller Action along with some additional properties that will store information to handle displaying errors and redirecting invalid requests :

public class PreventSpamAttribute : ActionFilterAttribute
{
       //This stores the time between Requests (in seconds)
       public int DelayRequest = 10;
       //The Error Message that will be displayed in case of excessive Requests
       public string ErrorMessage = "Excessive Request Attempts Detected.";
       //This will store the URL to Redirect errors to
       public string RedirectURL;

       public override void OnActionExecuting(ActionExecutingContext filterContext)
       {
              base.OnActionExecuting(filterContext);
       }
}

Identify the Requesting User and their Target

Next, we will need a method to store current information about the User and where their Request is originating from so that we can properly identify them. One method to do this would be to get some identifying information about the user (such as their IP Address) using the “HTTP_X_FORWARDED_FOR” header (and if that doesn’t exist, falling back on the “”REMOTE_ADDR” header value) and possibly appending the User Agent (using the “USER_AGENT” header)  in as well to further hone in on our User.

public override void  OnActionExecuting(ActionExecutingContext filterContext)
{
       //Store our HttpContext (for easier reference and code brevity)
       var request = filterContext.HttpContext.Request;

       //Grab the IP Address from the originating Request (very simple implementation for example purposes)
       var originationInfo = request.ServerVariables["HTTP_X_FORWARDED_FOR"] ?? request.UserHostAddress;

       //Append the User Agent
       originationInfo += request.UserAgent;

       //Now we just need the target URL Information
       var targetInfo = request.RawUrl + request.QueryString;

       base.OnActionExecuting(filterContext);
}

Generate a Hash to Uniquely Identify the Request

Now that we have the unique Request information for our User and their target, we can use this to generate a hash that will be stored and used to determine if later and possibly spammed requests are valid.

For this we will use the .NET Cryptography library (System.Security.Cryptography) to create a simple MD5 hash of your string values, so you will need to include the appropriate using statement where your ActionFilter is being declared :

using System.Security.Cryptography;

We can leverage LINQ to perform a short little single line conversion of our strings to a hashed string using this line (from this previous blog post) :

//Generate a hash for your strings (this appends each of the bytes of the value into a single hashed string
var hashValue = string.Join("", MD5.Create().ComputeHash(Encoding.ASCII.GetBytes(originationInfo + targetInfo)).Select(s => s.ToString("x2")));

Storing the Hash within the Cache

We can use the hashed string as a key that will be stored within the Cache to determine if a Request that is coming through is a duplicate and handle it accordingly.

public override void  OnActionExecuting(ActionExecutingContext filterContext)
{
       //Store our HttpContext (for easier reference and code brevity)
       var request = filterContext.HttpContext.Request;
       //Store our HttpContext.Cache (for easier reference and code brevity)
       var cache = filterContext.HttpContext.Cache;

       //Grab the IP Address from the originating Request (very simple implementation for example purposes)
       var originationInfo = request.ServerVariables["HTTP_X_FORWARDED_FOR"] ?? request.UserHostAddress;

       //Append the User Agent
       originationInfo += request.UserAgent;

       //Now we just need the target URL Information
       var targetInfo = request.RawUrl + request.QueryString;

       //Generate a hash for your strings (this appends each of the bytes of the value into a single hashed string
       var hashValue = string.Join("", MD5.Create().ComputeHash(Encoding.ASCII.GetBytes(originationInfo + targetInfo)).Select(s => s.ToString("x2")));

       //Checks if the hashed value is contained in the Cache (indicating a repeat request)
       if (cache[hashValue] != null)
       {
               //Adds the Error Message to the Model and Redirect
               filterContext.Controller.ViewData.ModelState.AddModelError("ExcessiveRequests", ErrorMessage);
       }
       else
       {
               //Adds an empty object to the cache using the hashValue to a key (This sets the expiration that will determine
               //if the Request is valid or not
               cache.Add(hashValue, null, null, DateTime.Now.AddSeconds(DelayRequest), Cache.NoSlidingExpiration, CacheItemPriority.Default, null);
       }
       base.OnActionExecuting(filterContext);
}

Decorating your Methods

In order to apply this functionality to one of your existing methods, you’ll just need to decorate the method with your newly created [PreventSpam] attribute :

//Displays your form initially
public ActionResult YourPage()
{
       return View(new TestModel());
}

[HttpPost]
[PreventSpam]
public ActionResult YourPage(TestModel yourModel)
{
       //If your Model was valid - output that it was successful!
       if (ModelState.IsValid)
       {
              return Content("Success!");
       }
       //Otherwise return the model to the View
       else
       {
              return View(yourModel);
       }
}

Now when you visit your Controller Action, you’ll be presented with a very simple form (created using the built-in scaffolding within MVC based on an Example Model) :

An example form awaiting submission.

An example form awaiting submission.

which after submitting will output a quick “Success!” message letting you know that the POST was performed properly.

However, if you decide to get an itchy trigger finger and go back any try to submit your form a few more times within the delay that was set within your attribute, you’ll be met with this guy :

Error Message for Repeated Requests

The ValidationSummary in your form will let you know that you cannot do that.

Since the properties within your ActionFilter are public, they can be accessed within the actual PreventSpam attribute if you wanted to change the required delay, error message or add any other additional properties that you desire.

//This action can only be accessed every 60 seconds and any additional requests within that timespan will 
//notify the user with a custom message. 
[PreventSpam(DelayRequest=60,ErrorMessage="You can only create a new widget every 60 seconds.")]
public ActionResult YourActionName(YourModel model)
{
       //Your Code Here
}

Summary

While I have no doubts that this is not any kind of airtight solution to the issue of spamming in MVC applications, it does provide a method to help mitigate it. This ActionFilter-based solution is also highly flexible and can be easily extended to add some additional functionality and features if your needs required it.

Hopefully this post provides a bit more insight into the wonderful world of ActionFilters and some of the functionality that they can provide to help solve all kinds of issues.

About these ads

4 thoughts on “Prevent Repeated Requests using ActionFilters in ASP.NET MVC

  1. Interesting article, but I have a question.

    Why should you hash that request? Couldn’t you just append the data to a single string? E.g:
    originationInfo += targetInfo;
    // Store originationInfo

    Is it because the hashed data is smaller?

  2. @Anders

    Firstly thanks for visiting :)

    I suppose that isn’t really any reason in particular to actually hash the Request itself.

    I had originally thought that you wouldn’t want to expose any of the appended strings (such as the IP Address etc) within the Cache value itself so I elected to use a Hash to generate a token that would be more similar to an Authentication Token. However, the code will work the same with an appended string or series of strings as well.

    It appears to just be an over-secure and over-thought method of handing it.

    • Ah, I see!

      As you say, I suspected a somewhat over-thought method, since it creates a small performance decrease (although a very small impact, since calculating a MD5 is not a huge task for modern cpu’s)

  3. Pingback: Creating Advanced Audit Trails using ActionFilters in ASP.NET MVC | rionscode

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s