Understanding and escaping the dreaded Security Component Blackhole

So, you have a Form that you built in your CakePHP view and messing around with it using some javascript, and all of a sudden….pooof….the form won’t post anymore as the Security Component blackholes it. What did you do? How/what happened? Where do you start? Do you just disable the security plugin? Do you have other options? Well you are in luck, as I just finished a rather lengthy debugging process of just this, and have written below how things work in a fair amount of detail. Hopefully this will save others some pain.

Alright, let’s get started right away.
You know that something in your form is blackholing, but don’t know what caused it – right? so let’s start from the beginning. How does the FormHelper and SecurityComponent work? how does it do it’s magic?
Well, you use the FormHelper to make your forms, and each time you insert a field, the FormHelper records it, and adds it to a ‘master list’, a $fields array in the form helper. Each type of field does something a little different, but basically, as long as the security component is included (in your app_controller or your controller), then by default, all fields that the FormHelper adds are “secured”.
Now, there are 2 different ways that the FormHelper secures form fields. On most fields, it secures just the fieldname, but on hidden form fields, it secures the fieldname and it’s value – this is called “locking” the field. This is important, as you will see in the example later. Securing just the fieldname means that all the form fieldnames must remain the same. None can be added, or removed, or renamed. All fieldnames must remain the same and can’t be changed. When you “lock” a form field. It’s name and value must remain the same.
Understanding that, let’s talk about how it secures things? how does it work? OK, we know that all form fields to be secured (either by locking it, or just securing the fieldname) are part of a master list in the FormHelper’s $fields property. When you call $form->end() in your view, it iterates through this array, and all form fields that should be secured – including the ones that should be “locked” are added to an array, and then a hash is generated from that array. That hash is stored in a hidden form element in that form (as [_Token][fields]). Now the [_Token][fields] contains the hash, and all the fields that were “locked” in an encoded format (rot13), which is serialized. The form also gets a [_Token][key] generated which is stored in the users session.
Now that the form is “tattooed” with the secure hash, it’s pretty easy for the SecurityComponent to verify things. When a form gets posted, the security component basically compares the key stored in the users [_Token][key] session variable with the form’s [_Token][key] to make sure it’s the same. If it is, then it simply takes all the forms current fieldnames, un-serialzes/un-encodes the rot13 “locked” fieldnames which indicate which fields were locked (which fieldnames had which values), puts it all in a new array, and takes a new hash of the entire thing. If the hash is stored in the original forms [_Token][fields]match, and the newly created hash match, then it id assumed that the form hasn’t been tampered with and things are good. If it doesn’t match – then the form has been tampered with, and the request is blackholed.
OK – so far it’s pretty straight forward – but the big thing for me is that, by default, all hidden form fields are locked.
Now, imagine in your form, you have a typical “edit” form, which you are loading data dynamically into with your javascript framework of choice. Now, you update the hidden “id” field because it needs to be updated to reflect the new dataset that is loaded into the form – so that if it’s posted it updates the correct row.
Well, in this case, with the above understanding we know that the form will be blackholed because a hidden form field (which is locked by default) has been changed. when the form gets posted, the security component will generate a new hash with the form data that it received from the post, and the new hash will not be the same as the old hash. crap, what do we do?
Well it turns out that you can disable security for hidden fields. Now before I tell you how, I must tell you that this isn’t really a good idea, and I have proposed a better way which I will get too below. How do you disable a hidden field from being secured? Easy:
$form->hidden('Model.fieldname', array('security'=>false);
Now, this still won’t work in our example? Why? Well, this disables that field entirely from the secure “hash”. It isn’t locked anymore, and it’s not secured/present in the FormHelpers $fields array that gets hashed. Maybe some of you are saying “AHA!”, and maybe still some of you don’t see it yet…well, what happens when the form is posted? What does the SecurityComponent see? it sees all posted fields and values, all of a sudden there is an extra field in there (the hidden field), and a different hash will be generated. Since there is now a hash mismatch, the request is blackholed. “AHHHHHHH” how do we get around that?
This is where the SecurityComponent’s disableFields property comes in. If you disable the security in hidden fields using the above method, you must also disable them in the SecurityComponent using the disabledFields property in your beforeFilter(). That way, when the form gets posted, the security component knows which fields to ignore when generating and comparing the hashes, and they will match…but is this good?
I have been reading around, and it seems that most people just disable the SecurityComponent when ajax comes into the picture. Now, I consider using the SecurityComponent’s disableFields property pretty much the same as effectively disabling the SecurityComponent – especailly in the above case.
Why? well, what happens in the beforeFilter? well, it is executed before each and every action for that controller. That means if we tell the Security component to disable the “id” field, all forms for that controller will have all their ID fields vulnerable for tampering. Is that a good thing? I would say the answer to that is a big no.
What really is needed is a way to control the hidden fields. a way to say “secure the hidden field, but don’t lock it”. This way the value of that field can be changed, but the fieldname itself cant be. So that fieldname must be present and accounted for in the post. Also, if we had some of this control, we wouldn’t need to mess with the SecurityComponent’s disableFields property (or at least not as much) in these specific circumstances. This means that for specific instances, we could allow the hidden fields to have their values changed and still keep full security for all other forms for that controller. I would say that this is a much more secure method of dealing with things.
Now, this still isn’t the best, as this indeed doesn’t protect someone from changing the value of the hidden property – but you should also have other checks in place to safeguard against this (does the current user have the proper permissions to make this change?). But at least if we had this option in the FormHelper, then at least we could make that choice.
It turns out that to add this type of choice isn’t too hard, but, for now, it does require you to override the default FormHelper.
If you copy cake’s FormHelper into your view->helpers in your app directory, then all that is needed is to make the hidden method look like this:
function hidden($fieldName, $options = array()) {
    $secure = true;
    $secureValue = true;

    if (isset($options['secure'])) {
        $secure = $options['secure'];
        unset($options['secure']);
    }
   
    if (isset($options['secureValue'])) {
        $secureValue = $options['secureValue'];
        unset($options['secureValue']);
    }
       
    $options = $this->_initInputField($fieldName, array_merge(
        $options, array('secure' => false)
    ));
    $model = $this->model();

    if ($fieldName !== '_method' && $model !== '_Token' && $secure && $secureValue) {
        $this->__secure(null, '' . $options['value']);
    } elseif ($fieldName !== '_method' && $model !== '_Token' && $secure && !$secureValue) {
        $this->__secure();
    }
       
    return $this->output(sprintf(
        $this->Html->tags['hidden'],
        $options['name'],
        $this->_parseAttributes($options, array('name', 'class'), '', ' ')
    ));
}
if you do this, then on hidden form elements, you can pass an options array that has the following possibilities:
$form->hidden('Model.fieldname, array("secure"=>true,"secureValue"=>false));
secure, and secureValue defaults to true, meaning that by default, hidden fields are locked. but if you change the secureValue to false, the hidden field will just be secured (meaning that the value is allowed to change, but the fieldname itself must be present and accounted for, but you should know that by now..right ).
I did open a ticket at the lighthouse, so maybe one day that change will be included in the FormHelper…but until then, this work around is needed (or you have to still mess with the SecurityComponent’s disableFields property – and remember to pass the ’secure’=>false option on hidden fields also, and “unlock” it for that entire controller).
Another option is to not use the form helpers hidden fields, and use the regular text input fields, then disable them/hide them with css/javascript. If you do it this way, then you do not need the FormHelper modification above, and as long as you don’t add/remove fields, your form will not get blackholed.
Hopefully now you have a better understanding of what types of changes will cause blackholes, and what options you have in dealing with them. If you rather have a patch for the above workaround, you can grab it from here