Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / web

Preventing Resending by Refresh and Reducing the Need of Captcha

4.00/5 (2 votes)
14 Mar 2015CPOL4 min read 9.1K   21  
Still asking your users not to press the Browser’s Refresh Button?

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:

SQL
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:

PHP
$isPostPack= (strtoupper($_SERVER['REQUEST_METHOD']) == 'POST');
$RefreshCookie='Quickfox';
$RefreshCookieHashed=HashedFieldName($RefreshCookie);
if(!$isPostPack){
/*
This Cookie should not exist when requesting the form for the first time.  
The existence of this cookie means that the user is trying refilling the form again. 
*/
if(isset($_COOKIE[$RefreshCookieHashed])){
$isError=true;
$errorMessage[$errorCounter++]="Please do not register again!";
}

}//!$isPostPack

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:

PHP
if(!$isError){
$CookiePersistenceTime=1800;//Half an hour

$thecookietime=time() + $CookiePersistenceTime;

$$RefreshCookie=TokenizewithRandom();
setcookie($RefreshCookieHashed, $$RefreshCookie,$thecookietime ,'/');

$RefreshCookieTokener='Lazydog';
$RefreshCookieTokenerHashed=HashedFieldName($RefreshCookieTokener);

$$RefreshCookieTokener=Tokenize($$RefreshCookie);
setcookie($RefreshCookieTokenerHashed, $$RefreshCookieTokener,$thecookietime ,'/');

}//!$isError

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):

PHP
function HashedFieldName($field){
$salt='hawom169';
$fullSalt=$_SERVER['HTTP_USER_AGENT'].getRealIpAddr().$salt.$field.$salt;
return 'A'. substr(sha1($fullSalt),0,20);
}//function HashedFieldName

The TokenizewithRandom() function that was used to make the random string may look like this:

PHP
function TokenizewithRandom(){
$salt='tahajaafar';
$fullSalt=$_SERVER['HTTP_USER_AGENT'].getRealIpAddr().$salt.microtime();
return sha1($fullSalt).RandomStringGenerator(60);
}//function TokenizewithRandom

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:

PHP
function Tokenize($txt){
$salt='abbasalygith';
$fullSalt=$salt.$_SERVER['HTTP_USER_AGENT'].getRealIpAddr().$txt.$salt.$salt;
return sha1($fullSalt);
}//function Tokenize

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:

PHP
if($isPostPack){

//If the cookies do not exist, either the user filled the form 
//in a long time or she has deleted the cookie

if(!isset($_COOKIE[$RefreshCookieHashed]) || !isset($_COOKIE[$RefreshCookieTokenerHashed])){
$isError=true;
$errorMessage[$errorCounter++]="You have spent too long time filling the form!";
}//!isset($_COOKIE[$RefreshCookieHashed]) || !isset($_COOKIE[$RefreshCookieTokenerHashed]

}//$isPostPack

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:

PHP
if(!$isError){

$$RefreshCookie=$_COOKIE[$RefreshCookieHashed];

$$RefreshCookieTokener=$_COOKIE[$RefreshCookieTokenerHashed];

//If the cookie value not equals the tokenized cookie value, means that the user played with our system!
if(Tokenize($$RefreshCookie)!=$$RefreshCookieTokener){
$isError=true;
$errorMessage[$errorCounter++]="Do not play with our system!";
}//Tokenize($$RefreshCookie)!=$$RefreshCookieTokener

}//!$isError

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:

PHP
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 there are rows, means the user resend by refresh
if($rows_found){
$isError=true;
$errorMessage[$errorCounter++]="Data is resent by refresh!";
CloseDatabaseConnection();
}

}//!$isError

If all checks were passed, we then save that data to the database.

PHP
//If everything went ok, it is time to save the user data into the database!
if(!$isError){

$$namefield=$_POST[$namefieldHashed];
$$mailfield=$_POST[$mailfieldHashed];
$$addressfield=$_POST[$addressfieldHashed];
$$phonefield=$_POST[$phonefieldHashed];

// You may make more checking to the data

$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!";
}//try
catch (PDOException $Exception) 
 { 
	  $PDOisExecption=true;
	 $errorMessage[$errorCounter++]=$Exception->getMessage();    
 }//catch

CloseDatabaseConnection();
}//!$isError

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:

PHP
$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:

PHP
$$waitCookie=$_COOKIE[$waitCookieHashed];
$$waitCookieTokener=$_COOKIE[$waitCookieTokenerHashed];
if(Tokenize($$waitCookie)!=$$waitCookieTokener){
$isError=true;
$errorMessage[$errorCounter++]="Do not play with our system!";
}//Tokenize($$waitCookie)!=$$waitCookieTokener

If they are equal, then check the elapsed time passed since the form is requested:

PHP
if(!$isError){
$waittime=5; //Wait Time in seconds before submitting the form
 $elapsedtime=time()-intval($$waitCookie);
   if($elapsedtime<$waittime){
		      $isError=true;
	           $errorMessage[$errorCounter++]="Time is too short in filling the form!";
	}//$elapsedtime<$waittime
		
}//!$isError

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)