Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

ASP.Net MVC Client / Server Bot Trap

0.00/5 (No votes)
6 Feb 2012 2  
Using Client Side Timer and Hidden Elements to catch a Bot submission in Asp.Net MVC

Introduction

After recently noticing on my Joomla website, the volume of spam that was still being submitted to my guestbook (about 5000 per month), despite having a Captcha mechanism in place, I was just casually browsing the net one day on the matter, and came across a blog post [1] in which the blog owner was also complaining about spam in his blog comments, and how he basically killed them stone dead by simply using a client side javascript timer and a hidden field checker on the submission of the comment.

This got me thinking, and given my recent interest in ASP.Net MVC, I wondered how the same thing could be achieved, and so a more of a little bit of learning, I fired up VS2010 and got down to business.

NOTE: I am still learning MVC, so excuse me if things look a bit dirty, it is more the concept I was interested in and seeing if I could work things out for myself and get a working prototype.

Requirements

A user entering form data will typically take some time to type out the information before hitting the submit button. On the other hand a bot will almost instantly fill out a form and submit it. What we need is:

  • A timer to run on the client side
  • A method of checking the timer has expired at the server side
  • A error to send back to the client if the post was deemed to be a bot entry

For this article, I will be using a simple Guestbook entry page as the test case. This will be built on MVC3 in C#, and I am using Visual Studio 2010 Pro. There is no requirement for any database in this demo, and we will just use an in memory list of guestbook entries to store the submissions and read back to the user.

Create the base project

The first thing we need to do is create the base project to build our demo on. Open up Visual Studio, in the list of project types, navigate to Visual C#, Web, ASP.Net MVC 3 Web Application, provide your project name and click ok. Visual Studio will now create the default application for a project of this type and provide the basic framework required for an MVC3 project.

If you look at the solution explorer image below, you will see what we are going to be adding and modifying within the project.

The Model and the Data Store

As I stated at the start of the article, we will not be using any database to store the guestbook entries. For the purposes of testing, we will simply create an in memory list of guestbook entries. The first thing we will do is create the object to represent the model. Right click on the Models folder in solution explorer and select Add Class. Name this file GuestbookModels. We define the object that represents a guestbook entry as shown below:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;            // <- We need to add this reference to make use of the data annotations

namespace FormSubmitBotTest.Models
{
    public class GuestbookModels
    {
        [Required]
        [DataType(DataType.Text)]
        [Display(Name = "Name")]
        [StringLength(50,ErrorMessage= "Name must be under 50 characters.")]
        public virtual string Name { get; set; }

        [Required]
        [DataType(DataType.EmailAddress)]
        [Display(Name = "Email Address (For Admin only)")]
        [RegularExpression("^[a-z0-9_\\+-]+(\\.[a-z0-9_\\+-]+)*@[a-z0-9-]+(\\.[a-z0-9-]+)*\\.([a-z]{2,4})$", ErrorMessage = "Email does not appear to be valid format.")]
        [StringLength(256, ErrorMessage="Email Address Length too long.")]
        public virtual string Email { get; set; }

        [Required]
        [DataType(DataType.Html)]
        [Display(Name = "Message")]
        [StringLength(255, ErrorMessage = "Message must be under 255 characters.")]
        public virtual string Message { get; set; }

        public virtual DateTime Date { get; set; }
    }
}

This is just a very basic guestbook entries. In reality, you would probably have more fields. As you can see above, I have also added a reference to allow us to make use of DataAnnotations. These provide some of the validation requirements such as fields being required, correct formats and restricting lengths. This is outwith the scope of the article, and will not be discussed further here, and there is plenty other sources to search against.

Next, we need somewhere to store the in memory 'database', i.e. a suitable object collection. To achieve this for the purpose of the demo, we will add this into the globals.asax file. At the start of the file you can see we are declaring a List<GuestbookModels>, and in the Application_Start method, we instantiate it. Now, this is very dirty way to do things, and would not be recommended in a production environment, you could get into all sorts of bother with threading issues etc, but this is just a demo!

    public class MvcApplication : System.Web.HttpApplication
    {
        //In memory Guestbook entry store
        public static List<FormSubmitBotTest.Models.GuestbookModels> Guestbook;
           
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
        }

        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                "Default", // Route name
                "{controller}/{action}/{id}", // URL with parameters
                new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
            );
        }

        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();

            RegisterGlobalFilters(GlobalFilters.Filters);
            RegisterRoutes(RouteTable.Routes);

            //instantiate in memory Guestbook Entries
             Guestbook = new List<FormSubmitBotTest.Models.GuestbookModels>();
        }
    }

