Software and Other Mysteries

On code and productivity with a dash of unicorn dust.

Form Protection Revisited

About a month ago I wrote a post on how to protect your forms from double posting and CSRF attacks using nonce words in CodeIgniter. I realized pretty soon though that the code I posted wasn’t as smooth or, in fact, as functional as it should be. So to save my own ass I though I should share this update with you guys.

I would like to put this in a less shameful way, but the fact is the library didn’t do what it was supposed to do. On every page refresh a new nonce was created, which indeed hindered an attempt to simply press the back button and resubmit. Problem was that if you submitted yet another time after the error message about the invalid nonce (well, you probably should word it differently to the end user) was displayed, the nonce passed validation.

That was the big problem which has been solved, but there’s more! First of all, the nonce now also contains a random element, further lessening the chance of an attacker being able to brute force the nonce word even though a successful attempt would be highly unlikely even before this addition.

Secondly, I’ve made some convenient changes to how to use this extension. The hidden field is now automatically output when you use the form_open function, and if you’d like to do it manually you should be able to figure it out yourself. Also the run method has been extended to mark a nonce as used after successful validation, so you don’t have to do it manually in the controller.

So this is the code you’ll need:

MY_form_helper.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
<?php
/*
 * Helper (helpers/MY_form_helper.php)
 */
function form_open($action = '', $attributes = '', $hidden = array()) {
    $CI = & get_instance();

    if ($attributes == '') {
        $attributes = 'method="post"';
    }

    $action = ( strpos($action, '://') === FALSE) ? $CI->config->site_url($action) : $action;

    $form = '<form action="' . $action . '"';
    $form .= _attributes_to_string($attributes, TRUE);
    $form .= '>';

    if($CI->form_validation->has_nonce()) {
        $value = set_value('nonce');
        if($value == '')
            $value = $CI->form_validation->create_nonce();
        $hidden['nonce'] = set_value('nonce', $value);
    }

    if (is_array($hidden) && count($hidden) > 0) {
        $form .= form_hidden($hidden);
    }

    return $form;
}

/*
 * Library (libraries/MY_Form_validation.php)
*/
class MY_Form_validation extends CI_Form_Validation {
    private $use_nonce = false;

    function __construct() {
        parent::__construct();
    }

    /**
     * Create a new unique nonce, save it to the current session and return it.
     */
    public function create_nonce() {
        $nonce = md5(rand() . $this->CI->input->ip_address() . microtime());
        $this->CI->session->set_userdata('nonce', $nonce);
        return $nonce;
    }

    public function has_nonce() {
        return $this->use_nonce;
    }

    public function run($group = '') {
        $result = parent::run($group);
        if($result === true) {
            $this->save_nonce();
        }
        return $result;
    }

    /**
     * Mark the nonce sent from the form as already used.
     */
    private function save_nonce() {
        $this->CI->session->set_userdata('old_nonce', $this->set_value('nonce'));
    }

    /**
     * Set form validation rules for the nonce.
     */
    function nonce() {
        $this->use_nonce = true;
        $this->set_rules('nonce', 'Nonce', 'required|valid_nonce');
    }

    /**
     * Make sure the nonce is valid.
     */
    function valid_nonce($str) {
        return ($str == $this->CI->session->userdata('nonce') &&
                $str != $this->CI->session->userdata('old_nonce'));
    }
}

All you have to do now is open the form with form_open in your view, load the library in the controller and call the nonce method. That should do it! A remaining potential problem is that this will not work with more than one form at a time, though I’ll have to leave that as homework for you guys. Comments are open!

Comments