Three Necessary Defenses for Open Credit Card Submission Forms
An open credit card submission form on the web is one that can be accessed and submitted with no prior steps: no lead-in pages, no registration, and so forth. Just call up the form page, enter the fields it needs, and hit submit. For most online merchants and services, the open credit card submission page makes no sense - it is a long way removed from the ideal setup for their customers or their business. You will, however, see it used on donation pages for non-profits, as a landing page for the call to action link in an email campaign, and so forth.
Put up an open credit card submission page at your peril, however. The lower echelons of the credit card fraud criminal community are rife with less skilled menials who are very interested in quick, easy, and potentially scriptable ways of testing stolen credit card numbers. Your open credit card submission page is exactly such a testing device, and without at least some defenses, you will find that a range of unsavory characters will submit fraudulent transactions - potentially a very large number of fraudulent transactions, and potentially enough to put you in hot water with your payment processor.
Fortunately, there are a few quick and easy ways to make your open credit card submission form unattractive to the criminal element.
1) Limit the number of different credit cards submitted by IP address
The fellow with a stash of stolen credit cards differs from a normal customer exactly by having a stash of many credit cards. Most small online businesses do not store credit card data locally, but that doesn't stop you from using salted hashes of credit card numbers to compare. If we're using MySQL and PHP, let's say you create the following table:
create table credit_card_md5 ( ip_address int(10) unsigned not null, card_md5 varchar(32) not null, created timestamp not null default current_timestamp, primary key (ip_address, card_md5) )
Every time your open credit card form is submitted, you (a) store a salted md5 hash of the card number into the table, and (b) check to see how many different cards have turned up recently from the given IP address. If there are too many attempts with different md5 hashes, then deny that user use of the form - for a small business, even as few as three different cards in a few week period of time is unlikely to be anything other than fraud.
$iplong = ip2long($_SERVER['REMOTE_ADDR']); $card_md5 = md5('my md5 salt' . $card_number); $sql = sprintf( "REPLACE INTO credit_card_md5 SET ip_address = '%d', card_md5 = '%s'" ,mysql_real_escape_string($iplong) ,mysql_real_escape_string($card_md5) ); mysql_query($sql); $sql = sprintf( "SELECT count(1) as count FROM credit_card_signatures" . " WHERE ip_address = '%d' and created > date_add(now(), interval -2 week)" ,mysql_real_escape_string($iplong) ); $result = mysql_query($sql); if( $result && $row = mysql_fetch_array($result) ) { if( $row['count'] > 2 ) { header('Location: /tooManyCards'); die(); } }
All that escaping of database input isn't strictly necessary here, but it's a good habit to get into. Better the ritual of sometimes unnecessary escaping than not enough escaping of database inputs one day.
2) Require a user to have viewed the form page
Given the choice, no low level criminal wants to do more work on their credit card testing script than is absolutely necessary. Their need is to be able to run through the test of hundreds or thousands of stolen card numbers quickly and with little effort. You can therefore raise the bar by requiring a randomly determined parameter to be passed with the form, and tracking this parameter in a session. Using PHP as a quick and dirty example, as the form renders you would do something like this:
<?php @session_start(); function getAntiSpamValue($key) { if( !isset($_SESSION['antispam']) ) { $_SESSION['antispam'] = array(); } list($usec, $sec) = explode(' ', microtime()); $seed = (float) $sec + ((float) $usec * 100000); srand($seed); $value = substr(md5(rand()),0,rand(8,14)); $_SESSION['antispam'][$key] = $value; return $value; } ?> <script type="text/javascript"> function set_spamfree_var(thisform) { thisform.spamfreevar.value = "<?php echo getAntiSpamValue('spamfreevar'); ?>"; } <script> <form id="credit_card_form" method="POST" onsubmit="set_spamfree_var(this)"> <input type="hidden" name="spamfreevar" value="" /> ...
It is being set in Javascript on form submission just to be awkward - there is no real advantage there beyond making it (a) less likely that an unskilled criminal has an out of the box script that works, and (b) easier to break a parser by completely changing the way in which the parameter is set.
In the code processing the submission, you would check the submitted value against the one stored in the session:
@session_start(); function isCorrectAntiSpamValue($key, $value) { return isset($_SESSION['antispam']) && $_SESSION['antispam'][$key] == $value; } if( !isset($_POST['spamfreevar']) || !isCorrectAntiSpamValue('spamfreevar', $_POST['spamfreevar']) ) { header("Location: /denied"); die(); }
Doing this changes the requirements of a stolen credit card submission script from one that just has to make POST requests to one that has to additionally view the form page, parse out a value, and manage a cookie to keep the session correct. The more work you make the criminals do, the less likely they are to abuse you.
3) Block the most dubious IP address ranges
Most credit card fraud comes from a small number of ISPs, and thus a small range of IP addresses. Based on the experience of supporting an open credit card submission form for a couple of years, here are the IP address ranges you want to block:
174.129.0.0 - 174.129.255.255 | Amazon EC2, which is at least as bad an origin of fraudulent credit card submissions as Nigerian ISPs. |
86.97.120.0 - 86.97.124.255 | Emirates Telecommunication Company in Dubai |
217.164.230.0 - 217.164.255.255 | Emirates Telecommunication Company in Dubai |
58.186.80.0 - 58.186.95.255 | FPT Broadband Service in Vietnam |
123.19.0.0 - 123.19.255.255 | Vietnam Posts and Telecommunications |
41.219.0.0 - 41.219.255.255 | Various African hotspots, including (and especially) Nigerian dial-up service. |
41.205.0.0 - 41.205.191.255 | Various African hotspots, including (and especially) Nigerian dial-up service. |
You're always going to see the odd attempt here and there from other ISPs, but the ranges above were the worst offenders by far in 2009 and 2010.
You could block these ranges in your firewall, but some small businesses aren't going to have the resident expertise or access to maintain firewall rules well. There is nothing stopping you from enacting a block in the form processing code. For example, create a table to hold the IP ranges you want to block:
create table ip_address_ranges ( id int(11) not null auto_increment, start int(10) unsigned not null default '0', end int(10) unsigned not null default '0', status varchar(20) not null default '', notes text not null, created datetime not null default current_timestamp, primary key (id) )
Then check against the table when the form is submitted or viewed:
$iplong = ip2long($_SERVER['REMOTE_ADDR']); $status = 'Banned'; $sql = sprintf( "SELECT count(1) as count FROM ip_address_ranges" . " WHERE start <= '%d' AND END >= '%d' AND status = '%s'" ,mysql_real_escape_string($IPlong) ,mysql_real_escape_string($IPlong) ,mysql_real_escape_string($status) ); $result = mysql_query($sql); if( $result && $row = mysql_fetch_array($result) ) { if( $row['count'] > 0 ) { header('Location: /bannedIPAddress'); die(); } }