The Controller

The next thing we will do is create the controller for the Guestbook. Again this is going to be a simple controller that will only permit viewing the entries and adding an entry. Right click the Controllers folder and select Add Controller. Enter the name GuestbookController as shown below and hit ok.

In this controller we are going to add some Actions. We will require one to handle the Index page and another to retrieve the AddEntry page. We will also add a Post method for the AddEntry which will handle the submission of the user form for adding the guestbook entry to the database. The code for the controller is shown below.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace FormSubmitBotTest.Controllers
{
    public class GuestbookController : Controller
    {
        //
        // GET: /Guestbook/
        public ActionResult Index()
        {
            //Get list of entries to pass to the view
            var entries = from gb in MvcApplication.Guestbook
                          orderby gb.Date descending  
                          select gb;
            
            return View(entries.ToList());
        }

        //
        // GET:/Guestbook/AddEntry
        public ActionResult AddEntry()
        {
            return View();
        }

        //
        // POST: /Guestbook/AddEntry
        [HttpPost]
        public ActionResult AddEntry(FormSubmitBotTest.Models.GuestbookModels model, FormCollection formCollection)
        {
            if (!formCollection["FormEntryBotWatch"].Equals("JS-GOT-ME"))
            {
                ModelState.AddModelError("", "The controller thinks you are a bot, please wait for message to say it is safe to submit entry (30 Seconds)");
            }
            else
            {
                if (ModelState.IsValid)
                {
                    //Add the server Date/Time to the record
                    model.Date = DateTime.Now;

                    //Add the entry to the in memory guestbook
                    MvcApplication.Guestbook.Add(model);

                    //If we got here, everything ok, redirect back to the index
                    return RedirectToAction("Index", "GuestBook");
                }
            }
            return View();
        }
    }
}

The Index action simply returns a list of entries in the data store, and passes them to the view. The get action for the AddEntry is a straight forward view. Next is the [HttpPost], in this you will notice a model and a form collection references in the constructor. These parameters take care of the data being based between the view and controller. The one we are interested in is the FormCollection. In the code you will notice we are checking to see if the form element FormEntryBotWatch is not equal to the value "JS-GOT-ME". This form element is initialised with a different value in the view (which we will see later), and we are checking to see if it is the expected value after the timer has changed its value.

If the value does not match the expected value, the javascript has not fired and a ModelError is injected into the ModelState with an appropriate message. The whole lot is passed back to the view. This way, the form data that has been entered is not lost, and the user just simply has to wait the appropriate time to re-submit. In reality, the timer could be 5 or 10 seconds, but for the purposes of this demo, I have set it to 30 seconds, to be able to track the changes in the browser debugger.

If the value does match the expected value of the form element, we simply check the model state is valid and add it to the data store, followed by redirecting the user back to the index. Again, if there is an issue with the model i.e. the model state is invalid, we simple return back to the view.

The Views

First, right click on the Views folder and add a folder Guestbook this will hold the views returned by the guestbook controller. Right click this folder and Add Views for AddEntry and Index.

Firstly, let us take a quick look at the Index view:

@model List<FormSubmitBotTest.Models.GuestbookModels>
              
@{
    ViewBag.Title = "Guestbook Index";
}

<h2>Guestbook Index</h2>

@Html.ActionLink("Add Entry","AddEntry") to the guestbook <br /><hr />

@{
    if (Model.Count > 0)
    {
        foreach (var item in Model)
        {
        <table>
            <tr><td>From:</td><td>@item.Name</td></tr>
            @if (User.IsInRole("Administrator"))
                {
                <tr><td>Email:</td><td>@item.Email</td></tr>
                } 
            <tr><td>Message:</td><td>@item.Message</td></tr> 
            <tr><td>Date:</td><td>@item.Date.ToLongDateString(), @item.Date.ToLongTimeString()</td></tr> 
        </table>
        <hr/>
        }
     }
     else 
     {
        <p>The guestbook contains no entries.</p>
     }
}

First we pass in a reference to the model, in this case a list of Guestbook entries, then we simply build a table for each entry. You will notice, if the current user is member of the Administrator role, then they can also see the email address of the poster. There is also a link to the Add Entry action for submitting a guestbook entry. The index view will with a populated list of one entry is shown below.

