Skip to content

Uploadify Upload Class CSRF Tokens Session data The right way .

World Wide Web Server edited this page Jul 4, 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 : [code] <?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;
}  

} [/code] 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. [code] <?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&#40;$data['full_path']&#41;;
        return $this->set_error('invalid_mime_type');
    }
    
    if( ! empty($mimes[$ext]) && ! is_array($mimes[$ext]) && $data['file_type'] != $mimes[$ext])
    {
        @unlink&#40;$data['full_path']&#41;;
        return $this->set_error('invalid_mime_type');
    }
    elseif( ! empty($mimes[$ext]) && is_array($mimes[$ext]) && ! in_array($data['file_type'],$mimes[$ext]))
    {
        @unlink&#40;$data['full_path']&#41;;
        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&#40;$data['full_path']&#41;;
        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&#40;$data['full_path']&#41;;
        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&#40;$data['full_path']&#41;;
            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&#40;$data['full_path']&#41;;
        return $this->set_error('not_logged_in');
    }
    if($this->_only_admin && empty($userdata->is_admin))
    {
        @unlink&#40;$data['full_path']&#41;;
        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 **/
    } [/code] 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 : [code] [removed] $(function(){ <?php $userdata = json_encode($this->session->userdata); $userdata = $this->encrypt->encode($userdata); $userdata = base64_encode($userdata); ?&gt; $("#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'];?&gt;'}, '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> [/code] 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 : [code] <?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
 }

} [/code]

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 . [code] 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;
}

[/code] 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 : [code] 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); } [/code]

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