niksoft

SSIMySQLi

May 27th, 2011
92
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
PHP 11.69 KB | None | 0 0
  1. /*
  2. Explanation:
  3. Before inline interpolation (example from Mike Samuel):
  4.  
  5. $stmt = $db->prepare(   'I met a ? in the ? who had a ? full of ?.  He said'
  6.         .' "Hello, ?, How are you today\?"  "Fine," I replied'
  7.         .' "but I can't seem to find my ?."  "Hmm," he said.'
  8.         .' "I Can\'t help you with that."  "How about ? ? to'
  9.         .' make you feel ?\?"');
  10. $stmt->bind_param("sssssiss", $profession, $landmark, $container, $species_of_monkey, $proper_name, $common_household_item, $some_number, $beverage, $state_of_mind);
  11.  
  12. It is secure, but its as much of a pain to read as it is to write...
  13.  
  14. But what if we could just say
  15. $db->query( 'I met a ^^profession in the ^^landmark who had a ^^container full of ^^species_of_monkey.  He said'
  16.         .' "Hello, ^^proper_name, How are you today\?"  "Fine," I replied'
  17.         .' "but I can't seem to find my ^^common_household_item."  "Hmm," he said.'
  18.         .' "I Can\'t help you with that."  "How about ^^number ^^beverage to'
  19.         .' make you feel ^^state_of_mind\?"');
  20. and still be secure without adding anything that we don't already have (like b64d function in mysql)? well, we can :)
  21. In addition to determining and passing data and query via separate channels, if someone tries to inline inject strings into the code or something in the query, it will fail.
  22.  
  23. You are welcome to use or sony(meaning pwn) this code, if you improve the code or manage to inject or break something, please let me know.
  24. */
  25.  
  26. // An attempt at forcing developers to use secure string interpolation in mysql queries with some integrated caching and direct csv output.
  27. class SSIMysqli extends mysqli
  28. {
  29.     // Members
  30.     protected static $mysqli;
  31.     protected $memcache=false, $result=false, $bind_arr=array(), $row=0, $data=null, $key=null, $vars=null;
  32.  
  33.     // Private Methods
  34.     private function clean($vars) {
  35.         $clean = array('GLOBALS', 'argc', 'argv', '_GET', '_POST', '_COOKIE', '_FILES', '_SERVER');
  36.         foreach($clean as $key) {
  37.             if(@array_key_exists($key, $vars)) { unset($vars[$key]); }
  38.         }
  39.         return $vars;
  40.     }
  41.  
  42.     private function output_csv(&$vals, $key, $stream) { fputcsv($stream, $vals, ',', '"'); }
  43.  
  44.     //Public Methods
  45.     public function __construct($db = MYSQLI_DB, $host = MYSQLI_HOST, $user = MYSQLI_USER, $pass = MYSQLI_PASS, $port = MYSQLI_PORT, $mhost = MEM_SERVER, $mport = MEM_PORT, $mtime = MEM_TIMEOUT, $mctime = MEM_CONNECT_TIMEOUT, $mrtime = MEM_RETRY_TIMEOUT, $mcomp = MEM_COMPRESSION)
  46.     {
  47.         parent::__construct($host, $user, $pass, $db, $port);
  48.         if(mysqli_connect_errno()) {
  49.             error_log("Could not connect to database! Repent! The end is neigh!");
  50.             die();
  51.         }
  52.         if(extension_loaded('memcached.so') && $mtime != 0){
  53.             $this->memcache = new Memcached();
  54.             if (!$this->memcache->addServer($mhost, $mport)) {
  55.                 error_log('Could not connect to MemCache server');
  56.                 $obj = false;
  57.             }
  58.             else {
  59.                 $this->memcache->setOption(Memcached::OPT_CONNECT_TIMEOUT, $mctime); // connection timeout in milliseconds
  60.                 $this->memcache->setOption(Memcached::OPT_RETRY_TIMEOUT, $mrtime); // retry timeout in seconds
  61.                 $this->memcache->setOption(Memcached::OPT_COMPRESSION, $mcomp); // Set this to false if you ever start using memcached append
  62.             }
  63.         }
  64.     }
  65.  
  66.     public function __clone() { error_log("Can't clone Mysqli!"); die(); }
  67.  
  68.     public function query($query, $backtrace=array(), $vars=null) {
  69.         // I need this here to check cache so that i dont have to do that crazy thing down below every time before checking for cache
  70.         $this->vars = (is_null($this->vars)) ? $this->clean($GLOBALS) : $this->vars; // globally defined vars
  71.         $qvars = array(); // globally defined vars used in the query
  72.  
  73.         preg_match_all('/\^\^[a-zA-Z0-9\-_]+/m', $query, $matches);
  74.  
  75.         foreach($matches[0] as $var) {
  76.             $var = substr($var, 2);
  77.             if(array_key_exists($var, $this->vars)) {
  78.                 $qvars[$var] = $this->vars[$var];
  79.             }
  80.         }
  81.  
  82.         // Check cache first
  83.         if($this->memcache) {
  84.             $this->key = $query;
  85.             foreach($qvars as $name=>$var) {
  86.                 $this->key = preg_replace("/\^\^".$name."/", $var, $key_query);
  87.             }
  88.             $this->key = "mysql_".md5($this->key);
  89.             $obj = $memcache->get($this->key); //check cache
  90.             if($obj !== false) {
  91.                 $this->data = unserialize($obj);
  92.                 return true;
  93.             }
  94.         }
  95.  
  96.         // Because the point of this function is to prevent inline string concatenation, i am going to check for it.
  97.         // If you remove this section, know that you are playing with fire and making the code insecure, so if anyone asks you to do it
  98.         // DON'T!. I DO NOT ALLOW THE USE OF THIS CODE IF THIS SECTION IS REMOVED! This section may be modified to better fulfill its function
  99.         // which is to detect and prevent concatenation of strings, specifically strings and variables in the line calling this function.
  100.         // BEGIN
  101.  
  102.         if(!is_array($backtrace) || count($backtrace)>=0) { $backtrace = debug_backtrace(); }
  103.         if(array_key_exists(0, $backtrace)) { $backtrace=$backtrace[0]; }
  104.         if(!array_key_exists("file", $backtrace)) { error_log("We don't seem to have the right globals data"); return false; }
  105.  
  106.         $fh = file($backtrace["file"]);
  107.  
  108.         // All this just to deal with multi-line input and convert it to a single line (note line returns the last line in a multi-line split so this reads backwards
  109.         $i=$backtrace["line"]-1;
  110.         $line = "";
  111.         do {
  112.             $line = preg_replace('/\s+/', ' ', $fh[$i].$line);
  113.             $i--;
  114.         } while(!preg_match('/[;}]\s*$/', $fh[$i]) || $i<=0);
  115.  
  116.  
  117.         // This pulls quoted strings from a magic function call (yeah i know that wond do multi-level very well, but it will have to do for now)
  118.         // then it evals it to get the value, basically this is a cheasy way to run it through php interpreter and determine if there is any
  119.         // string concatenation happening there by checking the $ count before and after
  120.         $pre = 0;
  121.         $post = 0;
  122.         if(preg_match_all('/'.$backtrace['function'].'\((.*)\)/', $line, $matches)) {
  123.             foreach($matches[0] as $line) {
  124.                 if(preg_match_all('/["\'].*["\']/', $line, $query_matches)) {
  125.                     foreach($query_matches[0] as $query_match) {
  126.                         $pre = preg_match('/\$/', $query_match);
  127.                         @eval('$post='.$query_match.';'); // This should be safe as it will reference local scope variables which dont exist. and if they do, its programmer's fault and this will still not work in terms of code injection
  128.                         if($pre != preg_match('/\$/', $post)){
  129.                             error_log("There seems to be some string concatenation in the function call in {$backtrace['file']}, at line {$backtrace['line']}, around {$query_match}");
  130.                             return false;
  131.                         }
  132.                     }
  133.                 } else {
  134.                     error_log("invalid file format, please call {$backtrace['function']}(\$this_link, \"query\")");
  135.                     return false;
  136.                 }
  137.             }
  138.         } else {
  139.             error_log("Invalid calling line format");
  140.             return false;
  141.         }
  142.         // END
  143.  
  144.         $bind_str = "";
  145.         $bind_params = array();
  146.         $query = preg_replace('/\s+/', ' ', $query);
  147.  
  148.         foreach($qvars as $var => $val) {
  149.             if(is_null($val)) {
  150.                 if(preg_match('/where.*\^\^'.$var.'/', $query)) {
  151.                     preg_match('/(\s[=<>!\s]*(not|is|like)*)*(\^\^'.$var.')/', $query, $matches);
  152.                     $comp = (preg_match('/(!|not)/',$matches[0])) ? " is not " : " is ";
  153.                     $query = preg_replace('/(\s[=<>!\s]*(not|is|like)*)*(\^\^'.$var.')/', $comp."NULL", $query);
  154.                 } else {
  155.                     $query = preg_replace('/(\^\^'.$var.')/', "?", $query);
  156.                 }
  157.             } else {
  158.                 $query = preg_replace('/(\^\^'.$var.')/', "?", $query);
  159.                 switch($val){
  160.                     case (is_int($val)):
  161.                         $bind_str .= "i";
  162.                         break;
  163.                     case (is_float($val)):
  164.                         $bind_str .= "d";
  165.                         break;
  166.                     default:
  167.                         $bind_str .= "s";
  168.                         break;
  169.                 }
  170.                 array_push($bind_params, &$qvars[$var]);
  171.             }
  172.         }
  173.         // Prepare and execute the query
  174.         if(!$this || !preg_match('/.*mysqli.*/i',get_class($this))) { error_log("Didn't get a legitimate mysqli resource"); return false; }
  175.         $this->result=$this->prepare($query);
  176.         if($this->errno) { error_log($this->error); return false; } // check for mysql errors
  177.         if(count($bind_params)>0) {
  178.             array_unshift($bind_params, $bind_str);
  179.             call_user_func_array(array($this->result,'bind_param'), $bind_params);
  180.             if($this->result->errno) { error_log($this->result->error); return false; } // again
  181.         }
  182.         $this->result->execute();
  183.         if($this->result->errno) { error_log($this->result->error); return false; } // and again
  184.         $this->result->store_result();
  185.  
  186.         // This will bind the result array
  187.         if(!$fields = $this->result->result_metadata()) { error_log("The result contains no data." . $this->result->error); return false; }
  188.         if(!$fields = $fields->fetch_fields()) { error_log("Could not fetch fields: " . $this->result->error); return false;}
  189.         $bind_cmd = '$this->result->bind_result(';
  190.         foreach($fields as $field)  { $bind_cmd .='$this->bind_arr[\''.$field->name.'\'],'; }
  191.         $bind_cmd = substr($bind_cmd, 0, -1).");";
  192.         eval($bind_cmd);
  193.         return true;
  194.     }
  195.  
  196.     private function fetch_array($type=NULL, $prefix=NULL, $postfix=NULL, $join="_", $drop=false) //drop will allow you to drop the pre/postfix
  197.     {
  198.         // This actually magically fetches data
  199.         if($this->result->num_rows <= 0) { error_log("We need one or more results in the result set first."); return false; }
  200.         if(!$this->result->fetch()) return FALSE;
  201.         while (list($key, $val) = each($this->bind_arr))  { $ret[$key] =  $val; }
  202.         reset($this->bind_arr);
  203.         if( is_null($type) || strtoupper($type)=="MYSQLI_NUM" || $type==MYSQLI_NUM) $ret=array_values($ret);
  204.  
  205.         if($prefix || $postfix) {
  206.             if(!$drop) {
  207.                 if($prefix == "MYSQLI_ROW")  { $prefix = $this->row; }
  208.                 if($postfix == "MYSQLI_ROW")  { $postfix = $this->row; }
  209.                 foreach($ret as $key=>$val)  { $ret[(($prefix||$prefix=="0")?$prefix.$join:"").$key.(($postfix||$postfix=="0")?$join.$postfix:"")] = $val; unset($ret[$key]); }
  210.             } else {
  211.                 foreach($ret as $key=>$val)  { $ret[preg_replace('/('.(($prefix||$prefix=="0")?$prefix.$join:"").'|'.(($postfix||$postfix=="0")?$join.$postfix:"").')', '',$key)] = $val; unset($ret[$key]); }
  212.             }
  213.         }
  214.         $this->row++;
  215.         return $ret;
  216.     }
  217.  
  218.     public function fetch_all($type=NULL, $prefix=NULL, $postfix=NULL, $join=NULL, $drop=false)
  219.     {
  220.         if($this->memcache && !empty($this->data)) {
  221.             $data = $this->data;
  222.             unset($this->data);
  223.             return $data;
  224.         }
  225.  
  226.         $tmp = array(); $ret = array(); // It's nice to initilaize arrays
  227.         if($type==MYSQLI_DOC){
  228.             $tmp = $this->fetch_array(MYSQLI_ASSOC,$prefix,$postfix,$join,$drop);
  229.             array_push($ret, array_keys($tmp), array_values($tmp));
  230.             $type=MYSQLI_NUM;
  231.         }
  232.         while($tmp = $this->fetch_array($type,$prefix,$postfix,$join,$drop)) { $ret[] = $tmp; }
  233.        
  234.         $this->result->free_result(); // Free the original result set (note on change, in mysqli query free is aliased to free_result, always free the result as per the documentation
  235.         $this->bind_arr = array();
  236.  
  237.         if($this->memcache) {   // Store data in cache if its not there yet
  238.             $timeout = (defined(MEM_TIMEOUT)) ? MEM_TIMEOUT : 3600 ;
  239.             if(!$memcache->replace($key, serialize($res), $timeout)) {
  240.                 if(!$memcache->add($key, serialize($res),$timeout)) {
  241.                     error_log('Failed to store data in MemCache');
  242.                 }
  243.             }
  244.         }
  245.        
  246.         return $ret;
  247.     }
  248.  
  249.     public function get_csv($type=NULL, $prefix=NULL, $postfix=NULL, $join=NULL, $drop=false)
  250.     {
  251.         $stream = fopen("php://output", 'w');
  252.         if($this->memcache && !empty($this->data)) {
  253.             $this->arr_csv($this->data, $stream);
  254.         } else {
  255.             $tmp=array();
  256.             while($tmp = $this->fetch_array($type,$prefix,$postfix,$join,$drop)) { $this->arr_csv($tmp, $stream); }
  257.         }
  258.         fclose($stream);
  259.     }
  260.  
  261.     public function arr_csv($data, $stream)
  262.     {
  263.         if(!is_array($data) || gettype($stream) != "resource") { return false; }
  264.         if(is_array($data[0])) { array_walk($data, get_class($this).'::outputCSV', $stream); }
  265.             else { $this->output_csv($data, null, $stream); }
  266.     }
  267.    
  268.     public function num_rows()
  269.     {
  270.         return $this->result->num_rows;
  271.     }
  272. }
Advertisement
Add Comment
Please, Sign In to add comment