Safe Forms with Perl

The Problem

A user enters information into a web form, and submits it. The data is sent to the server. The server saves the information and prints a response. The user presses reload. The data is resent to the server. This information is then processed twice. Eeek! We don't want this to happen twice!

Form Example:

#!/usr/bin/perl -w
use CGI;
my $query = new CGI;
print $query->header();
print "<HTML><BODY>\n";
print "<FORM><INPUT NAME=\"Email\"></INPUT TYPE=\"submit\"></FORM>\n";
print "</BODY></HTML>\n";

Perl processing code:

#!/usr/bin/perl -w
use CGI;
my $query = new CGI;
my $email = $query->param('Email');
&doStuffWithEmail($email);
print $query->header();
print "<HTML><BODY>\n";
print "<P>Thank you</P>\n";
print "</BODY></HTML>\n";

Solutions

Solution One

The initial form sends the user to a script to process it. This script processes the form and prints a REDIRECT header to a third page, which does no processing at all.

Advantage: fast, easy

Disadvantage: user can go 'back'

#!/usr/bin/perl -w
use CGI;
my $query = new CGI;
my $email = $query->param('Email');
&doStuffWithEmail($email);
my $redirect = $query->param('Redirect');
print $query->redirect();

Solution Two

The inital form has a hidden variable which contains a unique identifier. This identifier is stored in a temporary table. When the script returns the data, the first thing it does is check to see if the identifier is in the temporary table, and if so, remove it from the table and process the rest of the form. If the identifier is not there, do not process the form data.

Advantage: user can go back over the page as much as they like.

Disadvantage: another table

#!/usr/bin/perl -w
use CGI;
use DBI;
my $query = new CGI;
my $dbname = "MNyDatabase";
my $dbh = DBI->connect("dbi:mysql:database=$dbname");

my $scriptID = $query->param('ScriptID');
my $sql = "SELECT * FROM ScriptID WHERE ScriptID = '$scriptID'";
my $sth = $dbh->prepare($sql);
my $numrows = $sth->execute;
$sth->finish;
if ($numrows == 1) {
	$sql = "DELETE FROM ScriptID WHERE ScriptID = '$scriptID'";
	$numrows = $dbh->do($sql);

	my $email = $query->param('Email');
	&doStuffWithEmail($email);
	}
$dbh->disconnect;
my $redirect = $query->param('Redirect');
print $query->redirect($redirect);

Solution Two(a)

The identifier is a combination of a start time, timeout and a unique symbol. The script can thus time-out, and the identifier be reused.

Advantage: forms left idle for a while cannot be later executed causing unexpected results, identifier reuse.

#!/usr/bin/perl -w
use CGI;
use DBI;
my $query = new CGI;
my $dbname = "MyDatabase";
my $dbh = DBI->connect("dbi:mysql:database=$dbname");

my $scriptID = $query->param('ScriptID');
my $sql = "SELECT * FROM ScriptID WHERE ScriptID = '$scriptID' AND Expires >= now()";
my $sth = $dbh->prepare($sql);
my $numrows = $sth->execute;
$sth->finish;
if ($numrows == 1) {
	$sql = "DELETE FROM ScriptID WHERE ScriptID = '$scriptID'";
	$numrows = $dbh->do($sql);

	my $email = $query->param('Email');
	&doStuffWithEmail($email);
	}
# While we are here, clear out the old expires...
$sql = "DELETE FROM ScriptID WHERE Expires < now()";
$numrows = $dbh->do($sql);
$dbh->disconnect;
my $redirect = $query->param('Redirect');
print $query->redirect($redirect);

Comments? Corrections? Suggestions? Email me!

Back to Ecommerce Home