Book HomePHP CookbookSearch this book

9.6. Guarding Against Multiple Submission of the Same Form

9.6.1. Problem

You want to prevent people from submitting the same form multiple times.

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.

When creating the form, use uniqid( ) to get a unique identifier:

<?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.

9.6.4. See Also

Recipe 14.4 for more details on verifying data with hashes; documentation on uniqid( ) at http://www.php.net/uniqid.



Library Navigation Links

Copyright © 2003 O'Reilly & Associates. All rights reserved.