Skip to content

Uploadify Upload Class CSRF Tokens Session data The right way .

anshul edited this page Aug 17, 2012 · 6 revisions

Recently i had some troubles with the uploadify script and security .So i wrote , what i believe that is a better way to work with Uploadify in CI .

STEP 1. I extended the Upload Class as follows :

<?php  if (!defined('BASEPATH')) exit('No direct script access allowed');


class MY_Upload extends CI_Upload{

    private $ci;
    public $ignore_mime ;
    
    public function __construct()
    {
        parent::CI_Upload();
        $this->ci =& get_instance();
    }


    /**
     * Verify that the filetype is allowed
     * 
     * @access    public
     * @return    bool
         */    
    function is_allowed_filetype($ignore_mime = FALSE)
    {
        if (count($this->allowed_types) == 0 OR ! is_array($this->allowed_types))
        {
            $this->set_error('upload_no_file_types');
            return FALSE;
        }
        
        $ext = strtolower(ltrim($this->file_ext, '.'));
        
        if ( ! in_array($ext, $this->allowed_types))
        {
            return FALSE;
        }

        // Images get some additional checks
        $image_types = array('gif', 'jpg', 'jpeg', 'png', 'jpe');

        if (in_array($ext, $image_types))
        {
            if (getimagesize($this->file_temp) === FALSE)
            {
                return FALSE;
            }            
        }

        if ($this->ignore_mime === TRUE)
        {
            return TRUE;
        }
        
        $mime = $this->mimes_types($ext);
                
        if (is_array($mime))
        {
            if (in_array($this->file_type, $mime, TRUE))
            {
                return TRUE;
            }            
        }
        elseif ($mime == $this->file_type)
        {
                return TRUE;
        }
        
        return FALSE;
    }  
}

What the above method does, is just that allows me to skip the mime type checking after the file is uploaded. I made this change in order to avoid changing the mime.php config file, because i really believe is stupid to add application/octet-stream for every file you upload(doing like this is not a check anymore).

STEP 2. I created another library to validate the mime type, after the file is uploaded, what this library does, is actually what Upload class would do in normal circumstances and a bit more, you'll see.

<?php  if (!defined('BASEPATH')) exit('No direct script access allowed');

class Uploadify{

    private $ci;
    private $_tmp_path;
    private $_field_name        = 'Filedata';
    private $_allowed_types     = 'gif|png|jpg|jpeg';
    private $_use_upload_token  = TRUE;
    private $_max_size          = 0;
    private $_max_width         = 0;
    private $_max_height        = 0;
    private $_encrypt_name      = TRUE ;
    private $_only_logged_in    = TRUE ;
    private $_only_admin        = TRUE ;
    private $errors             = array(); 
    
    public function __construct($config = array())
    {
        $this->ci =& get_instance();
        
        if( ! empty($config))
        {
            $this->initialize($config);
        }
        
        if(empty($this->_tmp_path))
        {
            $this->set('tmp_path',FCPATH.'tmp/');
        }    

        log_message('debug','Uploadify Class Initialized');
        
        $this->_set_error_messages();
    }
    
    public function initialize($config)
    {
        if(is_array($config) && count($config) > 0)
        {
            foreach($config AS $key=>$value)
            {
                $this->set($key,$value);
            }    
        }
        return $this;
    }
       
    public function set($key,$value='')
    {
        if(is_array($key))
        {
            foreach($key AS $k=>$v)
            {
                $this->set($k,$v);
            }
        }
        else
        {
            $this->{'_'.$key} = $value ;
        }
        return $this;
    }
    
    public function get($key)
    {
        return $this->{'_'.$key};
    }