Now, let us take a look at the Add Entry view:

@model FormSubmitBotTest.Models.GuestbookModels

@{
    ViewBag.Title = "Guestbook Add Entry";
}

<h2>Guestbook Add Entry</h2>
@using (Html.BeginForm())
{
    @Html.ValidationSummary(true, "Unable to add entry. Please correct the errors and try again.")
    <div>
        <fieldset>
            <legend>Guestbook Entry</legend>
            <div>
            @Html.Hidden("FormEntryBotWatch","MVC-IS-WATCHING-YOU")
            </div>

            <div class="editor-label">
                @Html.LabelFor(m => m.Name)
            </div>
            <div class="editor-field">
                @Html.TextBoxFor(m => m.Name, new { @MaxLength = "50" })
                @Html.ValidationMessageFor(m => m.Name)
            </div>

            <div class="editor-label">
                @Html.LabelFor(m => m.Email)
            </div>
            <div class="editor-field">
                @Html.TextBoxFor(m => m.Email, new { @MaxLength = "256" })
                @Html.ValidationMessageFor(m => m.Email)
            </div>

            <div class="editor-label">
                @Html.LabelFor(m => m.Message)
            </div>
            <div class="editor-field">
                @Html.TextBoxFor(m => m.Message,new {@MaxLength = "255"})
                @Html.ValidationMessageFor(m => m.Message)
            </div>

            <script type="text/javascript">
                setTimeout(function () {
                    var element = document.getElementById("FormEntryBotWatch");
                    element.setAttribute("value", "JS-GOT-ME");
                    var message = document.getElementById("submitMessage");
                    message.innerHTML = "You may submit the form when ready.";
                }, 30000);
            </script>
            <div id="submitMessage"></div>
            <p>
                <input type="submit" value="Submit Entry" />
            </p>
        </fieldset>
    </div>
}

The GuestBookModels is defined as the model for the view. Using the DataAnnotations in the model, the various labels and fields are established on the form. In addition to this, there is the error message containers used by the validation process to notify the issues to the user. At the top of the form there is a hidden field FormEntryBotWatch with the value "MVC-IS-WATCHING-YOU". A script block is placed on the view which performs first establishes the timer and the function to execute after the predetermined interval, in this case 30 Seconds. The function will first change the value of the hidden field to "JS-GOT-ME" and then secondly, display a message that the form may be submitted when the user is ready.

If the form is submitted before the timer has executed, the controller pushes the error message and the pre-populated form elements back to the user as shown below. In addition to this, if there are any fields that are not valid for the model, the error messages are also displayed for these elements, as shown in the image below.

The Shared Layout

The other thing that needs to be done is to allow us to access the guestbook easily (without manually having to type URLs) is to updated the shared layout. The _Layout.cshtml file simply has another navigation tab entry added to the list. The additional code is shown below.

    <ul id="menu">
        <li>@Html.ActionLink("Home", "Index", "Home")</li>
        <li>@Html.ActionLink("About", "About", "Home")</li>
        <li>@Html.ActionLink("Guestbook", "Index", "Guestbook")</li>
    </ul>

C# vs VB

I decided to port the project across to VB, just to see how much different the Razor markup etc, would be. I was surprised to see quite a few differences, and you appear to have to jump through several more hoops to get the same things done. It is safe to say, I will be keeping clear of MVC with VB and stick to C#.

Some of the difference in the markup are;

@model vs @modeltype

@{...} vs @code ... end Code

@Html.LabelFor(m => m.Name) vs @Html.LabelFor(Function(model) model.Name)

Summary

In summary, we have used a simple timer on the client side to try and differentiate between a user and bot. This could of course be combined with a Captcha. I have found this a nice simple learning project, which has helped to further understand some of the elements of MVC and Razor, particularly around the validation and error messages.
There are many other things you could also do, and this does have some limitations, you can read some of these in the comments of the original blog post. However, that wasn't the point of the exercise at this time.

Hope you have at least enjoyed the read, so thanks for reading.

References

History

  • 6th February 2012 - V1.2, Add equivalent VB project, and additional C# vs VB section
  • 5th February 2012 - V1.1, Solution Explorer Image corrected.
  • 5th February 2012 - V1.0, First submission of article

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here