Tek-Tips is the largest IT community on the Internet today!

Members share and learn making Tek-Tips Forums the best source of peer-reviewed technical information on the Internet!

  • Congratulations Mike Lewis on being selected by the Tek-Tips community for having the most helpful posts in the forums last week. Way to Go!

nonce - unique single use number for for xss problem 1

Status
Not open for further replies.

jasc2k

Programmer
Nov 2, 2005
113
GB
hi all, back again I love this forum, lol

below is a simple php class that generates a unique number thats included on all of my forms to prevent xss. admitadly the class was not originally mine just improved and customized a little.

anyways the problem is simple - in my __construct function the old form key is retreived from a global $_SESSION var and works great until a user opens a new tab with my page open, because I refresh the key on each page load the SESSION var is now wrong and their original tab open with my page will fail the xss and any form submission will error.

I am stumped at the moment of how else I should store this 'old formkey' that wont effect extra tabs.

as always ideas are appreciated, thanks


Code:
<?php
/**
 * Formkey.php
 * 
 * This class is intended to protect forms from xss
 *
 * Customized by: Bladeone_2k - August 2010
 */
class formKey
{
	private $formKey; //Here we store the generated form key
	private $old_formKey; //Here we store the old form key (more info at step 4)

	//The constructor stores the form key (if one exists) in our class variable
	function __construct(){
		//We need the previous key so we store it
		if(isset($_SESSION['form_key'])){
			$this->old_formKey = $_SESSION['form_key'];
		}
		//Generate the key and store it inside the class (one per page)
		$this->formKey = $this->generateKey();
	}

	//Function to generate the form key
	private function generateKey(){
		//Get the IP-address of the user
		$ip = $_SERVER['REMOTE_ADDR'];

		//I use mt_rand() instead of rand() because it is better for generating random numbers.
		//I use 'true' to get a longer string.
		//See [URL unfurl="true"]http://www.php.net/mt_rand[/URL] for a precise description of the function and more examples.
		$uniqid = uniqid(mt_rand(), true);

		//Return the hash
		return md5($ip . $uniqid);
	}

	//Function to output the form key
	public function outputKey($html=0){
		//Store the form key in the session
		$_SESSION['form_key'] = $this->formKey;
		//Output the form key      
		if($html == 0){
			echo "<input type='hidden' name='form_key' id='form_key' value='".$this->formKey."' />";
		} else {
			return $this->formKey;
		}
	}

	//Function that validated the form key POST data
	public function validate(){
	//echo $_POST['form_key'];
	//echo $this->old_formKey; <----- I dont think it knows this?
		//We use the old formKey and not the new generated version
		if(isset($_POST['form_key'])){
			if($_POST['form_key'] == $this->old_formKey){
				//The key is valid, return true.
				return true;
			}
		} else {
			//The key is invalid, return false.
			return false;
		}
	}
}
?>

- free mp3 downloads and streaming
 
there are a bunch of different approaches, all of which qualify the nonce somehow. typically by a type parameter.

the alternative is to allow a bunch of recent nonces all to be valid. say any nonce created within the last [X] minutes.

if you do not want to specify a type parameter, then a randomly generated equivalent is acceptable.

my class will accept either a specified or random (anonymous) type parameter. using an anonymous parameter is less secure.

using the IP address as part of the hash is only useful (I think) if you are trying to prevent against man-in-the-middle attacks. i'm not convinced that even then it will be effective but have not given it much thought).

if you want a full blown nonce implementation extract the relevant code from wordpress core. My original class (which looks rather similar to your code) has been updated and now looks like this

Code:
<?php
class nonce{
	
	public $error = false;
	public $errorMessages = array();
	private $req = array();
	
	public function __construct(){
		if(session_id() == '') session_start();
	}
 
