How To Top Spammers From Abusing Contact Forms
Posted by Dexter Nelson: Wednesday, November 13, 2013 (3:39 PM)
Stopping spammers, or at least slowing their roll to a manageable pace, is a full time job if you're a developer like me, and it's not a problem that's limited to contact forms either.
Nearly all of my applications have a form or mail function whether it's a contact form, registration form for member centers, lead generation forms for clients, comment boxes on blogs and forums; you name it.
If it has a form, someone will try to abuse it - and that means higher bandwidth, faulty registrations and fake accounts, a slower site (if it's really bad), and a lot more... not to mention a logistical nightmare having to try and manage it all, from approving messages to filtering out spam in your inbox.
You not only have to protect yourself, but protect your clients.
So what do you do if you run into problems with spammers and bots that try to ninja your forms?
Here are a few of the solutions I've come up with that almost anyone can do with just a little bit of knowledge. It's helped me to not only ward of spammers, but keep everything manageable and free up my time considerably.
1. Make sure that your own domain name can't be used to send forms. This is probably one of those big ones that really first put a dent in the amount of spam that was sent from my applications. Note: Now, I am a perl developer so I'm going to write this in perl code but I'm sure it's easily translateable to php or another language.
We could write a book on verifying email but I find basic regex works.
if ($email_address !~ /[\w\-]+\@[\w\-]+\.[\w\-]+/) { # $email_address is the variable from the email field in the form.
&dienice("You didn't enter a valid e-mail address.");
}
That will make sure that the email is a basic email address like [email protected] and make sure that there aren't any evil characters used, but we need to take that a step futher.
For the second part, I made a list of "bad names," or terms that someone can't put into a form, then kicks an error if they try to use it. Take a look.
my(@badname) = ("administrator", "admin", "root", "user", "username", "guest", "foo", "techdex.net");
foreach $i (@badname) {
if ($email_address =~ /$i/i) { # Use i at the end so it's insensitive matching.
&dienice("You are not allowed to create an account with $i in it.");
}
}
Blocking bad words was the second thing that really put a dent in the abuse. Not only was it blocking people from using my own domain name (techdex.net) but other things as well.
Now let's look at something more powerful...
2. Use POST or die - I know some people like to use the GET method (when information is passed in the browser's address bar), for their forms and this a personal preference of mine.
I always use POST, and then do this...
# lets block direct access that is not via the form post
if ($ENV{"REQUEST_METHOD"} ne "POST") {
&dienice("Method not allowed, input must be via a form post.");
}
The ENV{'REQUEST_METHOD'} will let you know which method is being used, POST or GET and if it's neither, (for example the spammer is using a bot, command line or some other method to bypass your form, the script dies.
This won't stop people from manually filling out your forms, or creating an iframe or something with a script that fills out the form, but it did kill about 1/3rd of the abuse after I implemented it.
3. CAPTCHA
Those 3 steps alone brought spam and abuse down a great deal. Using different forms that emailed me, spam literally went from thousands of messages a week to a few hundred a week, but I needed to go further.
These next few steps brought the spam down from hundreds a week, to around 10 or so a week and dropping.
4. Keyword Blocking: Remember in step 2 using the "badwords" blocking so people can't use certain words in the email address field? I did the same thing for the body. As spam got through, i'd take the time to make a note of URLs and keywords that people used and blocked them, except I used a mysql database.
First, let's create the database:
CREATE TABLE IF NOT EXISTS `ban_keyword` (
`keyword_id` int(8) NOT NULL AUTO_INCREMENT,
`keyword_name` varchar(32) NOT NULL,
`date` datetime NOT NULL,
PRIMARY KEY (`keyword_id`),
UNIQUE KEY `keyword_name` (`keyword_name`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1;
Very simple database and I created a web form that allows me, when I see a keyword I want to ban, to enter it into the form, click submit and it enters it into the database. Here's what that command looks like:
$sth = $dbh->prepare("INSERT INTO ban_keyword(keyword_name) values(?)") or die(mysql_error());
$sth->execute("$form_keyword_name"); # $form_keyword_name is the form variable passed.
The other part of this is to parse the form data being submitted and see if any of the keyword_names in the database are present. If they are, kill the form so nothing is sent. It works the same way as in the other example. Don't worry, it's not as hard as it seems.
my($searchstr);
my(@searchstr) = ($cgi->param('message'));
foreach $i (@searchstr) {
chomp($i);
$sth = $dbh->prepare("SELECT keyword_name from ban_keyword") or die(mysql_error());
$rv = $sth->execute;
while (my($keywords) = $sth->fetchrow_array) {
if ($i =~ /$keywords/i) {
&dienice("Process stopped. $i is a banned keyword.");
}
}
}
Pretty simple right? Make the entire body of the email into an array, then if the keyword exists anywhere, sending is stopped.
You'll like this next one that I came up with...
5. IP_WAIT Feature: A lot of people (including myself in recent past) like to use IP banning, but to be honest, it was a pain to keep up with.
I didn't like having to mess with a whole stack of IPs and constantly banning and unbanning IPs or messing with ranges and tables and all of that time-sucking boredom, so i came up with a more manageable solution that I like to call WAIT IP.
Here's the idea. Instead of banning IPs, what if a person had to wait a certain amount of time before they could submit again? They already have that but they're cheap counters. Someone could just wait and submit all day long right?
What if you could do that with an IP address, where instead of a timer script, and IP address is used? Check this out...
Same database set up:
CREATE TABLE IF NOT EXISTS `ip_wait` (
`ip_address` varchar(15) NOT NULL,
`date` datetime NOT NULL,
PRIMARY KEY (`ip_address`),
UNIQUE KEY `ip_address` (`ip_address`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
This happens automatically AFTER all security checks, so make sure it happens when email is sent successfully, (preferrably just before the mail is sent).
$sth = $dbh->prepare("INSERT INTO ip_wait(ip_address,date) values(?,current_timestamp())") or die(mysql_error());
$sth->execute("$ip_address"); # $ip_address is the global $ENV{'REMOTE_ADDR'} variable.
Now that ip addresses are automatically added to the database when your forms are submitted, you want to back up just a bit and add one more quick step to check and see if that ip address was recently used to submit a form.
# Get the current ip from the table and kick an error if it exists already.
$sth = $dbh->prepare("SELECT ip_address,date FROM ip_wait WHERE ip=? LIMIT 1") or die(mysql_error());
$rv = $sth->execute("$ip_address");
This will select the record from the database that matches the IP address of the submitting IP address.
a. If there is a match, you can just kill the form altogether like this.
if ($rv == 1) {
&dienice("Process halted! Your IP address was recently used to submit this form.
To prevent abuse we have enacted a 48 hour waiting period before this IP is released.");
}
Now, for the complicated thinkers out there, the logical step would have been to compare the timestamp in the database to the timestamp of the person's computer sending mail, then do some complicated scripting right? Yeah... i did that too, then I realized simpler was better.
DELETE FROM ip_wait WHERE date >= DATE_SUB(NOW(), INTERVAL 48 HOUR)
That is the query that you need to run on the database, and it will delete any records from the database where the interval between dates is greater than or equal to 48 hours.
I created a perl script (clean_ipwait.pl) with these two commands in it.
# Delete entries 48 hours or older.
$rv = $dbh->do("DELETE FROM ip_wait WHERE date >= DATE_SUB(NOW(), INTERVAL 48 HOUR)") or die(mysql_error());
# Optimize the table to eliminate overhead.
$rv = $dbh->do("OPTIMIZE TABLE `ip_wait`") or die(mysql_error());
And to further automate this, I created a cron tab that executes a perl script daily.
0 0 * * * perl /path/to/script/clean_ipwait.pl
Now, every day that script is run and the ip_wait table is automatically cleaned, and I don't have to mess with anything.
The result is that I get maybe a dozen spam emails all week on those test forms, and even better is that the number is decreasing because of the keyword banning. The last two on their own are effective on their own, however when you put all 5 together it is a powerful combination and very, very effective.
If you would like to download a working copy of the forms and scripts that I use, it's the latest $5.00 script download I have available.
Click here now to purchase and download. This is non-refundable.