9.6.1 Problem
9.6.2 Solution
Generate a unique identifier and store the token as a hidden
field in the form. Before processing the form, check to see if that token has
already been submitted. If it hasn't, you can proceed; if it has, you should
generate an error.
<?php $unique_id = uniqid(microtime(),1); ... ?> <input type="hidden" name="unique_id" value="<?php echo $unique_id; ?>"> </form>
Then, when processing, look for this ID:
$unique_id = $dbh->quote($_GET['unique_id']);
$sth = $dbh->query("SELECT * FROM database WHERE unique_id = $unique_id");
if ($sth->numRows( )) {
// already submitted, throw an error
} else {
// act upon the data
}
9.6.3 Discussion
For a variety of reasons, users often resubmit a form. Usually
it's a slip-of-the-mouse: double-clicking the Submit button. They may hit their
web browser's Back button to edit or recheck information, but then they re-hit
Submit instead of Forward. It can be intentional: they're trying to stuff the
ballot box for an online survey or sweepstakes. Our Solution prevents the
nonmalicious attack and can slow down the malicious user. It won't, however,
eliminate all fraudulent use: more complicated work is required for that.
The Solution does prevent your database from being cluttered
with too many copies of the same record. By generating a token that's placed in
the form, you can uniquely identify that specific instance of the form, even
when cookies is disabled. When you then save the form's data, you store the
token alongside it. That allows you to easily check if you've already seen this
form and record the database it belongs to.
Start by adding an extra column to your database table —
unique_id — to hold the identifier. When you insert data for a record,
add the ID also. For example:
$username = $dbh->quote($_GET['username']);
$unique_id = $dbh->quote($_GET['unique_id']);
$sth = $dbh->query("INSERT INTO members ( username, unique_id)
VALUES ($username, $unique_id)");
By associating the exact row in the database with the form, you
can more easily handle a resubmission. There's no correct answer here; it
depends on your situation. In some cases, you'll want to ignore the second
posting all together. In others, you'll want to check if the record has changed,
and, if so, present the user with a dialog box asking if they want to update the
record with the new information or keep the old data. Finally, to reflect the
second form submission, you could update the record silently, and the user never
learns of a problem.
All these possibilities should be considered given the
specifics of the interaction. Our opinion is there's no reason to allow the
deficits of HTTP to dictate the user experience. So, while the third choice,
silently updating the record, isn't what normally happens, in many ways this is
the most natural option. Applications we've developed with this method are more
user friendly; the other two methods confuse or frustrate most users.
It's tempting to avoid generating a random token and instead
use a number one greater then the number of records already in the database. The
token and the primary key will thus be the same, and you don't need to use an
extra column. There are (at least) two problems with this method. First, it
creates a race condition. What happens when a second person starts the form
before the first person has completed it? The second form will then have the
same token as the first, and conflicts will occur. This can be worked around by
creating a new blank record in the database when the form is requested, so the
second person will get a number one higher than the first. However, this can
lead to empty rows in the database if users opt not to complete the form.
The other reason not do this is because it makes it trivial to
edit another record in the database by manually adjusting the ID to a different
number. Depending on your security settings, a fake GET or POST submission
allows the data to be altered without difficulty. A long random token, however,
can't be guessed merely by moving to a different integer.