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.