Forum Moderators: phranque

Message Too Old, No Replies

Here's how to secure your formmail script from spammers

         

MichaelBluejay

12:51 pm on Oct 14, 2005 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



(Pros, please feel free to correct anything I got wrong, or to add important things I missed.)

At some point you might outgrow the generic off-the-shelf formmail script you downloaded from somewhere and decide to roll your own. The usual reason is that you want to customize the output/formatting of the message that gets sent. That's fine, but unless you're careful, spammers will hijack your formmail script to send out spam. This doesn't just hurt the people who get the spam message -- when they complain about it your host could shut down your website or even your whole server. Ouch! So let's see how to thwart attempts to hijack our formmail.

Spammers do their hijacking by copying your <FORM> onto their machine and changing the values of the fields. For example, if your <FORM> has a field called <recipient>, then our spammer can easily add a bunch of addresses separated by commas:

recipient= victim1@dom.com, victim2@dom.com, victim3@dom.com

An easy defense against this is to hard-code the To: address into your script:

VULNERABLE: print "To: $form{'recipient'}\n";

BETTER: print "To: me\@mydomain.com\n";

But we're not done. You probably want to let the visitor type in their own email address into the form, and have that be the From: address in the message that's sent to you, so you can easily click Reply to reply to your visitor. But our spammer can simply load up the <from> field with a bunch of addresses, too. To defend against this we can use a regexp to reject forms that have multiple email addresses:

$sender = $form{'email'};
if ($sender =~ /\@.*\@¦,/) {
print "There was a problem sending your message. Please email us directly at us(at)ourdomain.com. We apologize for the inconvenience.";
exit;
}

[The ¦ character is supposed to be a solid pipe, not the broken pipe that WebmasterWorld displays.]

The first part of that checks to see if there are at least two @ signs in the email field. I also check to see if there's a comma in the field. That's probably overkill, but it never hurts to be safe.

Spammers who try to hijack your form will usually try to put the victims' addresses in a BCC field to hide them. They would try to do this by setting the value of the from/email field to something like:

From/Email field = "webmaster@yourdomain.com\nbcc:victim1@dom.com,victim2@dom.com,victim3@dom.com\n"

Our script above should catch this from either the duplicate @ signs or the presence of the comma. But to be super-safe, we can also check for the presence of "bcc" or the line feed (\n) character:

if ($sender =~ /\@.*\@¦,¦bcc:¦\n/i) ...

[Again, the ¦ character is supposed to be a solid pipe, not the broken pipe that WebmasterWorld displays.]

Note the /i at the end of that regexp which makes the expression case insensitive. So it will match bcc or BCC (or Bcc or bCC, for that matter).

The final vulnerability is the <subject> field. Either hard-code the value into the script, or run the value through the regexp listed above. Otherwise spammers could tack "\n victim1@dom.com..." to the end of the Subject field.

All this prevents spammers from using your script to spam multiple victims at once. But spammers could still hijack your script and just send a message to just one victim, and then call the script again to send to the second victim, etc. How do you deal with that? It's a bit more complicated to defend against, but the good news is that you probably don't have to. Most spammers simply don't work that way. If you've hard-coded your To: address and printed that as the first thing in your sendmail block, you can be sure you're getting a message every time your form is used, so if you suddenly get hundreds of messages with spam content then you'll know your form has been compromised. Otherwise you're probably fine. If a spammer does call your form one address at a time, over and over, then dealing with that will need to be the subject of another article...

LisaWeber

2:12 am on Oct 25, 2005 (gmt 0)

10+ Year Member



Thanks for the article - if you get a chance, could you explain how to keep the spammers from sending me hundreds of emails a day through my own formmail script.

JollyK

2:44 am on Oct 25, 2005 (gmt 0)

10+ Year Member



That's awesome! What a great resource. I only have a few tiny nitpicky things that I would add. What I do with the "from" is first, not place it in the "from," but in the Reply-to:, since if you're using sendmail then passing different characters can mess it up. Pretty much no MTA parses the reply-to, but pretty much all mail clients support it, so it's very handy for suspicious email from a web form.

Also, before putting it into the Reply-To, I remove all whitespace characters to prevent someone entering something like this as their email address:


joe@badguy.com
Bcc: user@vic.com, user2@vic.com, user3@vic.com, etc...
Subject: Buy our FABULOUS designer watches!

You want this stuff now! Yeah, boy, yeah!

Spam goes here

