Sunday, November 20, 2011

Trapping spambots with honeypots in MVC

Form spam is still a real issue for any public website. Spambots are mostly very primitive scripts. They’ll scan HTML for form fields, fill all of them with some value and submit the form. Using regular textbox fields, hidden from ‘real’ users by CSS, you can trick spam bots into filling in fields a normal user would leave blank. This gives you an easy way to tell people from bots.

Unobtrusive MVC honeypots
It’s quite straight forward to implement honeypot fields in ASP.NET MVC, but of course it should be as unobtrusive as possible. So, what I’ve come up with is an action filter to help out:

[HttpPost]
[HoneypotCaptcha("UserName")]
public virtual ActionResult Questionnaire( InputModel model )
{
...
}

The action filter checks the specified field in the post data, it must be present but empty. If it’s not empty an HTTP Exception with a 403 (access denied) status code is thrown.

public class HoneypotCaptchaAttribute : ActionFilterAttribute
{
  public HoneypotCaptchaAttribute( string formField )
  {
     if ( string.IsNullOrWhiteSpace( formField ) ) throw new ArgumentNullException( "formField" );
     FormField = formField;
  }

  public override void OnActionExecuting( ActionExecutingContext filterContext )
  {
     if ( filterContext.HttpContext.Request.HttpMethod == "POST" )
     {
        var value = filterContext.HttpContext.Request.Form[FormField];

        if ( value == null || value.Length > 0 )
        {
           throw new HttpException( 403, "Stop spamming this site." );
        }
     }
     base.OnActionExecuting( filterContext );
  }

  public string FormField { get; set; }
}

Html helper

To use the honeypot action filter, insert a form field and make it invisible using CSS.

<html>
  <head>
    <style type="text/css">
     .pooh { display: none; }
    <style>
  </head>
  </body>
    <% using( Html.Form() ) {%>
    <!-- your form stuff here -->
    <div class="pooh"><%: Html.Honeypot( "UserName" ) %></div>
    <% } %>
  <body>
<html>

This injects a label and text field:

public static class FormExtensions
{
    public static MvcHtmlString Honeypot(this HtmlHelper htmlHelper, string fieldName )
    {
       return MvcHtmlString.Create( htmlHelper.Label( fieldName, fieldName ).ToString() + htmlHelper.TextBox( fieldName, "" ) );
    }
}

If using this solution in a larger app you'll probably want to include the div in the helper and setup the CSS in the main style sheet.