-
Notifications
You must be signed in to change notification settings - Fork 7.6k
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) == 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 .