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 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 anything else you might be using 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.
CodeProject
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 (example)
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 System.Security.Cryptography namespace 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 post :
// Generate a hash for your strings (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 (example)
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 (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 :
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 :
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.
CodeProject