	public function getNonce($type=null){
		$type = is_null($type) ? $this->encode(uniqid('nonce_key_', true)) : $type;
		if (isset($_SESSION['nonce'][$type])){
			//
		} else {
			$_SESSION['nonce'][$type] = $this->encode (uniqid('nonce_', true));
		}
		return array('key'=>$type, 'value'=>$_SESSION['nonce'][$type]);
	}
	
	
	public function checkNonce($type=NULL){
		if(is_null($type)):
			return $this->checkAnon();
		endif;
		if (empty($_SESSION['nonce'][$type])){
			$this->setError('Session nonce not set.  Normally this occurs because the session has expired, the nonce has expired or this is a hack attempt');
			return false;
		}
		$this->getRequest();
		if (empty($this->req[$type])){
			$this->setError('Request nonce not set.  Normally this occurs because this is a hack attempt or someone has attempted to access a protected page directly');
			return false;
		}
		if ($_SESSION['nonce'][$type] == $this->req['nonce'][$type]){
			unset ($_SESSION['nonce'][$type]);
			return true;
		} else {
			$this->setError("Incorrect nonce.  Normally this occurs due to a back button or refresh event, or due to a hack attempt");
			return false;
		}
	}
	
	public function checkAnon(){
		if(empty($_SESSION['nonce']) || !is_array($_SESSION['nonce'])):
			$this->setError('No session nonces are set');
			return false;
		endif;
		if(empty($this->req['nonce'])):
			$this->setError('No request nonces are set');
		endif;
		foreach($this->req['nonce'] as $key=>$nonce ):
			if(isset($_SESSION['nonce'][$key]) && $_SESSION['nonce'][$key] == $nonce):
				unset($_SESSION['nonce'][$key]);
				return true;
			endif;
		endforeach;
		$this->setError('No matching nonces.  Normally this occurs due to a back button or refresh event');
		return false;
	}
	public function setError($message){
		$this->error= true;
		$this->errorMessages[] = $message;
	}
 
	public function insertNonceField($type=NULL){
		$array = getNonce($type);
	
		return <<<HTML
	<input type="hidden" name="nonce[{$array['key']}" value="{$array['value']}" />
HTML;
	}
	
	public function getNonceURL($type=NULL, $address=NULL, $params=array()){
		$array = getNonce($type);
		if(count($params) == 0):
			if(is_null($address)):
				return http_build_query(array($array['key'] => $array['value'])   );		
			else:
				return $address . '?' . http_build_query(array($array['key'] => $array['value'])   );
			endif;
		else:
			//unset nonce in the get
			if(isset($params['nonce'])) unset($params['nonce']);
			if(is_null($address)):	
				return http_build_query(array_merge($params, array($array['key'] => $array['value'])));		
			else:
				return $address . '?' .  http_build_query(array_merge($params, array($array['key'] => $array['value'])));
			endif;
		endif;
		
	}
	
	private function getRequest(){
		if(count($this->req) == 0):
			$g = isset($_GET['nonce']) && is_array($_GET['nonce']) ? $_GET['nonce'] : array();
			$p = isset($_POST['nonce']) && is_array($_POST['nonce']) ? $_POST['nonce'] : array();
			$this->req = array_merge($g, $p);
		endif;
	}
	private function encode($string){
		return sha1($string); //change if you want to use md5
	}
 
 
}

//sample usage
$nonce = new nonce;
if ($nonce->checkNonce()):
	//proceed
else:
	//ignore
endif;
?>

<!-- sample forms -->
<form method="post" action="#">
	<input type="text" name="myField" />
	<?php $nonce->insertNonceField(); ?>
	<input type="submit" name="submit" value="submit" />
</form>

<!-- sample url -->
<a href="<?php echo $nonce->getNonceURL(NULL, $_SERVER['PHP_SELF'], $_GET); ?>" >My Link </a>

the way that I used to do things was never to supply a new nonce where an existing one had not been used. this makes a lot of sense if, for example, you are editing a user record. so if you open the same user record on three tabs, only the first to submit will work. this improves data integrity. but it does not work so well when you might have ten nonces on a page for even idempotent actions.

