Source of trunk/safe_eval.php at revision 424 (04/21/2009 8:04:49, 7522 bytes, 242 lines, language: php) [download]:
| 1 | <?php |
| 2 | |
| 3 | /* |
| 4 | * Safe Eval Functions |
| 5 | |
| 6 | * Copyright (c) 2009 Codewise.org |
| 7 | */ |
| 8 | |
| 9 | /* |
| 10 | * !!! WARNING !!! |
| 11 | * This file initializes its function blacklist at include-time, so be sure |
| 12 | * to only include this file ONLY AFTER all other functions have been declared. |
| 13 | */ |
| 14 | |
| 15 | $safe_eval_cache = array(); |
| 16 | |
| 17 | list($safe_eval_blacklist_funcs, $safe_eval_whitelist_vars) = safe_eval_init(); |
| 18 | |
| 19 | /* |
| 20 | ** Builds the list of allowed and disallowed things in eval expressions. |
| 21 | ** |
| 22 | ** Returns an array with the following contents: |
| 23 | ** 0 => blacklisted functions |
| 24 | ** This is a list of all defined functions, minus the ones in the |
| 25 | ** whitelist. |
| 26 | ** 1 => whitelisted vars |
| 27 | ** This is a list of superglobal variables to allow. All others |
| 28 | ** should be blocked. |
| 29 | */ |
| 30 | function safe_eval_init() |
| 31 | { |
| 32 | // which superglobals do we want to allow access to? |
| 33 | $allowed_vars = array("_GET", "_POST", "_SERVER", "_REQUEST", "_FILES", |
| 34 | "_COOKIE"); |
| 35 | |
| 36 | // language constructs to disallow |
| 37 | // these need to be explicitly specified since they're not included in the |
| 38 | // get_defined_functions() list |
| 39 | $blacklist = array("++", "--", "=", "+=", "-=", "*=", "/=", ".=", "%=", |
| 40 | "&=", "|=", "^-", "<<=", ">>=", "include", "require", "if", "else", |
| 41 | "while", "for", "switch", "exit", "break", "print", "echo"); |
| 42 | |
| 43 | // which functions do we want to allow? |
| 44 | $whitelist = array("count", "isset", "substr", "str_replace", "htmlentities", "html_entity_decode"); |
| 45 | |
| 46 | $funcs = get_defined_functions(); |
| 47 | $funcs = array_merge($funcs['internal'], $funcs['user']); |
| 48 | |
| 49 | foreach ($whitelist as $entry) { |
| 50 | unset($funcs[array_search($entry, $funcs)]); |
| 51 | } |
| 52 | |
| 53 | $blacklist = array_merge($blacklist, $funcs); |
| 54 | |
| 55 | return array($blacklist, $allowed_vars); |
| 56 | } |
| 57 | |
| 58 | /* |
| 59 | ** Perform a safe eval() call |
| 60 | ** |
| 61 | ** $code is the code to eval |
| 62 | ** $environment is an array ('name' => $value) of variables to allow $code |
| 63 | ** to access |
| 64 | */ |
| 65 | function safe_eval($code, $environment = array()) |
| 66 | { |
| 67 | global $safe_eval_cache, $safe_eval_blacklist_funcs, $safe_eval_whitelist_vars; |
| 68 | |
| 69 | // munge the code for easy checking |
| 70 | $stripped_code = safe_eval_strip_code($code); |
| 71 | |
| 72 | foreach ($safe_eval_blacklist_funcs as $bad) { |
| 73 | if (($where = strpos($stripped_code, " $bad ")) !== false) { |
| 74 | echo "code has blacklisted function/construct $bad at character $where\n"; |
| 75 | echo "<pre>".htmlspecialchars($code)."</pre>"; |
| 76 | exit; |
| 77 | } |
| 78 | } |
| 79 | |
| 80 | $allowed_vars = array_merge($safe_eval_whitelist_vars, array_keys($environment)); |
| 81 | |
| 82 | //foreach (array_keys($GLOBALS) as $global) { |
| 83 | // if (strpos($global, "_") !== 0) // only care about superglobals |
| 84 | // continue; |
| 85 | foreach (array("_ENV", "_POST", "_GET", "_COOKIE", "_SERVER", "_FILES", |
| 86 | "_REQUEST", "_SESSION") as $global) { |
| 87 | if (in_array($globals, $allowed_vars)) |
| 88 | continue; |
| 89 | if (($where = strpos($stripped_code, " \$bad ")) !== false) { |
| 90 | echo "code has blacklisted variable $bad at character $where"; |
| 91 | echo "<pre>".htmlspecialchars($code)."</pre>"; |
| 92 | exit; |
| 93 | } |
| 94 | } |
| 95 | |
| 96 | // if the code passed, save it so we don't have to re-munge it next time |
| 97 | $safe_eval_cache[$code] = true; |
| 98 | |
| 99 | // import the environment |
| 100 | foreach ($environment as $name => $value) { |
| 101 | $$name = $value; |
| 102 | } |
| 103 | |
| 104 | return eval($code); |
| 105 | } |
| 106 | |
| 107 | /* |
| 108 | * This elaborate state machine's purpose is to strip out all non-symbolic |
| 109 | * text in the given piece of code so the safe_eval function can search it for |
| 110 | * badness. |
| 111 | * |
| 112 | * It returns a string with all constructs/functions/variables separated by |
| 113 | * whitespace. Variables start with '$'. |
| 114 | * |
| 115 | * Example: "foreach (glob("plugins/*.php{$var['hax']} $foo" as $plugin) {" |
| 116 | * becomes: " foreach glob $db $var $plugin " |
| 117 | */ |
| 118 | function safe_eval_strip_code($code) |
| 119 | { |
| 120 | $stripped_code = ""; |
| 121 | $state = array("not in str"); |
| 122 | for ($i = 0; $i < strlen($code); $i++) { |
| 123 | switch ($state[count($state) - 1]) { |
| 124 | case "not in str": |
| 125 | case "dq braced var": |
| 126 | switch ($code[$i]) { |
| 127 | case "\\": |
| 128 | $i++; |
| 129 | break; |
| 130 | case "\"": |
| 131 | array_push($state, "dqstr"); |
| 132 | $stripped_code .= " "; |
| 133 | break; |
| 134 | case "'": |
| 135 | array_push($state, "sqstr"); |
| 136 | $stripped_code .= " "; |
| 137 | break; |
| 138 | case "+": |
| 139 | if ($code[$i+1] == "+") { |
| 140 | $i++; |
| 141 | $stripped_code .= " ++ "; |
| 142 | } else |
| 143 | $stripped_code .= " "; |
| 144 | break; |
| 145 | case "-": |
| 146 | if ($code[$i+1] == "-") { |
| 147 | $i++; |
| 148 | $stripped_code .= " -- "; |
| 149 | } else |
| 150 | $stripped_code .= " "; |
| 151 | break; |
| 152 | case "=": |
| 153 | $operator = "="; |
| 154 | if ($code[$i+1] != "=") { |
| 155 | // continue to the end of the operator and then work back |
| 156 | // they all end with '=' |
| 157 | for ($j = $i - 1; $j >= 0; $j--) { |
| 158 | if (preg_match("#[<>^!=+/*%&-]#", $code[$j])) { |
| 159 | $operator = $code[$j].$operator; |
| 160 | } else { |
| 161 | $stripped_code .= " $operator "; |
| 162 | break; |
| 163 | } |
| 164 | } |
| 165 | } |
| 166 | break; |
| 167 | case "}": |
| 168 | if ($state[count($state) - 1] == "dq braced var") |
| 169 | array_pop($state); |
| 170 | case "{": |
| 171 | case "?": |
| 172 | case ":": |
| 173 | case "(": |
| 174 | case ")": |
| 175 | case "[": |
| 176 | case "]": |
| 177 | case "|": |
| 178 | case "&": |
| 179 | case "!": |
| 180 | case "~": |
| 181 | case "^": |
| 182 | case "<": |
| 183 | case ">": |
| 184 | case "%": |
| 185 | case ".": |
| 186 | case ";": |
| 187 | case "\n": |
| 188 | case "\t": |
| 189 | $stripped_code .= " "; |
| 190 | break; |
| 191 | default: |
| 192 | if (!preg_match("/[0-9]/", $code[$i])) |
| 193 | $stripped_code .= $code[$i]; |
| 194 | break; |
| 195 | } |
| 196 | break; |
| 197 | case "dqstr": |
| 198 | switch ($code[$i]) { |
| 199 | case "\\": |
| 200 | $i++; |
| 201 | break; |
| 202 | case "$": |
| 203 | array_push($state, "dq var"); |
| 204 | $stripped_code .= "$"; |
| 205 | break; |
| 206 | case "{": |
| 207 | if ($code[++$i] == "$") { |
| 208 | array_push($state, "dq braced var"); |
| 209 | $stripped_code .= "$"; |
| 210 | } |
| 211 | break; |
| 212 | case "\"": |
| 213 | array_pop($state); |
| 214 | break; |
| 215 | } |
| 216 | break; |
| 217 | case "sqstr": |
| 218 | switch ($code[$i]) { |
| 219 | case "\\": |
| 220 | $i++; |
| 221 | break; |
| 222 | case "'": |
| 223 | array_pop($state); |
| 224 | break; |
| 225 | } |
| 226 | break; |
| 227 | case "dq var": |
| 228 | if ($code[$i-1] == "$" ? !preg_match("/[a-zA-Z_\x7f-\xff]/", $code[$i]) : !preg_match("/[a-zA-Z0-9_\x7f-\xff]/", $code[$i])) { |
| 229 | array_pop($state); |
| 230 | $stripped_code .= " "; |
| 231 | } else { |
| 232 | $stripped_code .= $code[$i]; |
| 233 | } |
| 234 | break; |
| 235 | } |
| 236 | } |
| 237 | |
| 238 | return " $stripped_code "; |
| 239 | } |
| 240 | |
| 241 | ?> |
| 242 |