You got that for the subject, but actually, you can do that in any header, since double-newline defines the end of headers and beginning of the message. Your regexp for the email address doesn't use "s" on the end, so it may not even see the commas and extra @ signs after the first newline. That has bitten me in the behind a few times, so I'd make this change:


if ($sender =~ /(\@.*\@)¦,/s) {

Mine actually does something like this:


$sender =~ s/[\"<>,'\s]//sg;

It removes all double and single quotes, angle brackets, and spaces (including newline and carriage return) from the email address, making it safer to put into the Reply-to.

I actually strip all those out and send it anyway since I'm not using the address in the From, though. :-)

Great job!

Oh, and LisaWeber: it really depends on what the spammers are doing. If there are a bunch of CC: lines in there, and you can see other email addresses in the spam, then what MichaelBlueJay has above may help cut down on it. Another thing I do is capture the remote IP of the sender, and if I start getting a bunch, I just put "deny from <ip address>" in the .htaccess file for the directory where the form mail script is.

You can capture the remote address and put it in the headers something like this (in Perl):

X-Sender-IP: $ENV{'REMOTE_ADDR'}

or in PHP:

X-Sender-IP: $_SERVER['REMOTE_ADDR']

Or even make it add the IP to the bottom of the email it sends.

Even easier, don't name your form mail script "formmail.cgi," "formmail.pl," mail.cgi, mail.pl, mailto.cgi, mailto.pl, or other easily-guessable names, because the spammers run scripts that just search for them. Name your email script something like "wsendm47.cgi" instead. It won't stop spammers from seeing it, but it will stop random skript kiddiez from finding it on a search for "formmail.cgi" or "FormMail.pl" etc. If you want to get fancy, you can then replace your old formmail.cgi (or whatever) with a script that does nothing but capture the person's IP address and block it in .htaccess:

#!/usr/bin/perl

open(FILE, ">>.htaccess");
# You can flock it if you have the flock function, but
# append should be atomic.
print FILE "deny from $ENV{'REMOTE_ADDR'}\n";
close(FILE);
print "Content-type: text/html\n\nGood bye!\n";

Most people don't want to quite go that far, though, in case they end up blocking half of AOL by mistake. :-)

I hate spammers so much, but I hate form mail spammers almost the worst.

JK

MichaelBluejay

12:14 am on Oct 26, 2005 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member Top Contributors Of The Month



Thanks for the tips, Jolly K. So here's a summary incorporating your improvements:

The problem: Spammers can hijack your homegrown formmail script by using automated programs (bots) to stuff a bcc: header into the To, From, or Subject fields. The message your script prepares will then look something like one of these:

To: webmaster@yourdomain.com
Bcc: victim1@domain.com, victim2@domain.com
From: fakesender@junkdomain.com
Subject: Buy vi*gra now!

To: webmaster@yourdomain.com
From: fakesender@junkdomain.com
Subject: Buy vi*gra now!
Bcc: victim1@domain.com, victim2@domain.com

Solutions:

(1) Hard-code as much of the header info as possible. That is, rather than get the To: and Subject: from fields in the <FORM>, hard-code those headers into your script.

VULNERABLE: print "To: $form{recipient}";
BETTER: print 'To: webmaster@yourdomain.com';

(2) Use a Reply-To: header instead of a From: header for the sender's email address. Few MTA's will allow a header to be tacked onto a Reply-To.

VULNERABLE: print "From: $form{sender}";
BETTER: print "Reply-To: $form{sender}";

(3) Run every header variable that's not hard-coded through a regular expression to make sure it hasn't been stuffed. Example:

if ($SendersEmail =~ /\@.*\@¦['"<>,;:\s]/s) {
print 'There was a problem submitting your form. Please email us at webmaster(at)yourdomain.com';
exit;
}

(Change the broken ¦ to a solid pipe.)

----
What's the regexp that rejects a string that contains any non-alphanumeric character but still allows these three: @-.
?
I know that \w matches alphanumerics plus the underscore, and \W matches non-alphanumerics plus the underscore, and that [] are character classes, but I don't know how to put it all together.

JollyK

10:48 pm on Oct 28, 2005 (gmt 0)

10+ Year Member



What's the regexp that rejects a string that contains any non-alphanumeric character but still allows these three: @-.

You can do this for a test:

if($string =~ /[^\w\@\-\.]/s){
print "BAD characters!";
}

or strip them like this:

$string =~ s/[^\w\@\-\.]//sg;

That allows any "word character" (which includes underscore), @, -, and ".".

When you go [^abcd], the ^ means "not these characters."

JK

[edit: woops, left off "/s" on the "if" regex thingy]