Introduction
In my previous article, I introduced a technique for dynamically naming a web form elements. The article can be found at Naming Form Elements Dynamically.
In this tip, I am going to provide a technique for preventing resending a web form by pressing the browser’s refresh button. The technique also prevents a web form from being resubmitted again and again by some kind of a bad visitor. Although the technique is standalone and does not depend on my previous technique, I am going to use my previous approach for further improving the system security.
The Idea
To prevent a form from being resubmitted again and again, I descended down to the database level. I added a database field to a table and marked that field as unique. When a form is requested for the first time, I provided a cookie that is holding a long random string
. The value of that string
will be inserted in that unique field. Then, if the form is resubmitted again, the database engine will refuse adding the redundant value! The table schema may look like this:
create table System_Users(
User_ID bigint auto_increment primary key,
User_Full_Name varchar(255),
User_EMail varchar(100),
User_Address varchar(100),
User_Phone varchar(100),
RefreshToken varchar(255) unique
);
For preventing the cookie value from being modified, I added another cookie which is holding a tokenized string of the original cookie value. For doing tokenization, I make a function which uses SHA1 and a fixed salt string. When the form is submitted, the original cookie value will be tokenized using the same function and compared with the value held in the tokenized cookie. If they are the same, the system will proceed saving the data; otherwise the system will refuse and closes the session. Further improving on the idea requires checking the existence of the cookie when the form is requested for the first time. The existence of the cookie means that the user is trying to register again in our system, which should not be allowed. The expiration of the cookies may be set to a relative short period (such as half an hour); so as not preventing another user who is using the same machine from being registered in our system.
Dynamically Naming Cookies and Form Elements
I applied my technique of dynamically naming cookies and form elements so as to achieve maximum security. If you have not gone through my previous article, you may refer to it from: Naming Form Elements Dynamically.
Using the Code
When a form is requested for the first time, we check if the cookie exists. If so, we inform the user and stop the process:
$isPostPack= (strtoupper($_SERVER['REQUEST_METHOD']) == 'POST');
$RefreshCookie='Quickfox';
$RefreshCookieHashed=HashedFieldName($RefreshCookie);
if(!$isPostPack){
if(isset($_COOKIE[$RefreshCookieHashed])){
$isError=true;
$errorMessage[$errorCounter++]="Please do not register again!";
}
}
If the cookie does not exist, we generate a random long string
, and assign that string
to the cookie. Then we tokenize that string
and assign it to another cookie:
if(!$isError){
$CookiePersistenceTime=1800;
$thecookietime=time() + $CookiePersistenceTime;
$$RefreshCookie=TokenizewithRandom();
setcookie($RefreshCookieHashed, $$RefreshCookie,$thecookietime ,'/');
$RefreshCookieTokener='Lazydog';
$RefreshCookieTokenerHashed=HashedFieldName($RefreshCookieTokener);
$$RefreshCookieTokener=Tokenize($$RefreshCookie);
setcookie($RefreshCookieTokenerHashed, $$RefreshCookieTokener,$thecookietime ,'/');
}
We see that the name of the cookies is meaningless, but, I save them in variables with meaningful names. We, also see that the name of each cookie is hashed, and the hashed version is sent to the browser.
For hashing cookie names, we used the function HashedFieldName($field)
:
function HashedFieldName($field){
$salt='hawom169';
$fullSalt=$_SERVER['HTTP_USER_AGENT'].getRealIpAddr().$salt.$field.$salt;
return 'A'. substr(sha1($fullSalt),0,20);
}
The TokenizewithRandom()
function that was used to make the random string
may look like this:
function TokenizewithRandom(){
$salt='tahajaafar';
$fullSalt=$_SERVER['HTTP_USER_AGENT'].getRealIpAddr().$salt.microtime();
return sha1($fullSalt).RandomStringGenerator(60);
}
The function Tokenize($txt)
is used to generate another string
that is a tokenization of the first string
, so that a hacker cannot play with our values. Tokenize($txt)
may look like this:
function Tokenize($txt){
$salt='abbasalygith';
$fullSalt=$salt.$_SERVER['HTTP_USER_AGENT'].getRealIpAddr().$txt.$salt.$salt;
return sha1($fullSalt);
}
When the form is submitted, we first check the existence of the cookies. If they do not exist, we inform the user, and stop the process:
if($isPostPack){
if(!isset($_COOKIE[$RefreshCookieHashed]) || !isset($_COOKIE[$RefreshCookieTokenerHashed])){
$isError=true;
$errorMessage[$errorCounter++]="You have spent too long time filling the form!";
}
}
If things went ok, we then compare the values of the cookies. We read the value of the original cookie, tokenize that value and compare it with the value stored in the tokenized one. If they are not the same, we stop the process because somebody played with our system:
if(!$isError){
$$RefreshCookie=$_COOKIE[$RefreshCookieHashed];
$$RefreshCookieTokener=$_COOKIE[$RefreshCookieTokenerHashed];
if(Tokenize($$RefreshCookie)!=$$RefreshCookieTokener){
$isError=true;
$errorMessage[$errorCounter++]="Do not play with our system!";
}
}
If the last check was passed, we then query the database to see if the value of the read string
is not previously stored there:
if(!$isError){
DatbaseConnect();
$sql="select User_ID from System_Users where RefreshToken=?";
$result =$PDOconn->prepare($sql);
$result->bindValue(1,$$RefreshCookie);
$result->execute();
$rows_found = $result->rowCount();
if($rows_found){
$isError=true;
$errorMessage[$errorCounter++]="Data is resent by refresh!";
CloseDatabaseConnection();
}
}
If all checks were passed, we then save that data to the database.
if(!$isError){
$$namefield=$_POST[$namefieldHashed];
$$mailfield=$_POST[$mailfieldHashed];
$$addressfield=$_POST[$addressfieldHashed];
$$phonefield=$_POST[$phonefieldHashed];
$sql="insert into System_Users
(User_Full_Name,User_EMail,User_Address,User_Phone,RefreshToken) values(?,?,?,?,?)";
$result =$PDOconn->prepare($sql);
$result->bindValue(1,$$namefield);
$result->bindValue(2,$$mailfield);
$result->bindValue(3,$$addressfield);
$result->bindValue(4,$$phonefield);
$result->bindValue(5,$$RefreshCookie);
try
{
$result->execute();
$successMessage="Thank you ".$$namefield." ,your registration was completed successfully!";
}
catch (PDOException $Exception)
{
$PDOisExecption=true;
$errorMessage[$errorCounter++]=$Exception->getMessage();
}
CloseDatabaseConnection();
}
Checking Form Filling Time
To ensure that the form is filled in a suitable period of time, I added one more checking step. I register the time when the form is originally requested and save that time value in a cookie. Again, I tokenized the value of that time and save it in another cookie:
$waitCookieTokener='JustBeSure';
$waitCookieTokenerHashed=HashedFieldName($waitCookieTokener);
$$waitCookie=time();
setcookie($waitCookieHashed, $$waitCookie,$thecookietime ,'/');
$$waitCookieTokener=Tokenize($$waitCookie);
setcookie($waitCookieTokenerHashed, $$waitCookieTokener,$thecookietime ,'/');
When the form is submitted, we first compare the values of the two cookies:
$$waitCookie=$_COOKIE[$waitCookieHashed];
$$waitCookieTokener=$_COOKIE[$waitCookieTokenerHashed];
if(Tokenize($$waitCookie)!=$$waitCookieTokener){
$isError=true;
$errorMessage[$errorCounter++]="Do not play with our system!";
}
If they are equal, then check the elapsed time passed since the form is requested:
if(!$isError){
$waittime=5;
$elapsedtime=time()-intval($$waitCookie);
if($elapsedtime<$waittime){
$isError=true;
$errorMessage[$errorCounter++]="Time is too short in filling the form!";
}
}
I improve the system friend-ness by adding a countdown counter that is worked at the client side using JavaScript. The counter starts counting down with the value stored in the $waittime
variable. The submit button is disabled till that counter reaches zero.
There is a message label that tells the user to wait for n seconds. (n=counter value).
Points of Interest
- The example is provided in PHP, it is obvious that it can be used by any other technology such as .NET or Java.
Update
- Update for correcting the logic of
Tokenize($txt)
.