Part of the EllisLab Network
x
 
Create New Page
 View Previous Changes    ( Last updated by Twisted1919 )

Uploadify Upload Class CSRF Tokens Session data - The right way .

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) == 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$mimeTRUE))
            
{
                
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',
        
scriptsite.site_url+'process_upload',
        
cancelImgsite.app_url+'/uploadify/cancel.png',
        
folder'',
        
scriptAccess'always',
        
fileDesc 'jpg,png,gif',
        
fileExt '*.jpg;*.png;*.gif',
        
multifalse,
        
wmode:'transparent',
        
scriptData {userdata:'<?php echo $userdata;?>','token':'<?php echo $token['value'];?>'},
        
'onError' : function (abcd{
            
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 (eventqueueIDfileObjresponsedata{
            
var object = $(event.currentTarget); 
            var 
id event.currentTarget.id
            $.
post(site.site_url+'process_upload/process_method',
            
{filearrayresponse,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 :

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 .