内容摘自https://github.com/EllisLab/CodeIgniter/wiki/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 ; $data['image_type'] = $ext; //原方法未返回image_type,在此添加 2012-09-05 by debi } 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) //if( ! empty($mimes[$ext]) && ! is_array($mimes[$ext]) && $data['file_type'] != $mimes[$ext]) { // 如果不是数组,最后一个比较条件应该是$mimes 而不是$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 :
$(function(){ $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"); } }); });
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 .
希望能帮助到大家,特别是在使用Uploadify上传文件的时候。