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
{
public int DelayRequest = 10;
public string ErrorMessage = "Excessive Request Attempts Detected.";
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)
{
var request = filterContext.HttpContext.Request;
var originationInfo = request.ServerVariables["HTTP_X_FORWARDED_FOR"] ?? request.UserHostAddress;
originationInfo += request.UserAgent;
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) :
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)
{
var request = filterContext.HttpContext.Request;
var cache = filterContext.HttpContext.Cache;
var originationInfo = request.ServerVariables["HTTP_X_FORWARDED_FOR"] ?? request.UserHostAddress;
originationInfo += request.UserAgent;
var targetInfo = request.RawUrl + request.QueryString;
var hashValue = string.Join("", MD5.Create().ComputeHash(Encoding.ASCII.GetBytes(originationInfo + targetInfo)).Select(s => s.ToString("x2")));
if (cache[hashValue] != null)
{
filterContext.Controller.ViewData.ModelState.AddModelError("ExcessiveRequests", ErrorMessage);
}
else
{
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 :
public ActionResult YourPage()
{
return View(new TestModel());
}
[HttpPost]
[PreventSpam]
public ActionResult YourPage(TestModel yourModel)
{
if (ModelState.IsValid)
{
return Content("Success!");
}
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.
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 :
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.
[PreventSpam(DelayRequest=60,ErrorMessage="You can only create a new widget every 60 seconds.")]
public ActionResult YourActionName(YourModel model)
{
}
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.