当前位置:Gxlcms > 数据库问题 > php-sql-parser sql防注入脚本

php-sql-parser sql防注入脚本

时间:2021-07-01 10:21:17 帮助过:18人阅读

/** * SQL Parser from: http://code.google.com/p/php-sql-parser/ * License: New BSD */ class PHPSQLParser { var $reserved = array(); var $functions = array(); function __construct($sql = false) { #LOAD THE LIST OF RESERVED WORDS $this->load_reserved_words(); if($sql) $this->parse($sql); } function parse($sql) { $sql = trim($sql); #lex the SQL statement $in = $this->split_sql($sql); #sometimes the parser needs to skip ahead until a particular #token is found $skip_until = false; #this is the output tree which is being parsed $out = array(); #This is the last type of union used (UNION or UNION ALL) #indicates a) presence of at least one union in this query # b) the type of union if this is the first or last query $union = false; #Sometimes a "query" consists of more than one query (like a UNION query) #this array holds all the queries $queries=array(); #This is the highest level lexical analysis. This is the part of the #code which finds UNION and UNION ALL query parts foreach($in as $key => $token) { $token=trim($token); if($skip_until) { if($token) { if(strtoupper($token) == $skip_until) { $skip_until = false; continue; } } else { continue; } } if(strtoupper($token) == "UNION") { $union = ‘UNION‘; for($i=$key+1;$i<count($in);++$i) { if(trim($in[$i]) == ‘‘) continue; if(strtoupper($in[$i]) == ‘ALL‘) { $skip_until = ‘ALL‘; $union = ‘UNION ALL‘; continue ; } else { break; } } $queries[$union][] = $out; $out = array(); } else { $out[]=$token; } } if(!empty($out)) { if ($union) { $queries[$union][] = $out; } else { $queries[] = $out; } } /*MySQL supports a special form of UNION: (select ...) union (select ...) This block handles this query syntax. Only one such subquery is supported in each UNION block. (select)(select)union(select) is not legal. The extra queries will be silently ignored. */ $union_types = array(‘UNION‘,‘UNION ALL‘); foreach($union_types as $union_type) { if(!empty($queries[$union_type])) { foreach($queries[$union_type] as $i => $tok_list) { foreach($tok_list as $z => $tok) { $tok = trim($tok); if(!$tok) continue; if(preg_match(‘/^\\(\\s*select\\s*/i‘, $tok)) { $queries[$union_type][$i] = $this->parse(substr($tok,1,-1)); break; } else { $queries[$union_type][$i] = $this->process_sql($queries[$union_type][$i]); break; } } } } } /* If there was no UNION or UNION ALL in the query, then the query is stored at $queries[0]. */ if(!empty($queries[0])) { $queries[0] = $this->process_sql($queries[0]); } if(count($queries) == 1 && !$union) { $queries = $queries[0]; } $this->parsed = $queries; return $this->parsed; } #This function counts open and close parenthesis and #returns their location. This might be faster as a regex private function count_paren($token,$chars=array(‘(‘,‘)‘)) { $len = strlen($token); $open=array(); $close=array(); for($i=0;$i<$len;++$i){ if($token[$i] == $chars[0]) { $open[] = $i; } elseif($token[$i] == $chars[1]) { $close[] = $i; } } return array(‘open‘ => $open, ‘close‘ => $close, ‘balanced‘ =>( count($close) - count($open))); } #This function counts open and close parenthesis and #returns their location. This might be faster as a regex private function count_backtick($token) { $len = strlen($token); $cnt=0; for($i=0;$i<$len;++$i){ if($token[$i] == ‘`‘) ++$cnt; } return $cnt; } #This is the lexer #this function splits up a SQL statement into easy to "parse" #tokens for the SQL processor private function split_sql($sql) { if(!is_string($sql)) { return false; } $sql = str_replace(array(‘\\\‘‘,‘\\"‘,"\r\n","\n","()"),array("‘‘",‘""‘," "," "," "), $sql); $regex=<<<EOREGEX /(`(?:[^`]|``)`|[@A-Za-z0-9_.`-]+(?:\(\s*\)){0,1}) |(\+|-|\*|\/|!=|>=|<=|<>|>|<|&&|\|\||=|\^) |(\(.*?\)) # Match FUNCTION(...) OR BAREWORDS |(‘(?:[^‘]|‘‘)*‘+) |("(?:[^"]|"")*"+) |([^ ,]+) /ix EOREGEX ; $tokens = preg_split($regex, $sql,-1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); $token_count = count($tokens); /* The above regex has one problem, because the parenthetical match is not greedy. Thus, when matching grouped expressions such as ( (a and b) or c) the tokenizer will produce "( (a and b)", " ", "or", " " , "c,")" This block detects the number of open/close parentheses in the given token. If the parentheses are balanced (balanced == 0) then we don‘t need to do anything. otherwise, we need to balance the expression. */ $reset = false; for($i=0;$i<$token_count;++$i) { if(empty($tokens[$i])) continue; $token = $tokens[$i]; $trim = trim($token); if($trim) { if($trim[0] != ‘(‘ && substr($trim,-1) == ‘)‘) { $trim=trim(substr($trim,0, strpos($trim,‘(‘))); } $tokens[$i]=$trim; $token=$trim; } if($token && $token[0] == ‘(‘) { $info = $this->count_paren($token); if($info[‘balanced‘] == 0) { continue; } // We need to find this many closing parentheses. $needed = abs($info[‘balanced‘]); $n = $i; while($needed > 0 && $n <$token_count-1) { ++$n; #echo "LOOKING FORWARD TO $n [ " . $tokens[$n] . "]\n"; $token2 = $tokens[$n]; $info2 = $this->count_paren($token2); $closes = count($info2[‘close‘]); if($closes != $needed) { $tokens[$i] .= $tokens[$n]; unset($tokens[$n]); $reset = true; $info2 = $this->count_paren($tokens[$i]); $needed = abs($info2[‘balanced‘]); # echo "CLOSES LESS THAN NEEDED (still need $needed)\n"; } else { /*get the string pos of the last close parenthesis we need*/ $pos = $info2[‘close‘][count($info2[‘close‘])-1]; $str1 = $str2 = ""; if($pos == 0) { $str1 = ‘)‘; } else { $str1 = substr($tokens[$n],0,$pos) . ‘)‘; $str2 = substr($tokens[$n],$pos+1); } #echo "CLOSES FOUND AT $n, offset:$pos [$str1] [$str2]\n"; if(strlen($str2) > 0) { $tokens[$n] = $str2; } else { unset($tokens[$n]); $reset = true; } $tokens[$i] .= $str1; $info2 = $this->count_paren($tokens[$i]); $needed = abs($info2[‘balanced‘]); } } } } #the same problem appears with backticks :( /* reset the array if we deleted any tokens above */ if ($reset) $tokens = array_values($tokens); $token_count=count($tokens); for($i=0;$i<$token_count;++$i) { if(empty($tokens[$i])) continue; $token=$tokens[$i]; $needed=true; $reset=false; if($needed && $token && strpos($token,‘`‘) !== false) { $info = $this->count_backtick($token); if($info %2 == 0) { #even number of backticks means we are balanced continue; } $needed=1; $n = $i; while($needed && $n <$token_count-1) { $reset=true; #echo "BACKTICK COUNT[$i]: $info old: {$tokens[$i]}, new: ($token)\n"; ++$n; $token .= $tokens[$n]; unset($tokens[$n]); $needed = $this->count_backtick($token) % 2; } } if($reset) $tokens[$i] = $token; } /* reset the array if we deleted any tokens above */ $tokens = array_values($tokens); return $tokens; } /* This function breaks up the SQL statement into logical sections. Some sections are then further handled by specialized functions. */ private function process_sql(&$tokens,$start_at = 0, $stop_at = false) { $prev_category = ""; $start = microtime(true); $token_category = ""; $skip_next=false; $token_count = count($tokens); if(!$stop_at) { $stop_at = $token_count; } $out = false; for($token_number = $start_at;$token_number<$stop_at;++$token_number) { $token = trim($tokens[$token_number]); if($token && $token[0] == ‘(‘ && $token_category == "") { $token_category = ‘SELECT‘; } /* If it isn‘t obvious, when $skip_next is set, then we ignore the next real token, that is we ignore whitespace. */ if($skip_next) { #whitespace does not count as a next token if($token == "") { continue; } #to skip the token we replace it with whitespace $new_token = ""; $skip_next = false; } $upper = strtoupper($token); switch($upper) { /* Tokens that get their own sections. These keywords have subclauses. */ case ‘SELECT‘: case ‘ORDER‘: case ‘LIMIT‘: case ‘SET‘: case ‘DUPLICATE‘: case ‘VALUES‘: case ‘GROUP‘: case ‘ORDER‘: case ‘HAVING‘: case ‘INTO‘: case ‘WHERE‘: case ‘RENAME‘: case ‘CALL‘: case ‘PROCEDURE‘: case ‘FUNCTION‘: case ‘DATABASE‘: case ‘SERVER‘: case ‘LOGFILE‘: case ‘DEFINER‘: case ‘RETURNS‘: case ‘EVENT‘: case ‘TABLESPACE‘: case ‘VIEW‘: case ‘TRIGGER‘: case ‘DATA‘: case ‘DO‘: case ‘PASSWORD‘: case ‘USER‘: case ‘PLUGIN‘: case ‘FROM‘: case ‘FLUSH‘: case ‘KILL‘: case ‘RESET‘: case ‘START‘: case ‘STOP‘: case ‘PURGE‘: case ‘EXECUTE‘: case ‘PREPARE‘: case ‘DEALLOCATE‘: if($token == ‘DEALLOCATE‘) { $skip_next = true; } /* this FROM is different from FROM in other DML (not join related) */ if($token_category == ‘PREPARE‘ && $upper == ‘FROM‘) { continue 2; } $token_category = $upper; #$join_type = ‘JOIN‘; if($upper == ‘FROM‘ && $token_category == ‘FROM‘) { /* DO NOTHING*/ } else { continue 2; } break; /* These tokens get their own section, but have no subclauses. These tokens identify the statement but have no specific subclauses of their own. */ case ‘DELETE‘: case ‘ALTER‘: case ‘INSERT‘: case ‘REPLACE‘: case ‘TRUNCATE‘: case ‘CREATE‘: case ‘TRUNCATE‘: case ‘OPTIMIZE‘: case ‘GRANT‘: case ‘REVOKE‘: case ‘SHOW‘: case ‘HANDLER‘: case ‘LOAD‘: case ‘ROLLBACK‘: case ‘SAVEPOINT‘: case ‘UNLOCK‘: case ‘INSTALL‘: case ‘UNINSTALL‘: case ‘ANALZYE‘: case ‘BACKUP‘: case ‘CHECK‘: case ‘CHECKSUM‘: case ‘REPAIR‘: case ‘RESTORE‘: case ‘CACHE‘: case ‘DESCRIBE‘: case ‘EXPLAIN‘: case ‘USE‘: case ‘HELP‘: $token_category = $upper; /* set the category in case these get subclauses in a future version of MySQL */ $out[$upper][0] = $upper; continue 2; break; /* This is either LOCK TABLES or SELECT ... LOCK IN SHARE MODE*/ case ‘LOCK‘: if($token_category == "") { $token_category = $upper; $out[$upper][0] = $upper; } else { $token = ‘LOCK IN SHARE MODE‘; $skip_next=true; $out[‘OPTIONS‘][] = $token; } continue 2; break; case ‘USING‘: /* USING in FROM clause is different from USING w/ prepared statement*/ if($token_category == ‘EXECUTE‘) { $token_category=$upper; continue 2; } if($token_category == ‘FROM‘ && !empty($out[‘DELETE‘])) { $token_category=$upper; continue 2; } break; /* DROP TABLE is different from ALTER TABLE DROP ... */ case ‘DROP‘: if($token_category != ‘ALTER‘) { $token_category = $upper; $out[$upper][0] = $upper; continue 2; } break; case ‘FOR‘: $skip_next=true; $out[‘OPTIONS‘][] = ‘FOR UPDATE‘; continue 2; break; case ‘UPDATE‘: if($token_category == "" ) { $token_category = $upper; continue 2; } if($token_category == ‘DUPLICATE‘) { continue 2; } break; break; case ‘START‘: $token = "BEGIN"; $out[$upper][0] = $upper; $skip_next = true; break; /* These tokens are ignored. */ case ‘BY‘: case ‘ALL‘: case ‘SHARE‘: case ‘MODE‘: case ‘TO‘: case ‘;‘: continue 2; break; case ‘KEY‘: if($token_category == ‘DUPLICATE‘) { continue 2; } break; /* These tokens set particular options for the statement. They never stand alone.*/ case ‘DISTINCTROW‘: $token=‘DISTINCT‘; case ‘DISTINCT‘: case ‘HIGH_PRIORITY‘: case ‘LOW_PRIORITY‘: case ‘DELAYED‘: case ‘IGNORE‘: case ‘FORCE‘: case ‘STRAIGHT_JOIN‘: case ‘SQL_SMALL_RESULT‘: case ‘SQL_BIG_RESULT‘: case ‘QUICK‘: case ‘SQL_BUFFER_RESULT‘: case ‘SQL_CACHE‘: case ‘SQL_NO_CACHE‘: case ‘SQL_CALC_FOUND_ROWS‘: $out[‘OPTIONS‘][] = $upper; continue 2; break; case ‘WITH‘: if($token_category == ‘GROUP‘) { $skip_next=true; $out[‘OPTIONS‘][] = ‘WITH ROLLUP‘; continue 2; } break; case ‘AS‘: break; case ‘‘: case ‘,‘: case ‘;‘: break; default: break; } if($prev_category == $token_category) { $out[$token_category][] = $token; } $prev_category = $token_category; } if(!$out) return false; #process the SELECT clause if(!empty($out[‘SELECT‘])) $out[‘SELECT‘] = $this->process_select($out[‘SELECT‘]); if(!empty($out[‘FROM‘])) $out[‘FROM‘] = $this->process_from($out[‘FROM‘]); if(!empty($out[‘USING‘])) $out[‘USING‘] = $this->process_from($out[‘USING‘]); if(!empty($out[‘UPDATE‘])) $out[‘UPDATE‘] = $this->process_from($out[‘UPDATE‘]); if(!empty($out[‘GROUP‘])) $out[‘GROUP‘] = $this->process_group($out[‘GROUP‘], $out[‘SELECT‘]); if(!empty($out[‘ORDER‘])) $out[‘ORDER‘] = $this->process_group($out[‘ORDER‘], $out[‘SELECT‘]); if(!empty($out[‘LIMIT‘])) $out[‘LIMIT‘] = $this->process_limit($out[‘LIMIT‘]); if(!empty($out[‘WHERE‘])) $out[‘WHERE‘] = $this->process_expr_list($out[‘WHERE‘]); if(!empty($out[‘HAVING‘])) $out[‘HAVING‘] = $this->process_expr_list($out[‘HAVING‘]); if(!empty($out[‘SET‘])) $out[‘SET‘] = $this->process_set_list($out[‘SET‘]); if(!empty($out[‘DUPLICATE‘])) { $out[‘ON DUPLICATE KEY UPDATE‘] = $this->process_set_list($out[‘DUPLICATE‘]); unset($out[‘DUPLICATE‘]); } if(!empty($out[‘INSERT‘])) $out = $this->process_insert($out); if(!empty($out[‘REPLACE‘])) $out = $this->process_insert($out,‘REPLACE‘); if(!empty($out[‘DELETE‘])) $out = $this->process_delete($out); return $out; } /* A SET list is simply a list of key = value expressions separated by comma (,). This function produces a list of the key/value expressions. */ private function process_set_list($tokens) { $column=""; $expression=""; foreach($tokens as $token) { $token=trim($token); if(!$column) { if($token ===

人气教程排行