thus the best way of all to use the new class is to mix specified types for known non-idempotent actions and use anonymous nonces otherwise.
 
wow as always many thanks for your reply
that is an awsome example of a nonce class and usage and even does nonce via URL ace!!

but it does not work so well when you might have ten nonces on a page for even idempotent actions.

yes I have this issue I have several forms on each page i.e a comment form and a search form will this code work in this scenario, and across multiple tabs? lol
also Im not quite sure I understand what the 'type' is for and what value I can populate that with?
I am hoping to get to this on friday night cause I really want this multi tab problem to go away :)

Thanks,
James

- free mp3 downloads and streaming
 
'type' is just a discriminator. it can be useful for narrowing down the right to do something not just to the type of action but to the object on which the action is to be performed

try this code for example (it also contains some debugging of the nonce class). note that user 10 is deliberately spoofed with the wrong nonce type and will not delete.

Code:
<?php
class nonce{
    
    public $error = false;
    public $errorMessages = array();
    private $req = array();
    
    public function __construct(){
        if(session_id() == '') session_start();
    }
 
    public function getNonce($type=null){
        $type = is_null($type) ? $this->encode(uniqid('nonce_key_', true)) : $type;
        if (isset($_SESSION['nonce'][$type])){
            //
        } else {
            $_SESSION['nonce'][$type] = $this->encode (uniqid('nonce_', true));
        }
        return array('key'=>$type, 'value'=>$_SESSION['nonce'][$type]);
    }
    
    
    public function checkNonce($type=NULL){
    	 if(is_null($type)):
            return $this->checkAnon();
        endif;
        if (empty($_SESSION['nonce'][$type])){
            $this->setError('Session nonce not set.  Normally this occurs because the session has expired, the nonce has expired or this is a hack attempt');
            return false;
        }
        $this->getRequest();
		
        if (empty($this->req[$type])){
            $this->setError('Request nonce not set.  Normally this occurs because this is a hack attempt or someone has attempted to access a protected page directly');
            return false;
        }
        if ($_SESSION['nonce'][$type] == $this->req[$type]){
            unset ($_SESSION['nonce'][$type]);
            return true;
        } else {
            $this->setError("Incorrect nonce.  Normally this occurs due to a back button or refresh event, or due to a hack attempt");
            return false;
        }
    }
    
    public function checkAnon(){
        if(empty($_SESSION['nonce']) || !is_array($_SESSION['nonce'])):
            $this->setError('No session nonces are set');
            return false;
        endif;
        if(empty($this->req['nonce'])):
            $this->setError('No request nonces are set');
        endif;
        foreach($this->req['nonce'] as $key=>$nonce ):
            if(isset($_SESSION['nonce'][$key]) && $_SESSION['nonce'][$key] == $nonce):
                unset($_SESSION['nonce'][$key]);
                return true;
            endif;
        endforeach;
        $this->setError('No matching nonces.  Normally this occurs due to a back button or refresh event');
        return false;
    }
    public function setError($message){
        $this->error= true;
        $this->errorMessages[] = $message;
    }
 
    public function insertNonceField($type=NULL){
        $array = $this->getNonce($type);
    
        return <<<HTML
    <input type="hidden" name="nonce[{$array['key']}" value="{$array['value']}" />
HTML;
    }
    
    public function getNonceURL($type=NULL, $address=NULL, $params=array()){
        $array = $this->getNonce($type);
        if(count($params) == 0):
            if(is_null($address)):
                return http_build_query(array("nonce[$array[key]]" => $array['value'])   );        
            else:
                return $address . '?' . http_build_query(array("nonce[$array[key]]" => $array['value'])   );
            endif;
        else:
            //unset nonce in the get
            if(isset($params['nonce'])) unset($params['nonce']);
            if(is_null($address)):    
                return http_build_query(array_merge($params, array("nonce[$array[key]]" => $array['value'])));        
            else:
                return $address . '?' .  http_build_query(array_merge($params, array("nonce[$array[key]]"=> $array['value'])));
            endif;
        endif;
        
    }
    