    /**
    * This is the method used for the most of the uploads, 
    * If something special is needed, a new method will be created .
    **/
    public function do_upload()
    {
        $config                     = array();
        $config['upload_path']      = $this->_tmp_path ; 
        $config['allowed_types']    = $this->_allowed_types ;
        $config['max_size']         = $this->_max_size;
        $config['max_width']        = $this->_max_width;
        $config['max_height']       = $this->_max_height;
        $config['encrypt_name']     = $this->_encrypt_name ;
        
        $this->ci->load->library('upload');
        $this->ci->upload->initialize($config);
        
        $this->ci->upload->ignore_mime = TRUE ;//skip mime check

        if ( ! $this->ci->upload->do_upload($this->_field_name))
        {
            return $this->ci->upload->display_errors();
        }

        $data = $this->ci->upload->data();
        
        $ext = strtolower(ltrim($data['file_ext'], '.'));

        $data['is_image'] = FALSE ;

        if($info = getimagesize($data['full_path']))
        {
            $data['file_type']      = $info['mime'];
            $data['image_width']    = $info[0];
            $data['image_height']   = $info[1];
            $data['image_size_str'] = $info[3];
            $data['is_image']       = TRUE ;
        }

        if( ! $mimes = $this->ci->upload->mimes_types($ext) )
        {
            @unlink($data['full_path']);
            return $this->set_error('invalid_mime_type');
        }
        
        if( ! empty($mimes[$ext]) && ! is_array($mimes[$ext]) && $data['file_type'] != $mimes[$ext])
        {
            @unlink($data['full_path']);
            return $this->set_error('invalid_mime_type');
        }
        elseif( ! empty($mimes[$ext]) && is_array($mimes[$ext]) && ! in_array($data['file_type'],$mimes[$ext]))
        {
            @unlink($data['full_path']);
            return $this->set_error('invalid_mime_type');
        }
        
        /**
        * THIS IS THE WAY THE DATA IS ENCRYPTED,USE THIS LOGIC TO DECRYPT.
        * $userdata = json_encode($this->session->userdata);
        * $userdata = $this->encrypt->encode($userdata);
        * $userdata = base64_encode($userdata);
        **/
        if( ! $userdata = $this->ci->input->post('userdata',TRUE) )
        {
            @unlink($data['full_path']);
            return $this->set_error('invalid_userdata');
        }
        $userdata = base64_decode($userdata);
        $userdata = $this->ci->encrypt->decode($userdata);
        $userdata = json_decode($userdata);//userdata is an object...
        
        if($userdata == NULL || ! is_object($userdata))
        {
            @unlink($data['full_path']);
            if(function_exists('json_last_error'))
            {
                switch(json_last_error())
                {
                    case JSON_ERROR_DEPTH:
                        $error = $this->set_error('json_error_depth');
                    break;
                    case JSON_ERROR_CTRL_CHAR:
                        $error = $this->set_error('json_error_ctrl_char');
                    break;
                    case JSON_ERROR_SYNTAX:
                        $error = $this->set_error('json_error_syntax');
                    break;
                    case JSON_ERROR_NONE:
                        $error = $this->set_error('json_error_none');
                    break;
                }
                return $error ;                
            }
            else
            {
                return $this->set_error('json_error_syntax');
            }
        }
        //We have a valid $userdata object now. do extra checks.
        //We need to check for a token ? 
        if($this->_use_upload_token)
        {
            $session_token = $userdata->token ;
            $post_token    = $this->ci->input->post('token',TRUE);
            if($session_token != $post_token)
            {
                @unlink($data['full_path']);
                return $this->set_error('invalid_token');
            }
        }
        //So if we need to check the token, the data has pass the filter.
        //The user needs to be logged in to upload, right ?
        // 0 = FALSE = EMPTY.
        if($this->_only_logged_in && empty($userdata->logged_in))
        {
            @unlink($data['full_path']);
            return $this->set_error('not_logged_in');
        }
        if($this->_only_admin && empty($userdata->is_admin))
        {
            @unlink($data['full_path']);
            return $this->set_error('only_admin');
        }
        
        return (array)$data ;
    }
    
    
    /**
    * This method will initialize some messages that can be used in case an error occurs .
    **/
    private function _set_error_messages()
    {
        $errors = array(
            'invalid_file_type' =>  'Invalid file type ',
            'invalid_mime_type' =>  'Invalid mime type ',
            'invalid_token'     =>  'Invalid security token.Please try again',
            'invalid_userdata'  =>  'The required userdata is missing.',
            'json_error_depth'  =>  'Maximum stack depth exceeded',
            'json_error_ctrl_char'  =>  'Unexpected control character found',
            'json_error_syntax' =>  'Syntax error, malformed JSON',
            'json_error_none'   =>  'No errors',
            'not_logged_in'     =>  'You are not logged in .',
            'only_admin'        =>  'This action can be made only by admins.',
        );
        $this->errors = $errors ;
    }
    /**
    * This method can be used to send the error messages to the user .
    **/
    private function set_error($key='')
    {
        if(array_key_exists($key,$this->errors))
        {
            return $this->errors[$key];
        }
        return FALSE ;
    }
    



/**
* Uploadify Class End
**/    
}

