diff options
author | Florian Pritz <bluewind@xinu.at> | 2013-09-03 15:49:46 +0200 |
---|---|---|
committer | Florian Pritz <bluewind@xinu.at> | 2013-09-03 15:49:46 +0200 |
commit | 26e2f14fb02bc6dd3df3e516f57a51b628a2e4c2 (patch) | |
tree | 8d6c7b87844b1319adab7f25d7ef5c7c51e0e513 | |
parent | fbd587a62dc1d84e4ebf3917ecdb86270ea8a48d (diff) | |
parent | 69e0cba93445496e7b045b54ecefe8243276fd50 (diff) |
Merge branch 'working'
-rw-r--r-- | application/config/config.php | 2 | ||||
-rw-r--r-- | application/controllers/file.php | 86 | ||||
-rw-r--r-- | application/controllers/user.php | 8 | ||||
-rw-r--r-- | application/core/MY_Controller.php | 64 | ||||
-rwxr-xr-x | application/errors/error_general.php | 10 | ||||
-rw-r--r-- | application/helpers/filebin_helper.php | 20 | ||||
-rw-r--r-- | application/models/mfile.php | 1 | ||||
-rw-r--r-- | application/models/muser.php | 35 | ||||
-rw-r--r-- | application/views/file/client.php | 4 | ||||
-rw-r--r-- | application/views/file/deleted.php | 9 | ||||
-rw-r--r-- | application/views/file/too_big.php | 3 | ||||
-rw-r--r-- | application/views/file/upload_error.php | 6 | ||||
-rw-r--r-- | application/views/file/upload_form.php | 2 | ||||
-rw-r--r-- | application/views/file_plaintext/client.php | 4 | ||||
-rw-r--r-- | application/views/file_plaintext/too_big.php | 2 | ||||
-rw-r--r-- | application/views/file_plaintext/upload_error.php | 2 | ||||
-rwxr-xr-x | system/core/Common.php | 11 | ||||
-rwxr-xr-x | system/core/URI.php | 5 | ||||
-rwxr-xr-x | system/libraries/Security.php | 737 |
19 files changed, 190 insertions, 821 deletions
diff --git a/application/config/config.php b/application/config/config.php index dda82de97..4aadac68d 100644 --- a/application/config/config.php +++ b/application/config/config.php @@ -293,7 +293,7 @@ $config['global_xss_filtering'] = FALSE; | 'csrf_cookie_name' = The cookie name | 'csrf_expire' = The number in seconds the token should expire. */ -$config['csrf_protection'] = FALSE; +$config['csrf_protection'] = FALSE; // our controller enables this later $config['csrf_token_name'] = 'csrf_test_name'; $config['csrf_cookie_name'] = 'csrf_cookie_name'; $config['csrf_expire'] = 7200; diff --git a/application/controllers/file.php b/application/controllers/file.php index ef2e87084..6e660b306 100644 --- a/application/controllers/file.php +++ b/application/controllers/file.php @@ -9,6 +9,12 @@ class File extends MY_Controller { + protected $json_enabled_functions = array( + "upload_history", + "do_upload", + "do_delete", + ); + function __construct() { parent::__construct(); @@ -280,6 +286,10 @@ class File extends MY_Controller { } } + if (request_type() == "json") { + return send_json_reply($this->data["urls"]); + } + if (is_cli_client()) { $redirect = false; } @@ -456,7 +466,7 @@ class File extends MY_Controller { ORDER BY date $order ", array($user))->result_array(); - if ($this->input->get("json") !== false) { + if (request_type() == "json") { return send_json_reply($query); } @@ -499,11 +509,11 @@ class File extends MY_Controller { $ids = $this->input->post("ids"); $errors = array(); - $msgs = array(); + $deleted = array(); $deleted_count = 0; $total_count = 0; - if (!$ids) { + if (!$ids || !is_array($ids)) { show_error("No IDs specified"); } @@ -511,20 +521,34 @@ class File extends MY_Controller { $total_count++; if (!$this->mfile->id_exists($id)) { - $errors[] = "'$id' didn't exist anymore."; + $errors[] = array( + "id" => $id, + "reason" => "doesn't exist", + ); continue; } if ($this->mfile->delete_id($id)) { - $msgs[] = "'$id' has been removed."; + $deleted[] = $id; $deleted_count++; } else { - $errors[] = "'$id' couldn't be deleted."; + $errors[] = array( + "id" => $id, + "reason" => "unknown error", + ); } } + if (request_type() == "json") { + return send_json_reply(array( + "errors" => $errors, + "deleted" => $deleted, + "total_count" => $total_count, + "deleted_count" => $deleted_count, + )); + } + $this->data["errors"] = $errors; - $this->data["msgs"] = $msgs; $this->data["deleted_count"] = $deleted_count; $this->data["total_count"] = $total_count; @@ -538,17 +562,14 @@ class File extends MY_Controller { $this->muser->require_access("apikey"); if (!is_cli_client()) { - echo "Not a listed cli client, please use the history to delete uploads.\n"; - return; + show_error("Not a listed cli client, please use the history to delete uploads.\n", 403); } $id = $this->uri->segment(3); $this->data["id"] = $id; if ($id && !$this->mfile->id_exists($id)) { - $this->output->set_status_header(404); - echo "Unknown ID '$id'.\n"; - return; + show_error("Unknown ID '$id'.", 404); } if ($this->mfile->delete_id($id)) { @@ -562,6 +583,7 @@ class File extends MY_Controller { function do_paste() { // desktop clients get a cookie to claim the ID later + // don't force them to log in just yet if (is_cli_client()) { $this->muser->require_access(); } @@ -571,20 +593,11 @@ class File extends MY_Controller { $filename = "stdin"; if (!$content) { - $this->output->set_status_header(400); - $this->data["msg"] = "Nothing was pasted, content is empty."; - $this->load->view('header', $this->data); - $this->load->view($this->var->view_dir.'/upload_error', $this->data); - $this->load->view('footer'); - return; + show_error("Nothing was pasted, content is empty.", 400); } if ($filesize > $this->config->item('upload_max_size')) { - $this->output->set_status_header(413); - $this->load->view('header', $this->data); - $this->load->view($this->var->view_dir.'/too_big'); - $this->load->view('footer'); - return; + show_error("Error while uploading: File too big", 413); } $limits = $this->muser->get_upload_id_limits(); @@ -605,6 +618,7 @@ class File extends MY_Controller { function do_upload() { // desktop clients get a cookie to claim the ID later + // don't force them to log in just yet if (is_cli_client()) { $this->muser->require_access("apikey"); } @@ -624,8 +638,6 @@ class File extends MY_Controller { foreach ($files as $key => $file) { // getNormalizedFILES() removes any file with error == 4 if ($file['error'] !== UPLOAD_ERR_OK) { - $this->output->set_status_header(400); - // ERR_OK only for completeness, condition above ignores it $errors = array( UPLOAD_ERR_OK => "There is no error, the file uploaded with success", @@ -638,27 +650,20 @@ class File extends MY_Controller { UPLOAD_ERR_EXTENSION => "A PHP extension stopped the file upload", ); - $this->data["msg"] = "Unknown error."; + $msg = "Unknown error."; if (isset($errors[$file['error']])) { - $this->data["msg"] = $errors[$file['error']]; + $msg = $errors[$file['error']]; } else { - $this->data["msg"] = "Unknown error code: ".$file['error'].". Please report a bug."; + $msg = "Unknown error code: ".$file['error'].". Please report a bug."; } - $this->load->view('header', $this->data); - $this->load->view($this->var->view_dir.'/upload_error', $this->data); - $this->load->view('footer'); - return; + show_error("Error while uploading: ".$msg, 400); } $filesize = filesize($file['tmp_name']); if ($filesize > $this->config->item('upload_max_size')) { - $this->output->set_status_header(413); - $this->load->view('header', $this->data); - $this->load->view($this->var->view_dir.'/too_big'); - $this->load->view('footer'); - return; + show_error("Error while uploading: File too big", 413); } } @@ -699,9 +704,16 @@ class File extends MY_Controller { $this->muser->require_access(); $last_upload = $this->session->userdata("last_upload"); + + if ($last_upload === false) { + show_error("Failed to get last upload data"); + } + $ids = $last_upload["ids"]; $errors = array(); + assert(is_array($ids)); + foreach ($ids as $key => $id) { $filedata = $this->mfile->get_filedata($id); diff --git a/application/controllers/user.php b/application/controllers/user.php index 498a626d7..56f571d6a 100644 --- a/application/controllers/user.php +++ b/application/controllers/user.php @@ -8,6 +8,10 @@ */ class User extends MY_Controller { + protected $json_enabled_functions = array( + "apikeys", + ); + function __construct() { @@ -127,6 +131,10 @@ class User extends MY_Controller { WHERE `user` = ? order by created desc ", array($userid))->result_array(); + if (request_type() == "json") { + return send_json_reply($query); + } + $this->data["query"] = $query; $this->load->view('header', $this->data); diff --git a/application/core/MY_Controller.php b/application/core/MY_Controller.php index 278768ad2..312b0f763 100644 --- a/application/core/MY_Controller.php +++ b/application/core/MY_Controller.php @@ -11,7 +11,7 @@ class MY_Controller extends CI_Controller { public $data = array(); public $var; - private $json_enabled_functions = array( + protected $json_enabled_functions = array( ); function __construct() @@ -19,6 +19,7 @@ class MY_Controller extends CI_Controller { parent::__construct(); $this->var = new StdClass(); + $csrf_protection = true; $this->load->library('migration'); if ( ! $this->migration->current()) { @@ -31,6 +32,67 @@ class MY_Controller extends CI_Controller { mb_internal_encoding('UTF-8'); $this->load->helper(array('form', 'filebin')); + // TODO: proper accept header handling or is this enough? + if (isset($_SERVER["HTTP_ACCEPT"])) { + if ($_SERVER["HTTP_ACCEPT"] == "application/json") { + request_type("json"); + } + } + + // Allow for easier testing in browser + if ($this->input->get("json") !== false) { + request_type("json"); + } + + if (request_type() == "json" && ! in_array($this->uri->rsegment(2), $this->json_enabled_functions)) { + show_error("Function not JSON enabled"); + } + + if ($this->input->post("apikey") !== false) { + /* This relies on the authentication code always verifying the supplied + * apikey. If the key is not verified/logged in an attacker could simply + * add an empty "apikey" field to the CSRF form to circumvent the + * protection. If we always log in if a key is supplied we can ensure + * that an attacker (and the victim since they get a cookie) can only + * access the attacker's account. + */ + $csrf_protection = false; + } + + $uri_start = $this->uri->rsegment(1)."/".$this->uri->rsegment(2); + $csrf_whitelisted_handlers = array( + "always" => array( + /* Whitelist the upload pages because they don't cause harm and a user + * might keep the upload page open for more than csrf_expire seconds + * and we don't want to annoy them when they upload a big file and the + * CSRF check fails. + */ + "file/do_upload", + "file/do_paste", + ), + "cli_client" => array( + "file/do_delete", + "file/delete", + "file/upload_history", + ), + ); + if (in_array($uri_start, $csrf_whitelisted_handlers["always"])) { + $csrf_protection = false; + } + + // TODO: replace cli client with request_type("plain")? + if (is_cli_client() && in_array($uri_start, $csrf_whitelisted_handlers["cli_client"])) { + $csrf_protection = false; + } + + if ($csrf_protection && !$this->input->is_cli_request()) { + // 2 functions for accessing config options, really? + $this->config->set_item('csrf_protection', true); + config_item("csrf_protection", true); + $this->security->__construct(); + $this->security->csrf_verify(); + } + $this->data['title'] = "FileBin"; } } diff --git a/application/errors/error_general.php b/application/errors/error_general.php index da3999cbd..fc3d3f607 100755 --- a/application/errors/error_general.php +++ b/application/errors/error_general.php @@ -9,6 +9,16 @@ if (class_exists("CI_Controller") && !isset($GLOBALS["is_error_page"])) { $CI->load->helper("filebin"); $CI->load->helper("url"); + if (request_type() == "json") { + $array = array( + "status" => "error", + "message" => strip_tags($message), + ); + header('Content-type: application/json'); + echo json_encode($array); + exit(); + } + if (is_cli_client()) { $message = strip_tags($message); echo "$heading: $message\n"; diff --git a/application/helpers/filebin_helper.php b/application/helpers/filebin_helper.php index 4af106a14..9b124506f 100644 --- a/application/helpers/filebin_helper.php +++ b/application/helpers/filebin_helper.php @@ -326,14 +326,30 @@ function auth_driver_function_implemented($function) function user_logged_in() { $CI =& get_instance(); + $CI->load->model("muser"); return $CI->muser->logged_in(); } -function send_json_reply($array) +function send_json_reply($array, $status = "success") { + $reply = array(); + $reply["status"] = $status; + $reply["data"] = $array; + $CI =& get_instance(); $CI->output->set_content_type('application/json'); - $CI->output->set_output(json_encode($array)); + $CI->output->set_output(json_encode($reply)); +} + +function request_type($set_type = null) +{ + static $type = null; + + if ($set_type !== null) { + $type = $set_type; + } + + return $type; } # vim: set noet: diff --git a/application/models/mfile.php b/application/models/mfile.php index fe762d954..e862f1930 100644 --- a/application/models/mfile.php +++ b/application/models/mfile.php @@ -312,7 +312,6 @@ class Mfile extends CI_Model { function delete_id($id) { - $this->muser->require_access("apikey"); $filedata = $this->get_filedata($id); $userid = $this->muser->get_userid(); diff --git a/application/models/muser.php b/application/models/muser.php index 843b7cad6..b3c16bf78 100644 --- a/application/models/muser.php +++ b/application/models/muser.php @@ -67,14 +67,6 @@ class Muser extends CI_Model { { $username = $this->input->post("username"); $password = $this->input->post("password"); - $apikey = $this->input->post("apikey"); - - if ($apikey !== false) { - if ($this->apilogin(trim($apikey))) { - return true; - } - show_error("API key login failed", 401); - } // prefer post parameters if either (username or password) is set if ($username === false && $password === false) { @@ -84,19 +76,24 @@ class Muser extends CI_Model { } } - if ($apikey === false && $username !== false && $password !== false) { + if ($username !== false && $password !== false) { if ($this->login($username, $password)) { return true; } else { show_error("Login failed", 401); } } + + return null; } function apilogin($apikey) { $this->require_session(); + // get rid of spaces and newlines + $apikey = trim($apikey); + $query = $this->db->query(" SELECT a.user userid FROM apikeys a @@ -111,7 +108,7 @@ class Muser extends CI_Model { return true; } - return false; + show_error("API key login failed", 401); } function logout() @@ -168,22 +165,26 @@ class Muser extends CI_Model { return true; } - show_error("Access denied", 403); + show_error("Access denied: Access level too low", 403); } function require_access($wanted_level = "full") { + if ($this->input->post("apikey") !== false) { + $this->apilogin($this->input->post("apikey")); + } + + if (is_cli_client()) { + $this->login_cli_client(); + } + if ($this->logged_in()) { return $this->check_access_level($wanted_level); } + // if a CLI client reaches this point it failed to log in if (is_cli_client()) { - if ($this->login_cli_client()) { - return $this->check_access_level($wanted_level); - } - - echo "FileBin requires you to have an account, please go to the homepage for more information.\n"; - exit(); + show_error("Not authenticated. FileBin requires you to have an account, please go to the homepage for more information.\n", 401); } // desktop clients get redirected to the login form diff --git a/application/views/file/client.php b/application/views/file/client.php index 5e141f141..29e254a80 100644 --- a/application/views/file/client.php +++ b/application/views/file/client.php @@ -42,7 +42,7 @@ machine <?php echo $domain; ?> login my_username password my_secret_password <h1>Shell</h1> <pre> -curl -n -F "file=@/home/user/foo" <?php echo site_url(); ?> (binary safe) -cat file | curl -n -F "file=@-;filename=stdin" <?php echo site_url(); ?> (binary safe) +curl -n -F "file=@/home/user/foo" <?php echo site_url("file/do_upload"); ?> (binary safe) +cat file | curl -n -F "file=@-;filename=stdin" <?php echo site_url("file/do_upload"); ?> (binary safe) </pre> diff --git a/application/views/file/deleted.php b/application/views/file/deleted.php index ef02398d9..8a5818f2d 100644 --- a/application/views/file/deleted.php +++ b/application/views/file/deleted.php @@ -1,12 +1,9 @@ <div class="center"> <?php if (!empty($errors)) { echo "<p>"; - echo implode("<br />\n", $errors); - echo "</p>"; - } ?> - <?php if (!empty($msgs)) { - echo "<p>"; - echo implode("<br />\n", $msgs); + foreach ($errors as $error) { + echo "${error["id"]}: ${error["reason"]}<br>\n"; + } echo "</p>"; } ?> diff --git a/application/views/file/too_big.php b/application/views/file/too_big.php deleted file mode 100644 index 5b970a4c8..000000000 --- a/application/views/file/too_big.php +++ /dev/null @@ -1,3 +0,0 @@ -<div class="center"> - <p>Sorry, the file you uploaded is too big.</p> -</div> diff --git a/application/views/file/upload_error.php b/application/views/file/upload_error.php deleted file mode 100644 index 83968eec2..000000000 --- a/application/views/file/upload_error.php +++ /dev/null @@ -1,6 +0,0 @@ -<div class="center"> - <p> - An error occurred while uploading.<br /> - <?php echo $msg; ?> - </p> -</div> diff --git a/application/views/file/upload_form.php b/application/views/file/upload_form.php index 841ac3746..21a2cc4e6 100644 --- a/application/views/file/upload_form.php +++ b/application/views/file/upload_form.php @@ -46,7 +46,7 @@ <?php } else { ?> <?php echo form_open('user/login'); ?> - <input type="text" name="username" placeholder="Username" /> + <input type="text" name="username" placeholder="Username" autofocus /> <input type="password" name="password" placeholder="Password" /> <input type="submit" class="btn btn-primary" value="Login" name="process" style="margin-bottom: 9px" /> <?php if(auth_driver_function_implemented("can_reset_password")) { ?> diff --git a/application/views/file_plaintext/client.php b/application/views/file_plaintext/client.php index b37fd81bd..0ab556df2 100644 --- a/application/views/file_plaintext/client.php +++ b/application/views/file_plaintext/client.php @@ -1,6 +1,6 @@ Shell (binary safe): - curl -n -F "file=@/home/user/foo" <?php echo site_url()."\n"; ?> - cat file | curl -n -F "file=@-;filename=stdin" <?php echo site_url()."\n"; ?> + curl -n -F "file=@/home/user/foo" <?php echo site_url("file/do_upload")."\n"; ?> + cat file | curl -n -F "file=@-;filename=stdin" <?php echo site_url("file/do_upload")."\n"; ?> Client: Development (git): http://git.server-speed.net/users/flo/fb diff --git a/application/views/file_plaintext/too_big.php b/application/views/file_plaintext/too_big.php deleted file mode 100644 index d27a0295c..000000000 --- a/application/views/file_plaintext/too_big.php +++ /dev/null @@ -1,2 +0,0 @@ -Sorry, the file you uploaded is too big. - diff --git a/application/views/file_plaintext/upload_error.php b/application/views/file_plaintext/upload_error.php deleted file mode 100644 index c86c56911..000000000 --- a/application/views/file_plaintext/upload_error.php +++ /dev/null @@ -1,2 +0,0 @@ -An error occurred while uploading. <?php echo $msg; ?> - diff --git a/system/core/Common.php b/system/core/Common.php index 07534c51f..f8d80b957 100755 --- a/system/core/Common.php +++ b/system/core/Common.php @@ -1,5 +1,10 @@ <?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); /** + * MODIFIED + * config_item(): option to override returned values + */ + +/** * CodeIgniter * * An open source application development framework for PHP 5.1.6 or newer @@ -268,7 +273,7 @@ if ( ! function_exists('get_config')) */ if ( ! function_exists('config_item')) { - function config_item($item) + function config_item($item, $value = null) { static $_config_item = array(); @@ -283,6 +288,10 @@ if ( ! function_exists('config_item')) $_config_item[$item] = $config[$item]; } + if ($value !== null) { + $_config_item[$item] = $value; + } + return $_config_item[$item]; } } diff --git a/system/core/URI.php b/system/core/URI.php index d78c8ee49..d3c25070e 100755 --- a/system/core/URI.php +++ b/system/core/URI.php @@ -1,5 +1,10 @@ <?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); /** + * MODIFIED: + * _detect_uri(): ltrim instead of trim at the end to preserve tailing slashes + */ + +/** * CodeIgniter * * An open source application development framework for PHP 5.1.6 or newer diff --git a/system/libraries/Security.php b/system/libraries/Security.php deleted file mode 100755 index ba64c7326..000000000 --- a/system/libraries/Security.php +++ /dev/null @@ -1,737 +0,0 @@ -<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); -/** - * CodeIgniter - * - * An open source application development framework for PHP 5.1.6 or newer - * - * @package CodeIgniter - * @author ExpressionEngine Dev Team - * @copyright Copyright (c) 2008 - 2011, EllisLab, Inc. - * @license http://codeigniter.com/user_guide/license.html - * @link http://codeigniter.com - * @since Version 1.0 - * @filesource - */ - -// ------------------------------------------------------------------------ - -/** - * Security Class - * - * @package CodeIgniter - * @subpackage Libraries - * @category Security - * @author ExpressionEngine Dev Team - * @link http://codeigniter.com/user_guide/libraries/sessions.html - */ -class CI_Security { - - public $xss_hash = ''; - public $csrf_hash = ''; - public $csrf_expire = 7200; // Two hours (in seconds) - public $csrf_token_name = 'ci_csrf_token'; - public $csrf_cookie_name = 'ci_csrf_token'; - - /* never allowed, string replacement */ - public $never_allowed_str = array( - 'document.cookie' => '[removed]', - 'document.write' => '[removed]', - '.parentNode' => '[removed]', - '.innerHTML' => '[removed]', - 'window.location' => '[removed]', - '-moz-binding' => '[removed]', - '<!--' => '<!--', - '-->' => '-->', - '<![CDATA[' => '<![CDATA[' - ); - /* never allowed, regex replacement */ - public $never_allowed_regex = array( - "javascript\s*:" => '[removed]', - "expression\s*(\(|&\#40;)" => '[removed]', // CSS and IE - "vbscript\s*:" => '[removed]', // IE, surprise! - "Redirect\s+302" => '[removed]' - ); - - public function __construct() - { - $this->csrf_token_name = (config_item('csrf_token_name')) ? config_item('csrf_token_name') : 'csrf_token_name'; - $this->csrf_cookie_name = (config_item('csrf_cookie_name')) ? config_item('csrf_cookie_name') : 'csrf_cookie_name'; - $this->csrf_expire = (config_item('csrf_expire')) ? config_item('csrf_expire') : 7200; - - // Append application specific cookie prefix to token name - $this->csrf_cookie_name = (config_item('cookie_prefix')) ? config_item('cookie_prefix').$this->csrf_token_name : $this->csrf_token_name; - - // Set the CSRF hash - $this->_csrf_set_hash(); - - log_message('debug', "Security Class Initialized"); - } - - // -------------------------------------------------------------------- - - /** - * Verify Cross Site Request Forgery Protection - * - * @access public - * @return null - */ - public function csrf_verify() - { - // If no POST data exists we will set the CSRF cookie - if (count($_POST) == 0) - { - return $this->csrf_set_cookie(); - } - - // Do the tokens exist in both the _POST and _COOKIE arrays? - if ( ! isset($_POST[$this->csrf_token_name]) OR ! isset($_COOKIE[$this->csrf_cookie_name])) - { - $this->csrf_show_error(); - } - - // Do the tokens match? - if ($_POST[$this->csrf_token_name] != $_COOKIE[$this->csrf_cookie_name]) - { - $this->csrf_show_error(); - } - - // We kill this since we're done and we don't want to polute the _POST array - unset($_POST[$this->csrf_token_name]); - - // Nothing should last forever - unset($_COOKIE[$this->csrf_cookie_name]); - $this->_csrf_set_hash(); - $this->csrf_set_cookie(); - - log_message('debug', "CSRF token verified "); - } - - // -------------------------------------------------------------------- - - /** - * Set Cross Site Request Forgery Protection Cookie - * - * @access public - * @return null - */ - public function csrf_set_cookie() - { - $expire = time() + $this->csrf_expire; - - setcookie($this->csrf_cookie_name, $this->csrf_hash, $expire, config_item('cookie_path'), config_item('cookie_domain'), 0); - - log_message('debug', "CRSF cookie Set"); - } - - // -------------------------------------------------------------------- - - /** - * Set Cross Site Request Forgery Protection Cookie - * - * @access private - * @return null - */ - private function _csrf_set_hash() - { - if ($this->csrf_hash == '') - { - // If the cookie exists we will use it's value. We don't necessarily want to regenerate it with - // each page load since a page could contain embedded sub-pages causing this feature to fail - if (isset($_COOKIE[$this->csrf_cookie_name]) AND $_COOKIE[$this->csrf_cookie_name] != '') - { - $this->csrf_hash = $_COOKIE[$this->csrf_cookie_name]; - } - else - { - $this->csrf_hash = md5(uniqid(rand(), TRUE)); - } - } - - return $this->csrf_hash; - } - - // -------------------------------------------------------------------- - - /** - * Show CSRF Error - * - * @access public - * @return null - */ - public function csrf_show_error() - { - show_error('The action you have requested is not allowed.'); - } - - // -------------------------------------------------------------------- - - /** - * XSS Clean - * - * Sanitizes data so that Cross Site Scripting Hacks can be - * prevented. This function does a fair amount of work but - * it is extremely thorough, designed to prevent even the - * most obscure XSS attempts. Nothing is ever 100% foolproof, - * of course, but I haven't been able to get anything passed - * the filter. - * - * Note: This function should only be used to deal with data - * upon submission. It's not something that should - * be used for general runtime processing. - * - * This function was based in part on some code and ideas I - * got from Bitflux: http://channel.bitflux.ch/wiki/XSS_Prevention - * - * To help develop this script I used this great list of - * vulnerabilities along with a few other hacks I've - * harvested from examining vulnerabilities in other programs: - * http://ha.ckers.org/xss.html - * - * @access public - * @param mixed string or array - * @return string - */ - public function xss_clean($str, $is_image = FALSE) - { - /* - * Is the string an array? - * - */ - if (is_array($str)) - { - while (list($key) = each($str)) - { - $str[$key] = $this->xss_clean($str[$key]); - } - - return $str; - } - - /* - * Remove Invisible Characters - */ - $str = remove_invisible_characters($str); - - /* - * Protect GET variables in URLs - */ - - // 901119URL5918AMP18930PROTECT8198 - - $str = preg_replace('|\&([a-z\_0-9\-]+)\=([a-z\_0-9\-]+)|i', $this->xss_hash()."\\1=\\2", $str); - - /* - * Validate standard character entities - * - * Add a semicolon if missing. We do this to enable - * the conversion of entities to ASCII later. - * - */ - $str = preg_replace('#(&\#?[0-9a-z]{2,})([\x00-\x20])*;?#i', "\\1;\\2", $str); - - /* - * Validate UTF16 two byte encoding (x00) - * - * Just as above, adds a semicolon if missing. - * - */ - $str = preg_replace('#(&\#x?)([0-9A-F]+);?#i',"\\1\\2;",$str); - - /* - * Un-Protect GET variables in URLs - */ - $str = str_replace($this->xss_hash(), '&', $str); - - /* - * URL Decode - * - * Just in case stuff like this is submitted: - * - * <a href="http://%77%77%77%2E%67%6F%6F%67%6C%65%2E%63%6F%6D">Google</a> - * - * Note: Use rawurldecode() so it does not remove plus signs - * - */ - $str = rawurldecode($str); - - /* - * Convert character entities to ASCII - * - * This permits our tests below to work reliably. - * We only convert entities that are within tags since - * these are the ones that will pose security problems. - * - */ - - $str = preg_replace_callback("/[a-z]+=([\'\"]).*?\\1/si", array($this, '_convert_attribute'), $str); - - $str = preg_replace_callback("/<\w+.*?(?=>|<|$)/si", array($this, '_decode_entity'), $str); - - /* - * Remove Invisible Characters Again! - */ - $str = remove_invisible_characters($str); - - /* - * Convert all tabs to spaces - * - * This prevents strings like this: ja vascript - * NOTE: we deal with spaces between characters later. - * NOTE: preg_replace was found to be amazingly slow here on large blocks of data, - * so we use str_replace. - * - */ - - if (strpos($str, "\t") !== FALSE) - { - $str = str_replace("\t", ' ', $str); - } - - /* - * Capture converted string for later comparison - */ - $converted_string = $str; - - /* - * Not Allowed Under Any Conditions - */ - - foreach ($this->never_allowed_str as $key => $val) - { - $str = str_replace($key, $val, $str); - } - - foreach ($this->never_allowed_regex as $key => $val) - { - $str = preg_replace("#".$key."#i", $val, $str); - } - - /* - * Makes PHP tags safe - * - * Note: XML tags are inadvertently replaced too: - * - * <?xml - * - * But it doesn't seem to pose a problem. - * - */ - if ($is_image === TRUE) - { - // Images have a tendency to have the PHP short opening and closing tags every so often - // so we skip those and only do the long opening tags. - $str = preg_replace('/<\?(php)/i', "<?\\1", $str); - } - else - { - $str = str_replace(array('<?', '?'.'>'), array('<?', '?>'), $str); - } - - /* - * Compact any exploded words - * - * This corrects words like: j a v a s c r i p t - * These words are compacted back to their correct state. - * - */ - $words = array('javascript', 'expression', 'vbscript', 'script', 'applet', 'alert', 'document', 'write', 'cookie', 'window'); - foreach ($words as $word) - { - $temp = ''; - - for ($i = 0, $wordlen = strlen($word); $i < $wordlen; $i++) - { - $temp .= substr($word, $i, 1)."\s*"; - } - - // We only want to do this when it is followed by a non-word character - // That way valid stuff like "dealer to" does not become "dealerto" - $str = preg_replace_callback('#('.substr($temp, 0, -3).')(\W)#is', array($this, '_compact_exploded_words'), $str); - } - - /* - * Remove disallowed Javascript in links or img tags - * We used to do some version comparisons and use of stripos for PHP5, but it is dog slow compared - * to these simplified non-capturing preg_match(), especially if the pattern exists in the string - */ - do - { - $original = $str; - - if (preg_match("/<a/i", $str)) - { - $str = preg_replace_callback("#<a\s+([^>]*?)(>|$)#si", array($this, '_js_link_removal'), $str); - } - - if (preg_match("/<img/i", $str)) - { - $str = preg_replace_callback("#<img\s+([^>]*?)(\s?/?>|$)#si", array($this, '_js_img_removal'), $str); - } - - if (preg_match("/script/i", $str) OR preg_match("/xss/i", $str)) - { - $str = preg_replace("#<(/*)(script|xss)(.*?)\>#si", '[removed]', $str); - } - } - while($original != $str); - - unset($original); - - /* - * Remove JavaScript Event Handlers - * - * Note: This code is a little blunt. It removes - * the event handler and anything up to the closing >, - * but it's unlikely to be a problem. - * - */ - $event_handlers = array('[^a-z_\-]on\w*','xmlns'); - - if ($is_image === TRUE) - { - /* - * Adobe Photoshop puts XML metadata into JFIF images, including namespacing, - * so we have to allow this for images. -Paul - */ - unset($event_handlers[array_search('xmlns', $event_handlers)]); - } - - $str = preg_replace("#<([^><]+?)(".implode('|', $event_handlers).")(\s*=\s*[^><]*)([><]*)#i", "<\\1\\4", $str); - - /* - * Sanitize naughty HTML elements - * - * If a tag containing any of the words in the list - * below is found, the tag gets converted to entities. - * - * So this: <blink> - * Becomes: <blink> - * - */ - $naughty = 'alert|applet|audio|basefont|base|behavior|bgsound|blink|body|embed|expression|form|frameset|frame|head|html|ilayer|iframe|input|isindex|layer|link|meta|object|plaintext|style|script|textarea|title|video|xml|xss'; - $str = preg_replace_callback('#<(/*\s*)('.$naughty.')([^><]*)([><]*)#is', array($this, '_sanitize_naughty_html'), $str); - - /* - * Sanitize naughty scripting elements - * - * Similar to above, only instead of looking for - * tags it looks for PHP and JavaScript commands - * that are disallowed. Rather than removing the - * code, it simply converts the parenthesis to entities - * rendering the code un-executable. - * - * For example: eval('some code') - * Becomes: eval('some code') - * - */ - $str = preg_replace('#(alert|cmd|passthru|eval|exec|expression|system|fopen|fsockopen|file|file_get_contents|readfile|unlink)(\s*)\((.*?)\)#si', "\\1\\2(\\3)", $str); - - /* - * Final clean up - * - * This adds a bit of extra precaution in case - * something got through the above filters - * - */ - foreach ($this->never_allowed_str as $key => $val) - { - $str = str_replace($key, $val, $str); - } - - foreach ($this->never_allowed_regex as $key => $val) - { - $str = preg_replace("#".$key."#i", $val, $str); - } - - /* - * Images are Handled in a Special Way - * - Essentially, we want to know that after all of the character conversion is done whether - * any unwanted, likely XSS, code was found. If not, we return TRUE, as the image is clean. - * However, if the string post-conversion does not matched the string post-removal of XSS, - * then it fails, as there was unwanted XSS code found and removed/changed during processing. - */ - - if ($is_image === TRUE) - { - if ($str == $converted_string) - { - return TRUE; - } - else - { - return FALSE; - } - } - - log_message('debug', "XSS Filtering completed"); - return $str; - } - - // -------------------------------------------------------------------- - - /** - * Random Hash for protecting URLs - * - * @access public - * @return string - */ - public function xss_hash() - { - if ($this->xss_hash == '') - { - if (phpversion() >= 4.2) - mt_srand(); - else - mt_srand(hexdec(substr(md5(microtime()), -8)) & 0x7fffffff); - - $this->xss_hash = md5(time() + mt_rand(0, 1999999999)); - } - - return $this->xss_hash; - } - - // -------------------------------------------------------------------- - - /** - * Compact Exploded Words - * - * Callback function for xss_clean() to remove whitespace from - * things like j a v a s c r i p t - * - * @access private - * @param type - * @return type - */ - private function _compact_exploded_words($matches) - { - return preg_replace('/\s+/s', '', $matches[1]).$matches[2]; - } - - // -------------------------------------------------------------------- - - /** - * Sanitize Naughty HTML - * - * Callback function for xss_clean() to remove naughty HTML elements - * - * @access private - * @param array - * @return string - */ - private function _sanitize_naughty_html($matches) - { - // encode opening brace - $str = '<'.$matches[1].$matches[2].$matches[3]; - - // encode captured opening or closing brace to prevent recursive vectors - $str .= str_replace(array('>', '<'), array('>', '<'), $matches[4]); - - return $str; - } - - // -------------------------------------------------------------------- - - /** - * JS Link Removal - * - * Callback function for xss_clean() to sanitize links - * This limits the PCRE backtracks, making it more performance friendly - * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in - * PHP 5.2+ on link-heavy strings - * - * @access private - * @param array - * @return string - */ - private function _js_link_removal($match) - { - $attributes = $this->_filter_attributes(str_replace(array('<', '>'), '', $match[1])); - return str_replace($match[1], preg_replace("#href=.*?(alert\(|alert&\#40;|javascript\:|charset\=|window\.|document\.|\.cookie|<script|<xss|base64\s*,)#si", "", $attributes), $match[0]); - } - - /** - * JS Image Removal - * - * Callback function for xss_clean() to sanitize image tags - * This limits the PCRE backtracks, making it more performance friendly - * and prevents PREG_BACKTRACK_LIMIT_ERROR from being triggered in - * PHP 5.2+ on image tag heavy strings - * - * @access private - * @param array - * @return string - */ - private function _js_img_removal($match) - { - $attributes = $this->_filter_attributes(str_replace(array('<', '>'), '', $match[1])); - return str_replace($match[1], preg_replace("#src=.*?(alert\(|alert&\#40;|javascript\:|charset\=|window\.|document\.|\.cookie|<script|<xss|base64\s*,)#si", "", $attributes), $match[0]); - } - - // -------------------------------------------------------------------- - - /** - * Attribute Conversion - * - * Used as a callback for XSS Clean - * - * @access private - * @param array - * @return string - */ - private function _convert_attribute($match) - { - return str_replace(array('>', '<', '\\'), array('>', '<', '\\\\'), $match[0]); - } - - // -------------------------------------------------------------------- - - /** - * Filter Attributes - * - * Filters tag attributes for consistency and safety - * - * @access private - * @param string - * @return string - */ - private function _filter_attributes($str) - { - $out = ''; - - if (preg_match_all('#\s*[a-z\-]+\s*=\s*(\042|\047)([^\\1]*?)\\1#is', $str, $matches)) - { - foreach ($matches[0] as $match) - { - $out .= preg_replace("#/\*.*?\*/#s", '', $match); - } - } - - return $out; - } - - // -------------------------------------------------------------------- - - /** - * HTML Entity Decode Callback - * - * Used as a callback for XSS Clean - * - * @access private - * @param array - * @return string - */ - private function _decode_entity($match) - { - return $this->entity_decode($match[0], strtoupper(config_item('charset'))); - } - - // -------------------------------------------------------------------- - - /** - * HTML Entities Decode - * - * This function is a replacement for html_entity_decode() - * - * In some versions of PHP the native function does not work - * when UTF-8 is the specified character set, so this gives us - * a work-around. More info here: - * http://bugs.php.net/bug.php?id=25670 - * - * NOTE: html_entity_decode() has a bug in some PHP versions when UTF-8 is the - * character set, and the PHP developers said they were not back porting the - * fix to versions other than PHP 5.x. - * - * @access public - * @param string - * @param string - * @return string - */ - public function entity_decode($str, $charset='UTF-8') - { - if (stristr($str, '&') === FALSE) return $str; - - // The reason we are not using html_entity_decode() by itself is because - // while it is not technically correct to leave out the semicolon - // at the end of an entity most browsers will still interpret the entity - // correctly. html_entity_decode() does not convert entities without - // semicolons, so we are left with our own little solution here. Bummer. - - if (function_exists('html_entity_decode') && (strtolower($charset) != 'utf-8' OR is_php('5.0.0'))) - { - $str = html_entity_decode($str, ENT_COMPAT, $charset); - $str = preg_replace('~&#x(0*[0-9a-f]{2,5})~ei', 'chr(hexdec("\\1"))', $str); - return preg_replace('~&#([0-9]{2,4})~e', 'chr(\\1)', $str); - } - - // Numeric Entities - $str = preg_replace('~&#x(0*[0-9a-f]{2,5});{0,1}~ei', 'chr(hexdec("\\1"))', $str); - $str = preg_replace('~&#([0-9]{2,4});{0,1}~e', 'chr(\\1)', $str); - - // Literal Entities - Slightly slow so we do another check - if (stristr($str, '&') === FALSE) - { - $str = strtr($str, array_flip(get_html_translation_table(HTML_ENTITIES))); - } - - return $str; - } - - // -------------------------------------------------------------------- - - /** - * Filename Security - * - * @access public - * @param string - * @return string - */ - public function sanitize_filename($str, $relative_path = FALSE) - { - $bad = array( - "../", - "<!--", - "-->", - "<", - ">", - "'", - '"', - '&', - '$', - '#', - '{', - '}', - '[', - ']', - '=', - ';', - '?', - "%20", - "%22", - "%3c", // < - "%253c", // < - "%3e", // > - "%0e", // > - "%28", // ( - "%29", // ) - "%2528", // ( - "%26", // & - "%24", // $ - "%3f", // ? - "%3b", // ; - "%3d" // = - ); - - if ( ! $relative_path) - { - $bad[] = './'; - $bad[] = '/'; - } - - return stripslashes(str_replace($bad, '', $str)); - } - -} -// END Security Class - -/* End of file Security.php */ -/* Location: ./system/libraries/Security.php */
\ No newline at end of file |