views:

99

answers:

2

Hi, I use this class (taken from a blog tutorial) to generate unique keys to validate a form:

class formKey {
    //Here we store the generated form key
    private $formKey;

    //Here we store the old form key
    private $old_formKey;

    //The constructor stores the form key (if one excists) 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'];
        }
    }

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

        //We use mt_rand() instead of rand() because it is better for generating random numbers.
        //We use 'true' to get a longer string.
        $uniqid = uniqid(mt_rand(), true);

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

    //Function to output the form key
    public function outputKey() {
        //Generate the key and store it inside the class
        $this->formKey = $this->generateKey();
        //Store the form key in the session
        $_SESSION['form_key'] = $this->formKey;

        //Output the form key
        // echo "<input type='hidden' name='form_key' id='form_key' value='".$this->formKey."' />";
        return $this->formKey;
    }

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

Everything in my website goes trough index.php first, so I put this in index.php: $formKey = new formKey();

Then, in every form I put this: <?php $formKey->outputKey(); ?>

That generates this: <input type="hidden" name="form_key" id="form_key" value="7bd8496ea1518e1850c24cf2de8ded23" />

Then I can simply check for if(!isset($_POST['form_key']) || !$formKey->validate())

I have two problems. First: I cant use more than one form per page becouse only the last key generated will validate.

Second: Because everything goes trough index.php first, if I use ajax to validate the form, the first time will validate but the second time not, because index.php generates a new key but the pages containing the form does't refresh so the form key is not updated..

I have tried several things but I cant get it to work.. Maybe YOU can update/modify the code/class to get it to work?? Thanks!!!

+1  A: 

Not tested, but it should work.

class formKey {
    //Here we store the generated form key
    private $formKey;

    //Here we store the old form key
    private $old_formKey;

    //The constructor stores the form key (if one excists) 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'];
            $this->formKey = $this->generateKey();
            $_SESSION['form_key'] = $this->formKey;
        }
    }

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

        //We use mt_rand() instead of rand() because it is better for generating random numbers.
        //We use 'true' to get a longer string.
        $uniqid = uniqid(mt_rand(), true);

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

    //Function to output the form key
    public function outputKey() {
        return $this->formKey;
    }

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

Edit: changed back to single key. Just call outputkey() to when needed. Don't create more than one instance of this class.

Jonah Bron
Everything goes trough index.php and there is where I create a new instance of the class.. how can I do to cretate it only once?
Jonathan
@Jonah Bron Multiple keys are not required, as long as the values is unknown to the attacker then a request cannot be forged.
Rook
@The Rook: I read some of your posts and I see you know a lot about security, can you please post an answer with the updated code and tell me how to use it correctly?
Jonathan
@The Rook, Oh yeah, only one key is needed. Changed the code.@Jonathan, Just do `$my_key = new formKey();`, and whenever you want to output a hidden form with the hash value, insert `$my_key->outputKey()` into the value attribute.
Jonah Bron
Its still not working because I think I create a new instance of the class every time, because the instance is created in index.php and all the page requests, form posts and everything goes trough index.php first... How do I create the instance only once???
Jonathan
I mean only create it once on each page. At the beginning of the page, put an instance of it into a variable and call that variable -> outputKey() when necessary.
Jonah Bron
I have a form with a form_key field. When I post the form (using ajax) the key is verified, accepted, and a new one is generated. If I try to use the form again (same key because the page was not refreshed) then the key does not validate, because the page containing the form was not refreshed because I used ajax... This is the problem...
Jonathan
Ok, i posted my answer.
Rook
+1  A: 

You could put this into a class, but this is needless complexity. Simple security systems are best because they are easier to audit.

//Put this in a header file
session_start();
if(!$_SESSION['xsrf_token']){
     //Not the best but this should be enough entropy
     $_SESSION['xsrf_token']=uniqid(mt_rand(),true);
}    
//$_REQUEST is used because you might need it for a GET or POST request. 
function validate_xsrf(){
   return $_SESSOIN['xsrf_token']==$_REQUEST['xsrf_token'] && $_SESSOIN['xsrf_token'];
}
//End of header file. 

The extra && $_SESSOIN['xsrf_token'] makes sure this variable is populated. Its there to make sure the implementation fails securely. (Like if you forgot the header file doah! ;)

This following html/php goes in any file you want to protect from XSRF, make sure you have the code above in a header file.

if(validate_xsrf()){
   //do somthing with $_POST
}

This is all you need to print out the form, again make sure you call session_start(); before you do anything, it doesn't matter if you call it multiple times.

<input type="hidden" name="xsrf_token" id="form_key" value="<?=$_SESSOIN['xsrf_token']?>" />
Rook
Thank you! Why do you say that its not the best solution but it should be enough entropy?
Jonathan
There are some type errors: value="<?=$_SESSOIN['xsrf_token'?>" instead of: value="<?=$_SESSION['xsrf_token]'?>" and $_SESSOIN instead of $_SESSION..
Jonathan
But again, when I use ajax posted forms, the token changes and in the second try it doesn't validate... why its creating a new token if $_SESSION['xsrf_token'] is already set?
Jonathan
@Jonathan i didn't run this code, i just typed it from memory. $_SESSION is maintained though all requests for that session. It only needs to be set once. Variables that are not set, are also equal to false. The uniqid() calculation is going to be a very large number, its heavily dependent on time and there for is a weak cryptographic nonce, but it its large enough that brute forcing it in a csrf attack is unrealistic.
Rook
@Jonathan its also important to note that XSS can be used to bypass token based or referer based XSRF protection. Make sure you fix your xss!
Rook
@The Rock: I have set if(!$_SESSION['xsrf_token']){ $_SESSION['xsrf_token']=uniqid(mt_rand(),true); } in index.php but every time a form is posted a new token is generated I dont understand why!?
Jonathan
@Jonathan When are you calling `session_start()`? This must be done before you `print()` anything out, and should be done before using $_SESSION in **any** file.
Rook
@The Rock: Problem solved I was checking for: if(!isset($_POST['xsrf_token']) || !validate_xsrf()) { and it was not working so I changed it to if(!validate_xsrf()) { only and it resolved the problem...
Jonathan
@Jonathan Cool I'm glad that works.
Rook
@Jonathan, If this is the solution, please mark it as such :)
Jonah Bron
If this solution protects from xsrf attacks like the first class I posted then this is the solution..
Jonathan
@Jonathan yep, it should work well for you.
Rook