Using uploadify not only that will break your file mime type, but will open another session(other user agent), so usually, you couldn't do further checks before/after the file has been uploaded using the session. With this library, the session data will be passed and we can do checks as we always do . The library will check to see if the user is logged in or if it is an admin . Also it'll check for a security token(we'll talk about this a bit later) .

STEP 3. The uploadify js code :

[removed]
$(function(){
<?php 
$userdata = json_encode($this->session->userdata);
$userdata = $this->encrypt->encode($userdata);
$userdata = base64_encode($userdata);
?>
$("#upload_image").uploadify({
        uploader: site.app_url+'/uploadify/uploadify.swf',
        script: site.site_url+'process_upload',
        cancelImg: site.app_url+'/uploadify/cancel.png',
        folder: '',
        scriptAccess: 'always',
        fileDesc : 'jpg,png,gif',
        fileExt : '*.jpg;*.png;*.gif',
        multi: false,
        wmode:'transparent',
        scriptData : {userdata:'<?php echo $userdata;?>','token':'<?php echo $token['value'];?>'},
        'onError' : function (a, b, c, d) {
            if (d.type === "File Size")
                alert(c.name+' '+d.type+' Limit: '+Math.round(d.sizeLimit/1024)+'KB');
             else
                alert('error '+d.type+": "+d.text);
            },
        'onComplete'   : function (event, queueID, fileObj, response, data) {
            var object = $(event.currentTarget); 
            var id = event.currentTarget.id; 
            $.post(site.site_url+'process_upload/process_method',
            {filearray: response,token:'<?php echo $token['value'];?>' },function(obj){
                if(obj.result === 'success'){
                    //Okay, say something nice
                }else{
                    //not okay, why ?
                }
            },"json");                                             
        }
    });
});
</ script>

So this code, will first send the file to be processed to the process_upload controller,the process_upload controller will load the Uploadify library and will do the checks, if everything will be okay, will post the filearray variable to process_method method from process_upload controller :

<?php if(! defined('BASEPATH')) exit('No direct script access allowed') ;


class Process_upload extends MY_Controller{

    public $tmp_path ;
    public $field_name ;
    public $allowed_types ;
    public $use_upload_token ;
    public $images_path ;   
    
    public function __construct()
    {
        parent::__construct() ;
        $this->tmp_path         = $this->config->item('upload_tmp_path');
        $this->field_name       = 'Filedata';
        $this->allowed_types    = $this->config->item('upload_allowed_types');
        $this->use_upload_token = $this->config->item('use_upload_token') ;
        $this->images_path      = FCPATH.'images/';
    }

    public function index()
    {
        //If everything is okay, the filearray will be returned.
        //Do extra checks here if is needed
        $this->load->library('uploadify');
        exit(json_encode($this->uploadify->do_upload()));
    }
    
    public function process_method()
    {
        $json = $this->input->post('filearray',TRUE);
        if(empty($json) || ! $this->valid_token())
        {
            exit(json_encode('your error type here'));
        }
        $json = json_decode($json);
     //And continue processing of the image here, as you want .
     //Move your uploaded file from tmp to real folder, etc etc
     }

}

STEP 4. During this example, we used a token algorithm, for avoiding CSRF attacks, so this is the logic for it , i placed it in MY_Controller because i use it often, you can create a library if you want .

    public function set_token()
    {
        $token      = sha1(uniqid(rand(), TRUE));
        $token_time = time();
        $token_data = array('token'=>$token,'token_time'=>$token_time);
        $this->session->set_userdata($token_data);
        return array(
                'value'     =>  $token,
                'input'     =>  '<input type="hidden" name="token" id="token" value="'.$token.'"/>'
                );
    }
    
    public function valid_token($show_error=FALSE, $token_life=300)
    {
        $token_time = intval($this->session->userdata('token_time'));
        if( (time() - $token_time) <= $token_life)
        {
            $post_token = $this->input->post('token',TRUE);
            $sess_token = $this->session->userdata('token',TRUE);
            if($post_token == $sess_token)
            {
                return TRUE ;
            }    
        }
        if($show_error)
        {
            show_error(lang('invalid_token'));
        }
        return FALSE;
    }

Now, in your controller you will set the token with $this->set_token(); and you will verify it with $this->valid_token(TRUE); Once you set your token, it can be accessible in your views with $token['input'] which will generate the input field, and $token['value'] that will show your token value .

Same token algorithm can be used into your forms as follows :

<?php
function my_form_template()
{ 
  if(!empty($_POST))
  {
    $this->valid_token(TRUE);
    //Add to database for example .
  }
  //OTHER LOGIC HERE
  $this->data['token'] = $this->set_token();
  $this->load->view('my-view-with-secure-form',$this->data);
}

Even if i am not to good at explaining things, i hope the above lines makes sense and will help you in the future .

Clone this wiki locally