    private function getRequest(){
        if(count($this->req) == 0):
            $g = isset($_GET['nonce']) && is_array($_GET['nonce']) ? $_GET['nonce'] : array();
            $p = isset($_POST['nonce']) && is_array($_POST['nonce']) ? $_POST['nonce'] : array();
            $this->req = array_merge($g, $p);
        endif;
    }
    private function encode($string){
        return sha1($string); //change if you want to use md5
    }
 
 
}

//sample usage
$nonce = new nonce;

?>

<?php
$action = isset($_GET['action']) ? $_GET['action'] : '';
switch ($action):
	case 'deleteUser':
		if(!isset($_GET['userID'])):
			$error[] = 'No user ID specified';
		endif;
		if (!$nonce->checkNonce('delete user ' . $_GET['userID'])):
			$error[] = implode ('<br/>', $nonce->errorMessages);
		endif;
		
		if (!isset($error)): 
			//delete user
			//assuming all correct
			if (isset($_GET['ajax']) && $_GET['ajax'] == 1):
				$return = array('result'=>'ok', 'userID'=>$_GET['userID']);
				echo json_encode($return);
				exit;
			endif;
		else:
			if (isset($_GET['ajax']) && $_GET['ajax'] == 1):
				$return = array('result'=>false, 'error'=>implode('<br/>', $error));
				echo json_encode($return);
				exit;
			endif;
		endif;
		break;
endswitch;

function getAllUsers(){
	for($i=1; $i<=10; $i++):
		$u = new stdClass;
		$u->userID = $i;
		$u->name = 'User ' . $i;
		$users[] = $u;
	endfor;
	return $users;
}
?>
<?php 
$users = getAllUsers();
?>
<table style="border-collapse:collapse;">
    <tr>
        <th>
            User Name
        </th>
        <th colspan="2">
            Action
        </th>
    </tr>
    <?php foreach ($users as $user): ?>
    <tr userid="<?php echo $user->userID;?>">
        <td>
<?php echo $user->name; ?>
        </td>
        <td>
            <?php if($user->userID != 10): ?>
			<a class="deleteUserAction" href="<?php echo $nonce->getNonceURL(	'delete user '. $user->userID, $_SERVER['PHP_SELF'], array(	'userID' => $user->userID, 'action'=>'deleteUser','ajax'=>1));?>">Delete</a>
        	<?php else: ?>
			<a class="deleteUserAction" href="<?php echo $nonce->getNonceURL(	'delete user 1', $_SERVER['PHP_SELF'], array(	'userID' => $user->userID, 'action'=>'deleteUser','ajax'=>1));?>">Delete</a>
			<?php endif;?>
		</td>
    </tr>
    <?php endforeach; ?>
</table>
<script type="text/javascript" src="[URL unfurl="true"]https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>[/URL]
<script type="text/javascript">
    	jQuery('document').ready(function(){
    		jQuery('a.deleteUserAction').bind('click', function(e){
    			e.preventDefault(); 
    			jQuery.getJSON(jQuery(this).attr('href'), function (data){
    				if(data.result == 'ok'){
						alert('user deleted.\ntry clicking again to see what happens');
    					var elem = jQuery('tr[userid="' + data.userID + '"]' );
						elem.css('backgroundColor','red');
										
    				} else {
						alert (data.error);
					}
    			});
    		});
    	});
</script>
 
apologies for the delay, what a week this has been, looking forward to the weekend.
thanks very much for the demo code made an interesting read.
I am nearly finished implementing the new class just trying to test all my different form scenarios.
lol mission, but so far looking good!

many thanks

- free mp3 downloads and streaming
 
Status
Not open for further replies.

Part and Inventory Search

Sponsor

Back
Top