| CODENOTIFIER | HelpYou are not signed inSign in |
Project: Joomla
Revision: 10151
Author: eddieajau
Date: 19 Mar 2008 07:00:49
Changes:Trunk merge
Files:| ... | ...@@ -1,496 +1,506 @@ | |
| 1 | <?php | |
| 2 | /** | |
| 3 | * @version $Id$ | |
| 4 | * @package Joomla.Framework | |
| 5 | * @subpackage Filter | |
| 6 | * @copyright Copyright (C) 2005 - 2008 Open Source Matters. All rights reserved. | |
| 7 | * @license GNU/GPL, see LICENSE.php | |
| 8 | * Joomla! is free software. This version may have been modified pursuant to the | |
| 9 | * GNU General Public License, and as distributed it includes or is derivative | |
| 10 | * of works licensed under the GNU General Public License or other free or open | |
| 11 | * source software licenses. See COPYRIGHT.php for copyright notices and | |
| 12 | * details. | |
| 13 | */ | |
| 14 | ||
| 15 | // Check to ensure this file is within the rest of the framework | |
| 16 | defined('JPATH_BASE') or die(); | |
| 17 | ||
| 18 | /** | |
| 19 | * JFilterInput is a class for filtering input from any data source | |
| 20 | * | |
| 21 | * Forked from the php input filter library by: Daniel Morris <dan@rootcube.com> | |
| 22 | * Original Contributors: Gianpaolo Racca, Ghislain Picard, Marco Wandschneider, Chris Tobin and Andrew Eddie. | |
| 23 | * | |
| 24 | * @author Louis Landry <louis.landry@joomla.org> | |
| 25 | * @package Joomla.Framework | |
| 26 | * @subpackage Filter | |
| 27 | * @since 1.5 | |
| 28 | */ | |
| 29 | class JFilterInput extends JObject | |
| 30 | { | |
| 31 | var $tagsArray; // default = empty array | |
| 32 | var $attrArray; // default = empty array | |
| 33 | ||
| 34 | var $tagsMethod; // default = 0 | |
| 35 | var $attrMethod; // default = 0 | |
| 36 | ||
| 37 | var $xssAuto; // default = 1 | |
| 38 | var $tagBlacklist = array ('applet', 'body', 'bgsound', 'base', 'basefont', 'embed', 'frame', 'frameset', 'head', 'html', 'id', 'iframe', 'ilayer', 'layer', 'link', 'meta', 'name', 'object', 'script', 'style', 'title', 'xml'); | |
| 39 | var $attrBlacklist = array ('action', 'background', 'codebase', 'dynsrc', 'lowsrc'); // also will strip ALL event handlers | |
| 40 | ||
| 41 | /** | |
| 42 | * Constructor for inputFilter class. Only first parameter is required. | |
| 43 | * | |
| 44 | * @access protected | |
| 45 | * @param array $tagsArray list of user-defined tags | |
| 46 | * @param array $attrArray list of user-defined attributes | |
| 47 | * @param int $tagsMethod WhiteList method = 0, BlackList method = 1 | |
| 48 | * @param int $attrMethod WhiteList method = 0, BlackList method = 1 | |
| 49 | * @param int $xssAuto Only auto clean essentials = 0, Allow clean blacklisted tags/attr = 1 | |
| 50 | * @since 1.5 | |
| 51 | */ | |
| 52 | function __construct($tagsArray = array(), $attrArray = array(), $tagsMethod = 0, $attrMethod = 0, $xssAuto = 1) | |
| 53 | { | |
| 54 | // Make sure user defined arrays are in lowercase | |
| 55 | $tagsArray = array_map('strtolower', (array) $tagsArray); | |
| 56 | $attrArray = array_map('strtolower', (array) $attrArray); | |
| 57 | ||
| 58 | // Assign member variables | |
| 59 | $this->tagsArray = $tagsArray; | |
| 60 | $this->attrArray = $attrArray; | |
| 61 | $this->tagsMethod = $tagsMethod; | |
| 62 | $this->attrMethod = $attrMethod; | |
| 63 | $this->xssAuto = $xssAuto; | |
| 64 | } | |
| 65 | ||
| 66 | /** | |
| 67 | * Returns a reference to an input filter object, only creating it if it doesn't already exist. | |
| 68 | * | |
| 69 | * This method must be invoked as: | |
| 70 | * <pre> $filter = & JFilterInput::getInstance();</pre> | |
| 71 | * | |
| 72 | * @static | |
| 73 | * @param array $tagsArray list of user-defined tags | |
| 74 | * @param array $attrArray list of user-defined attributes | |
| 75 | * @param int $tagsMethod WhiteList method = 0, BlackList method = 1 | |
| 76 | * @param int $attrMethod WhiteList method = 0, BlackList method = 1 | |
| 77 | * @param int $xssAuto Only auto clean essentials = 0, Allow clean blacklisted tags/attr = 1 | |
| 78 | * @return object The JFilterInput object. | |
| 79 | * @since 1.5 | |
| 80 | */ | |
| 81 | function & getInstance($tagsArray = array(), $attrArray = array(), $tagsMethod = 0, $attrMethod = 0, $xssAuto = 1) | |
| 82 | { | |
| 83 | static $instances; | |
| 84 | ||
| 85 | $sig = md5(serialize(array($tagsArray,$attrArray,$tagsMethod,$attrMethod,$xssAuto))); | |
| 86 | ||
| 87 | if (!isset ($instances)) { | |
| 88 | $instances = array(); | |
| 89 | } | |
| 90 | ||
| 91 | if (empty ($instances[$sig])) { | |
| 92 | $instances[$sig] = new JFilterInput($tagsArray, $attrArray, $tagsMethod, $attrMethod, $xssAuto); | |
| 93 | } | |
| 94 | ||
| 95 | return $instances[$sig]; | |
| 96 | } | |
| 97 | ||
| 98 | /** | |
| 99 | * Method to be called by another php script. Processes for XSS and | |
| 100 | * specified bad code. | |
| 101 | * | |
| 102 | * @access public | |
| 103 | * @param mixed $source Input string/array-of-string to be 'cleaned' | |
| 104 | * @param string $type Return type for the variable (INT, FLOAT, BOOLEAN, WORD, ALNUM, CMD, BASE64, STRING, ARRAY, PATH, NONE) | |
| 105 | * @return mixed 'Cleaned' version of input parameter | |
| 106 | * @since 1.5 | |
| 107 | * @static | |
| 108 | */ | |
| 109 | function clean($source, $type='string') | |
| 110 | { | |
| 111 | // Handle the type constraint | |
| 112 | switch (strtoupper($type)) | |
| 113 | { | |
| 114 | case 'INT' : | |
| 115 | case 'INTEGER' : | |
| 116 | // Only use the first integer value | |
| 117 | preg_match('/-?[0-9]+/', (string) $source, $matches); | |
| 118 | $result = @ (int) $matches[0]; | |
| 119 | break; | |
| 120 | ||
| 121 | case 'FLOAT' : | |
| 122 | case 'DOUBLE' : | |
| 123 | // Only use the first floating point value | |
| 124 | preg_match('/-?[0-9]+(\.[0-9]+)?/', (string) $source, $matches); | |
| 125 | $result = @ (float) $matches[0]; | |
| 126 | break; | |
| 127 | ||
| 128 | case 'BOOL' : | |
| 129 | case 'BOOLEAN' : | |
| 130 | $result = (bool) $source; | |
| 131 | break; | |
| 132 | ||
| 133 | case 'WORD' : | |
| 134 | $result = (string) preg_replace( '/[^A-Z_]/i', '', $source ); | |
| 135 | break; | |
| 136 | ||
| 137 | case 'ALNUM' : | |
| 138 | $result = (string) preg_replace( '/[^A-Z0-9]/i', '', $source ); | |
| 139 | break; | |
| 140 | ||
| 141 | case 'CMD' : | |
| 142 | $result = (string) preg_replace( '/[^A-Z0-9_\.-]/i', '', $source ); | |
| 143 | $result = ltrim($result, '.'); | |
| 144 | break; | |
| 145 | ||
| 146 | case 'BASE64' : | |
| 147 | $result = (string) preg_replace( '/[^A-Z0-9\/+=]/i', '', $source ); | |
| 148 | break; | |
| 149 | ||
| 150 | case 'STRING' : | |
| 151 | $filter = JFilterInput::getInstance(); | |
| 152 | $result = (string) $filter->_remove($filter->_decode((string) $source)); | |
| 153 | break; | |
| 154 | ||
| 155 | case 'ARRAY' : | |
| 156 | $result = (array) $source; | |
| 157 | break; | |
| 158 | ||
| 159 | case 'PATH' : | |
| 160 | $pattern = '/^[A-Za-z0-9_-]+[A-Za-z0-9_\.-]*([\\\\\/][A-Za-z0-9_-]+[A-Za-z0-9_\.-]*)*$/'; | |
| 161 | preg_match($pattern, (string) $source, $matches); | |
| 162 | $result = @ (string) $matches[0]; | |
| 163 | break; | |
| 164 | ||
| 165 | case 'USERNAME' : | |
| 166 | $result = (string) preg_replace( '/[\x00-\x1F\x7F<>"\'%&]/', '', $source ); | |
| 167 | break; | |
| 168 | ||
| 169 | default : | |
| 170 | // Are we dealing with an array? | |
| 171 | $filter = JFilterInput::getInstance(); | |
| 172 | if (is_array($source)) { | |
| 173 | foreach ($source as $key => $value) | |
| 174 | { | |
| 175 | // filter element for XSS and other 'bad' code etc. | |
| 176 | if (is_string($value)) { | |
| 177 | $source[$key] = $filter->_remove($filter->_decode($value)); | |
| 178 | } | |
| 179 | } | |
| 180 | $result = $source; | |
| 181 | } else { | |
| 182 | // Or a string? | |
| 183 | if (is_string($source) && !empty ($source)) { | |
| 184 | // filter source for XSS and other 'bad' code etc. | |
| 185 | $result = $filter->_remove($filter->_decode($source)); | |
| 186 | } else { | |
| 187 | // Not an array or string.. return the passed parameter | |
| 188 | $result = $source; | |
| 189 | } | |
| 190 | } | |
| 191 | break; | |
| 192 | } | |
| 193 | return $result; | |
| 194 | } | |
| 195 | ||
| 196 | /** | |
| 197 | * Function to determine if contents of an attribute is safe | |
| 198 | * | |
| 199 | * @static | |
| 200 | * @param array $attrSubSet A 2 element array for attributes name,value | |
| 201 | * @return boolean True if bad code is detected | |
| 202 | * @since 1.5 | |
| 203 | */ | |
| 204 | function checkAttribute($attrSubSet) | |
| 205 | { | |
| 206 | $attrSubSet[0] = strtolower($attrSubSet[0]); | |
| 207 | $attrSubSet[1] = strtolower($attrSubSet[1]); | |
| 208 | return (((strpos($attrSubSet[1], 'expression') !== false) && ($attrSubSet[0]) == 'style') || (strpos($attrSubSet[1], 'javascript:') !== false) || (strpos($attrSubSet[1], 'behaviour:') !== false) || (strpos($attrSubSet[1], 'vbscript:') !== false) || (strpos($attrSubSet[1], 'mocha:') !== false) || (strpos($attrSubSet[1], 'livescript:') !== false)); | |
| 209 | } | |
| 210 | ||
| 211 | /** | |
| 212 | * Internal method to iteratively remove all unwanted tags and attributes | |
| 213 | * | |
| 214 | * @access protected | |
| 215 | * @param string $source Input string to be 'cleaned' | |
| 216 | * @return string 'Cleaned' version of input parameter | |
| 217 | * @since 1.5 | |
| 218 | */ | |
| 219 | function _remove($source) | |
| 220 | { | |
| 221 | $loopCounter = 0; | |
| 222 | ||
| 223 | // Iteration provides nested tag protection | |
| 224 | while ($source != $this->_cleanTags($source)) | |
| 225 | { | |
| 226 | $source = $this->_cleanTags($source); | |
| 227 | $loopCounter ++; | |
| 228 | } | |
| 229 | return $source; | |
| 230 | } | |
| 231 | ||
| 232 | /** | |
| 233 | * Internal method to strip a string of certain tags | |
| 234 | * | |
| 235 | * @access protected | |
| 236 | * @param string $source Input string to be 'cleaned' | |
| 237 | * @return string 'Cleaned' version of input parameter | |
| 238 | * @since 1.5 | |
| 239 | */ | |
| 240 | function _cleanTags($source) | |
| 241 | { | |
| 242 | /* | |
| 243 | * In the beginning we don't really have a tag, so everything is | |
| 244 | * postTag | |
| 245 | */ | |
| 246 | $preTag = null; | |
| 247 | $postTag = $source; | |
| 248 | $currentSpace = false; | |
| 249 | $attr = ''; // moffats: setting to null due to issues in migration system - undefined variable errors | |
| 250 | ||
| 251 | // Is there a tag? If so it will certainly start with a '<' | |
| 252 | $tagOpen_start = strpos($source, '<'); | |
| 253 | ||
| 254 | while ($tagOpen_start !== false) | |
| 255 | { | |
| 256 | // Get some information about the tag we are processing | |
| 257 | $preTag .= substr($postTag, 0, $tagOpen_start); | |
| 258 | $postTag = substr($postTag, $tagOpen_start); | |
| 259 | $fromTagOpen = substr($postTag, 1); | |
| 260 | $tagOpen_end = strpos($fromTagOpen, '>'); | |
| 261 | ||
| 262 | // Let's catch any non-terminated tags and skip over them | |
| 263 | if ($tagOpen_end === false) { | |
| 264 | $postTag = substr($postTag, $tagOpen_start +1); | |
| 265 | $tagOpen_start = strpos($postTag, '<'); | |
| 266 | continue; | |
| 267 | } | |
| 268 | ||
| 269 | // Do we have a nested tag? | |
| 270 | $tagOpen_nested = strpos($fromTagOpen, '<'); | |
| 271 | $tagOpen_nested_end = strpos(substr($postTag, $tagOpen_end), '>'); | |
| 272 | if (($tagOpen_nested !== false) && ($tagOpen_nested < $tagOpen_end)) { | |
| 273 | $preTag .= substr($postTag, 0, ($tagOpen_nested +1)); | |
| 274 | $postTag = substr($postTag, ($tagOpen_nested +1)); | |
| 275 | $tagOpen_start = strpos($postTag, '<'); | |
| 276 | continue; | |
| 277 | } | |
| 278 | ||
| 279 | // Lets get some information about our tag and setup attribute pairs | |
| 280 | $tagOpen_nested = (strpos($fromTagOpen, '<') + $tagOpen_start +1); | |
| 281 | $currentTag = substr($fromTagOpen, 0, $tagOpen_end); | |
| 282 | $tagLength = strlen($currentTag); | |
| 283 | $tagLeft = $currentTag; | |
| 284 | $attrSet = array (); | |
| 285 | $currentSpace = strpos($tagLeft, ' '); | |
| 286 | ||
| 287 | // Are we an open tag or a close tag? | |
| 288 | if (substr($currentTag, 0, 1) == '/') { | |
| 289 | // Close Tag | |
| 290 | $isCloseTag = true; | |
| 291 | list ($tagName) = explode(' ', $currentTag); | |
| 292 | $tagName = substr($tagName, 1); | |
| 293 | } else { | |
| 294 | // Open Tag | |
| 295 | $isCloseTag = false; | |
| 296 | list ($tagName) = explode(' ', $currentTag); | |
| 297 | } | |
| 298 | ||
| 299 | /* | |
| 300 | * Exclude all "non-regular" tagnames | |
| 301 | * OR no tagname | |
| 302 | * OR remove if xssauto is on and tag is blacklisted | |
| 303 | */ | |
| 304 | if ((!preg_match("/^[a-z][a-z0-9]*$/i", $tagName)) || (!$tagName) || ((in_array(strtolower($tagName), $this->tagBlacklist)) && ($this->xssAuto))) { | |
| 305 | $postTag = substr($postTag, ($tagLength +2)); | |
| 306 | $tagOpen_start = strpos($postTag, '<'); | |
| 307 | // Strip tag | |
| 308 | continue; | |
| 309 | } | |
| 310 | ||
| 311 | /* | |
| 312 | * Time to grab any attributes from the tag... need this section in | |
| 313 | * case attributes have spaces in the values. | |
| 314 | */ | |
| 315 | while ($currentSpace !== false) | |
| 316 | { | |
| 317 | $fromSpace = substr($tagLeft, ($currentSpace +1)); | |
| 318 | $nextSpace = strpos($fromSpace, ' '); | |
| 319 | $openQuotes = strpos($fromSpace, '"'); | |
| 320 | $closeQuotes = strpos(substr($fromSpace, ($openQuotes +1)), '"') + $openQuotes +1; | |
| 321 | ||
| 322 | // Do we have an attribute to process? [check for equal sign] | |
| 323 | if (strpos($fromSpace, '=') !== false) { | |
| 324 | /* | |
| 325 | * If the attribute value is wrapped in quotes we need to | |
| 326 | * grab the substring from the closing quote, otherwise grab | |
| 327 | * till the next space | |
| 328 | */ | |
| 329 | if (($openQuotes !== false) && (strpos(substr($fromSpace, ($openQuotes +1)), '"') !== false)) { | |
| 330 | $attr = substr($fromSpace, 0, ($closeQuotes +1)); | |
| 331 | } else { | |
| 332 | $attr = substr($fromSpace, 0, $nextSpace); | |
| 333 | } | |
| 334 | } else { | |
| 335 | /* | |
| 336 | * No more equal signs so add any extra text in the tag into | |
| 337 | * the attribute array [eg. checked] | |
| 338 | */ | |
| 339 | if ($fromSpace != '/') { | |
| 340 | $attr = substr($fromSpace, 0, $nextSpace); | |
| 341 | } | |
| 342 | } | |
| 343 | ||
| 344 | // Last Attribute Pair | |
| 345 | if (!$attr && $fromSpace != '/') { | |
| 346 | $attr = $fromSpace; | |
| 347 | } | |
| 348 | ||
| 349 | // Add attribute pair to the attribute array | |
| 350 | $attrSet[] = $attr; | |
| 351 | ||
| 352 | // Move search point and continue iteration | |
| 353 | $tagLeft = substr($fromSpace, strlen($attr)); | |
| 354 | $currentSpace = strpos($tagLeft, ' '); | |
| 355 | } | |
| 356 | ||
| 357 | // Is our tag in the user input array? | |
| 358 | $tagFound = in_array(strtolower($tagName), $this->tagsArray); | |
| 359 | ||
| 360 | // If the tag is allowed lets append it to the output string | |
| 361 | if ((!$tagFound && $this->tagsMethod) || ($tagFound && !$this->tagsMethod)) { | |
| 362 | ||
| 363 | // Reconstruct tag with allowed attributes | |
| 364 | if (!$isCloseTag) { | |
| 365 | // Open or Single tag | |
| 366 | $attrSet = $this->_cleanAttributes($attrSet); | |
| 367 | $preTag .= '<'.$tagName; | |
| 368 | for ($i = 0; $i < count($attrSet); $i ++) | |
| 369 | { | |
| 370 | $preTag .= ' '.$attrSet[$i]; | |
| 371 | } | |
| 372 | ||
| 373 | // Reformat single tags to XHTML | |
| 374 | if (strpos($fromTagOpen, '</'.$tagName)) { | |
| 375 | $preTag .= '>'; | |
| 376 | } else { | |
| 377 | $preTag .= ' />'; | |
| 378 | } | |
| 379 | } else { | |
| 380 | // Closing Tag | |
| 381 | $preTag .= '</'.$tagName.'>'; | |
| 382 | } | |
| 383 | } | |
| 384 | ||
| 385 | // Find next tag's start and continue iteration | |
| 386 | $postTag = substr($postTag, ($tagLength +2)); | |
| 387 | $tagOpen_start = strpos($postTag, '<'); | |
| 388 | } | |
| 389 | ||
| 390 | // Append any code after the end of tags and return | |
| 391 | if ($postTag != '<') { | |
| 392 | $preTag .= $postTag; | |
| 393 | } | |
| 394 | return $preTag; | |
| 395 | } | |
| 396 | ||
| 397 | /** | |
| 398 | * Internal method to strip a tag of certain attributes | |
| 399 | * | |
| 400 | * @access protected | |
| 401 | * @param array $attrSet Array of attribute pairs to filter | |
| 402 | * @return array Filtered array of attribute pairs | |
| 403 | * @since 1.5 | |
| 404 | */ | |
| 405 | function _cleanAttributes($attrSet) | |
| 406 | { | |
| 407 | // Initialize variables | |
| 408 | $newSet = array(); | |
| 409 | ||
| 410 | // Iterate through attribute pairs | |
| 411 | for ($i = 0; $i < count($attrSet); $i ++) | |
| 412 | { | |
| 413 | // Skip blank spaces | |
| 414 | if (!$attrSet[$i]) { | |
| 415 | continue; | |
| 416 | } | |
| 417 | ||
| 418 | // Split into name/value pairs | |
| 419 | $attrSubSet = explode('=', trim($attrSet[$i]), 2); | |
| 420 | list ($attrSubSet[0]) = explode(' ', $attrSubSet[0]); | |
| 421 | ||
| 422 | /* | |
| 423 | * Remove all "non-regular" attribute names | |
| 424 | * AND blacklisted attributes | |
| 425 | */ | |
| 426 | if ((!preg_match('/[a-z]*$/i', $attrSubSet[0])) || (($this->xssAuto) && ((in_array(strtolower($attrSubSet[0]), $this->attrBlacklist)) || (substr($attrSubSet[0], 0, 2) == 'on')))) { | |
| 427 | continue; | |
| 428 | } | |
| 429 | ||
| 430 | // XSS attribute value filtering | |
| 431 | if ($attrSubSet[1]) { | |
| 432 | // strips unicode, hex, etc | |
| 433 | $attrSubSet[1] = str_replace('&#', '', $attrSubSet[1]); | |
| 434 | // strip normal newline within attr value | |
| 435 | $attrSubSet[1] = preg_replace('/\s+/', '', $attrSubSet[1]); | |
| 436 | // strip double quotes | |
| 437 | $attrSubSet[1] = str_replace('"', '', $attrSubSet[1]); | |
| 438 | // convert single quotes from either side to doubles (Single quotes shouldn't be used to pad attr value) | |
| 439 | if ((substr($attrSubSet[1], 0, 1) == "'") && (substr($attrSubSet[1], (strlen($attrSubSet[1]) - 1), 1) == "'")) { | |
| 440 | $attrSubSet[1] = substr($attrSubSet[1], 1, (strlen($attrSubSet[1]) - 2)); | |
| 441 | } | |
| 442 | // strip slashes | |
| 443 | $attrSubSet[1] = stripslashes($attrSubSet[1]); | |
| 444 | } | |
| 445 | ||
| 446 | // Autostrip script tags | |
| 447 | if (JFilterInput::checkAttribute($attrSubSet)) { | |
| 448 | continue; | |
| 449 | } | |
| 450 | ||
| 451 | // Is our attribute in the user input array? | |
| 452 | $attrFound = in_array(strtolower($attrSubSet[0]), $this->attrArray); | |
| 453 | ||
| 454 | // If the tag is allowed lets keep it | |
| 455 | if ((!$attrFound && $this->attrMethod) || ($attrFound && !$this->attrMethod)) { | |
| 456 | ||
| 457 | // Does the attribute have a value? | |
| 458 | if ($attrSubSet[1]) { | |
| 459 | $newSet[] = $attrSubSet[0].'="'.$attrSubSet[1].'"'; | |
| 460 | } elseif ($attrSubSet[1] == "0") { | |
| 461 | /* | |
| 462 | * Special Case | |
| 463 | * Is the value 0? | |
| 464 | */ | |
| 465 | $newSet[] = $attrSubSet[0].'="0"'; | |
| 466 | } else { | |
| 467 | $newSet[] = $attrSubSet[0].'="'.$attrSubSet[0].'"'; | |
| 468 | } | |
| 469 | } | |
| 470 | } | |
| 471 | return $newSet; | |
| 472 | } | |
| 473 | ||
| 474 | /** | |
| 475 | * Try to convert to plaintext | |
| 476 | * | |
| 477 | * @access protected | |
| 478 | * @param string $source | |
| 479 | * @return string Plaintext string | |
| 480 | * @since 1.5 | |
| 481 | */ | |
| 482 | function _decode($source) | |
| 483 | { | |
| 484 | // entity decode | |
| 485 | $trans_tbl = get_html_translation_table(HTML_ENTITIES); | |
| 486 | foreach($trans_tbl as $k => $v) { | |
| 487 | $ttr[$v] = utf8_encode($k); | |
| 488 | } | |
| 489 | $source = strtr($source, $ttr); | |
| 490 | // convert decimal | |
| 491 | $source = preg_replace('/&#(\d+);/me', "chr(\\1)", $source); // decimal notation | |
| 492 | // convert hex | |
| 493 | $source = preg_replace('/&#x([a-f0-9]+);/mei', "chr(0x\\1)", $source); // hex notation | |
| 494 | return $source; | |
| 495 | } | |
| 1 | <?php | |
| 2 | /** | |
| 3 | * @version $Id$ | |
| 4 | * @package Joomla.Framework | |
| 5 | * @subpackage Filter | |
| 6 | * @copyright Copyright (C) 2005 - 2008 Open Source Matters. All rights reserved. | |
| 7 | * @license GNU/GPL, see LICENSE.php | |
| 8 | * Joomla! is free software. This version may have been modified pursuant to the | |
| 9 | * GNU General Public License, and as distributed it includes or is derivative | |
| 10 | * of works licensed under the GNU General Public License or other free or open | |
| 11 | * source software licenses. See COPYRIGHT.php for copyright notices and | |
| 12 | * details. | |
| 13 | */ | |
| 14 | ||
| 15 | // Check to ensure this file is within the rest of the framework | |
| 16 | defined('JPATH_BASE') or die(); | |
| 17 | ||
| 18 | /** | |
| 19 | * JFilterInput is a class for filtering input from any data source | |
| 20 | * | |
| 21 | * Forked from the php input filter library by: Daniel Morris <dan@rootcube.com> | |
| 22 | * Original Contributors: Gianpaolo Racca, Ghislain Picard, Marco Wandschneider, Chris Tobin and Andrew Eddie. | |
| 23 | * | |
| 24 | * @author Louis Landry <louis.landry@joomla.org> | |
| 25 | * @package Joomla.Framework | |
| 26 | * @subpackage Filter | |
| 27 | * @since 1.5 | |
| 28 | */ | |
| 29 | class JFilterInput extends JObject | |
| 30 | { | |
| 31 | var $tagsArray; // default = empty array | |
| 32 | var $attrArray; // default = empty array | |
| 33 | ||
| 34 | var $tagsMethod; // default = 0 | |
| 35 | var $attrMethod; // default = 0 | |
| 36 | ||
| 37 | var $xssAuto; // default = 1 | |
| 38 | var $tagBlacklist = array ('applet', 'body', 'bgsound', 'base', 'basefont', 'embed', 'frame', 'frameset', 'head', 'html', 'id', 'iframe', 'ilayer', 'layer', 'link', 'meta', 'name', 'object', 'script', 'style', 'title', 'xml'); | |
| 39 | var $attrBlacklist = array ('action', 'background', 'codebase', 'dynsrc', 'lowsrc'); // also will strip ALL event handlers | |
| 40 | ||
| 41 | /** | |
| 42 | * Constructor for inputFilter class. Only first parameter is required. | |
| 43 | * | |
| 44 | * @access protected | |
| 45 | * @param array $tagsArray list of user-defined tags | |
| 46 | * @param array $attrArray list of user-defined attributes | |
| 47 | * @param int $tagsMethod WhiteList method = 0, BlackList method = 1 | |
| 48 | * @param int $attrMethod WhiteList method = 0, BlackList method = 1 | |
| 49 | * @param int $xssAuto Only auto clean essentials = 0, Allow clean blacklisted tags/attr = 1 | |
| 50 | * @since 1.5 | |
| 51 | */ | |
| 52 | function __construct($tagsArray = array(), $attrArray = array(), $tagsMethod = 0, $attrMethod = 0, $xssAuto = 1) | |
| 53 | { | |
| 54 | // Make sure user defined arrays are in lowercase | |
| 55 | $tagsArray = array_map('strtolower', (array) $tagsArray); | |
| 56 | $attrArray = array_map('strtolower', (array) $attrArray); | |
| 57 | ||
| 58 | // Assign member variables | |
| 59 | $this->tagsArray = $tagsArray; | |
| 60 | $this->attrArray = $attrArray; | |
| 61 | $this->tagsMethod = $tagsMethod; | |
| 62 | $this->attrMethod = $attrMethod; | |
| 63 | $this->xssAuto = $xssAuto; | |
| 64 | } | |
| 65 | ||
| 66 | /** | |
| 67 | * Returns a reference to an input filter object, only creating it if it doesn't already exist. | |
| 68 | * | |
| 69 | * This method must be invoked as: | |
| 70 | * <pre> $filter = & JFilterInput::getInstance();</pre> | |
| 71 | * | |
| 72 | * @static | |
| 73 | * @param array $tagsArray list of user-defined tags | |
| 74 | * @param array $attrArray list of user-defined attributes | |
| 75 | * @param int $tagsMethod WhiteList method = 0, BlackList method = 1 | |
| 76 | * @param int $attrMethod WhiteList method = 0, BlackList method = 1 | |
| 77 | * @param int $xssAuto Only auto clean essentials = 0, Allow clean blacklisted tags/attr = 1 | |
| 78 | * @return object The JFilterInput object. | |
| 79 | * @since 1.5 | |
| 80 | */ | |
| 81 | function & getInstance($tagsArray = array(), $attrArray = array(), $tagsMethod = 0, $attrMethod = 0, $xssAuto = 1) | |
| 82 | { | |
| 83 | static $instances; | |
| 84 | ||
| 85 | $sig = md5(serialize(array($tagsArray,$attrArray,$tagsMethod,$attrMethod,$xssAuto))); | |
| 86 | ||
| 87 | if (!isset ($instances)) { | |
| 88 | $instances = array(); | |
| 89 | } | |
| 90 | ||
| 91 | if (empty ($instances[$sig])) { | |
| 92 | $instances[$sig] = new JFilterInput($tagsArray, $attrArray, $tagsMethod, $attrMethod, $xssAuto); | |
| 93 | } | |
| 94 | ||
| 95 | return $instances[$sig]; | |
| 96 | } | |
| 97 | ||
| 98 | /** | |
| 99 | * Method to be called by another php script. Processes for XSS and | |
| 100 | * specified bad code. | |
| 101 | * | |
| 102 | * @access public | |
| 103 | * @param mixed $source Input string/array-of-string to be 'cleaned' | |
| 104 | * @param string $type Return type for the variable (INT, FLOAT, BOOLEAN, WORD, ALNUM, CMD, BASE64, STRING, ARRAY, PATH, NONE) | |
| 105 | * @return mixed 'Cleaned' version of input parameter | |
| 106 | * @since 1.5 | |
| 107 | * @static | |
| 108 | */ | |
| 109 | function clean($source, $type='string') | |
| 110 | { | |
| 111 | // Handle the type constraint | |
| 112 | switch (strtoupper($type)) | |
| 113 | { | |
| 114 | case 'INT' : | |
| 115 | case 'INTEGER' : | |
| 116 | // Only use the first integer value | |
| 117 | preg_match('/-?[0-9]+/', (string) $source, $matches); | |
| 118 | $result = @ (int) $matches[0]; | |
| 119 | break; | |
| 120 | ||
| 121 | case 'FLOAT' : | |
| 122 | case 'DOUBLE' : | |
| 123 | // Only use the first floating point value | |
| 124 | preg_match('/-?[0-9]+(\.[0-9]+)?/', (string) $source, $matches); | |
| 125 | $result = @ (float) $matches[0]; | |
| 126 | break; | |
| 127 | ||
| 128 | case 'BOOL' : | |
| 129 | case 'BOOLEAN' : | |
| 130 | $result = (bool) $source; | |
| 131 | break; | |
| 132 | ||
| 133 | case 'WORD' : | |
| 134 | $result = (string) preg_replace( '/[^A-Z_]/i', '', $source ); | |
| 135 | break; | |
| 136 | ||
| 137 | case 'ALNUM' : | |
| 138 | $result = (string) preg_replace( '/[^A-Z0-9]/i', '', $source ); | |
| 139 | break; | |
| 140 | ||
| 141 | case 'CMD' : | |
| 142 | $result = (string) preg_replace( '/[^A-Z0-9_\.-]/i', '', $source ); | |
| 143 | $result = ltrim($result, '.'); | |
| 144 | break; | |
| 145 | ||
| 146 | case 'BASE64' : | |
| 147 | $result = (string) preg_replace( '/[^A-Z0-9\/+=]/i', '', $source ); | |
| 148 | break; | |
| 149 | ||
| 150 | case 'STRING' : | |
| 151 | // Check for static usage and assign $filter the proper variable | |
| 152 | if(isset($this) && is_a( $this, 'JFilterInput' )) { | |
| 153 | $filter =& $this; | |
| 154 | } else { | |
| 155 | $filter =& JFilterInput::getInstance(); | |
| 156 | } | |
| 157 | $result = (string) $filter->_remove($filter->_decode((string) $source)); | |
| 158 | break; | |
| 159 | ||
| 160 | case 'ARRAY' : | |
| 161 | $result = (array) $source; | |
| 162 | break; | |
| 163 | ||
| 164 | case 'PATH' : | |
| 165 | $pattern = '/^[A-Za-z0-9_-]+[A-Za-z0-9_\.-]*([\\\\\/][A-Za-z0-9_-]+[A-Za-z0-9_\.-]*)*$/'; | |
| 166 | preg_match($pattern, (string) $source, $matches); | |
| 167 | $result = @ (string) $matches[0]; | |
| 168 | break; | |
| 169 | ||
| 170 | case 'USERNAME' : | |
| 171 | $result = (string) preg_replace( '/[\x00-\x1F\x7F<>"\'%&]/', '', $source ); | |
| 172 | break; | |
| 173 | ||
| 174 | default : | |
| 175 | // Check for static usage and assign $filter the proper variable | |
| 176 | if(is_object($this) && get_class($this) == 'JFilterInput') { | |
| 177 | $filter =& $this; | |
| 178 | } else { | |
| 179 | $filter =& JFilterInput::getInstance(); | |
| 180 | } | |
| 181 | // Are we dealing with an array? | |
| 182 | if (is_array($source)) { | |
| 183 | foreach ($source as $key => $value) | |
| 184 | { | |
| 185 | // filter element for XSS and other 'bad' code etc. | |
| 186 | if (is_string($value)) { | |
| 187 | $source[$key] = $filter->_remove($filter->_decode($value)); | |
| 188 | } | |
| 189 | } | |
| 190 | $result = $source; | |
| 191 | } else { | |
| 192 | // Or a string? | |
| 193 | if (is_string($source) && !empty ($source)) { | |
| 194 | // filter source for XSS and other 'bad' code etc. | |
| 195 | $result = $filter->_remove($filter->_decode($source)); | |
| 196 | } else { | |
| 197 | // Not an array or string.. return the passed parameter | |
| 198 | $result = $source; | |
| 199 | } | |
| 200 | } | |
| 201 | break; | |
| 202 | } | |
| 203 | return $result; | |
| 204 | } | |
| 205 | ||
| 206 | /** | |
| 207 | * Function to determine if contents of an attribute is safe | |
| 208 | * | |
| 209 | * @static | |
| 210 | * @param array $attrSubSet A 2 element array for attributes name,value | |
| 211 | * @return boolean True if bad code is detected | |
| 212 | * @since 1.5 | |
| 213 | */ | |
| 214 | function checkAttribute($attrSubSet) | |
| 215 | { | |
| 216 | $attrSubSet[0] = strtolower($attrSubSet[0]); | |
| 217 | $attrSubSet[1] = strtolower($attrSubSet[1]); | |
| 218 | return (((strpos($attrSubSet[1], 'expression') !== false) && ($attrSubSet[0]) == 'style') || (strpos($attrSubSet[1], 'javascript:') !== false) || (strpos($attrSubSet[1], 'behaviour:') !== false) || (strpos($attrSubSet[1], 'vbscript:') !== false) || (strpos($attrSubSet[1], 'mocha:') !== false) || (strpos($attrSubSet[1], 'livescript:') !== false)); | |
| 219 | } | |
| 220 | ||
| 221 | /** | |
| 222 | * Internal method to iteratively remove all unwanted tags and attributes | |
| 223 | * | |
| 224 | * @access protected | |
| 225 | * @param string $source Input string to be 'cleaned' | |
| 226 | * @return string 'Cleaned' version of input parameter | |
| 227 | * @since 1.5 | |
| 228 | */ | |
| 229 | function _remove($source) | |
| 230 | { | |
| 231 | $loopCounter = 0; | |
| 232 | ||
| 233 | // Iteration provides nested tag protection | |
| 234 | while ($source != $this->_cleanTags($source)) | |
| 235 | { | |
| 236 | $source = $this->_cleanTags($source); | |
| 237 | $loopCounter ++; | |
| 238 | } | |
| 239 | return $source; | |
| 240 | } | |
| 241 | ||
| 242 | /** | |
| 243 | * Internal method to strip a string of certain tags | |
| 244 | * | |
| 245 | * @access protected | |
| 246 | * @param string $source Input string to be 'cleaned' | |
| 247 | * @return string 'Cleaned' version of input parameter | |
| 248 | * @since 1.5 | |
| 249 | */ | |
| 250 | function _cleanTags($source) | |
| 251 | { | |
| 252 | /* | |
| 253 | * In the beginning we don't really have a tag, so everything is | |
| 254 | * postTag | |
| 255 | */ | |
| 256 | $preTag = null; | |
| 257 | $postTag = $source; | |
| 258 | $currentSpace = false; | |
| 259 | $attr = ''; // moffats: setting to null due to issues in migration system - undefined variable errors | |
| 260 | ||
| 261 | // Is there a tag? If so it will certainly start with a '<' | |
| 262 | $tagOpen_start = strpos($source, '<'); | |
| 263 | ||
| 264 | while ($tagOpen_start !== false) | |
| 265 | { | |
| 266 | // Get some information about the tag we are processing | |
| 267 | $preTag .= substr($postTag, 0, $tagOpen_start); | |
| 268 | $postTag = substr($postTag, $tagOpen_start); | |
| 269 | $fromTagOpen = substr($postTag, 1); | |
| 270 | $tagOpen_end = strpos($fromTagOpen, '>'); | |
| 271 | ||
| 272 | // Let's catch any non-terminated tags and skip over them | |
| 273 | if ($tagOpen_end === false) { | |
| 274 | $postTag = substr($postTag, $tagOpen_start +1); | |
| 275 | $tagOpen_start = strpos($postTag, '<'); | |
| 276 | continue; | |
| 277 | } | |
| 278 | ||
| 279 | // Do we have a nested tag? | |
| 280 | $tagOpen_nested = strpos($fromTagOpen, '<'); | |
| 281 | $tagOpen_nested_end = strpos(substr($postTag, $tagOpen_end), '>'); | |
| 282 | if (($tagOpen_nested !== false) && ($tagOpen_nested < $tagOpen_end)) { | |
| 283 | $preTag .= substr($postTag, 0, ($tagOpen_nested +1)); | |
| 284 | $postTag = substr($postTag, ($tagOpen_nested +1)); | |
| 285 | $tagOpen_start = strpos($postTag, '<'); | |
| 286 | continue; | |
| 287 | } | |
| 288 | ||
| 289 | // Lets get some information about our tag and setup attribute pairs | |
| 290 | $tagOpen_nested = (strpos($fromTagOpen, '<') + $tagOpen_start +1); | |
| 291 | $currentTag = substr($fromTagOpen, 0, $tagOpen_end); | |
| 292 | $tagLength = strlen($currentTag); | |
| 293 | $tagLeft = $currentTag; | |
| 294 | $attrSet = array (); | |
| 295 | $currentSpace = strpos($tagLeft, ' '); | |
| 296 | ||
| 297 | // Are we an open tag or a close tag? | |
| 298 | if (substr($currentTag, 0, 1) == '/') { | |
| 299 | // Close Tag | |
| 300 | $isCloseTag = true; | |
| 301 | list ($tagName) = explode(' ', $currentTag); | |
| 302 | $tagName = substr($tagName, 1); | |
| 303 | } else { | |
| 304 | // Open Tag | |
| 305 | $isCloseTag = false; | |
| 306 | list ($tagName) = explode(' ', $currentTag); | |
| 307 | } | |
| 308 | ||
| 309 | /* | |
| 310 | * Exclude all "non-regular" tagnames | |
| 311 | * OR no tagname | |
| 312 | * OR remove if xssauto is on and tag is blacklisted | |
| 313 | */ | |
| 314 | if ((!preg_match("/^[a-z][a-z0-9]*$/i", $tagName)) || (!$tagName) || ((in_array(strtolower($tagName), $this->tagBlacklist)) && ($this->xssAuto))) { | |
| 315 | $postTag = substr($postTag, ($tagLength +2)); | |
| 316 | $tagOpen_start = strpos($postTag, '<'); | |
| 317 | // Strip tag | |
| 318 | continue; | |
| 319 | } | |
| 320 | ||
| 321 | /* | |
| 322 | * Time to grab any attributes from the tag... need this section in | |
| 323 | * case attributes have spaces in the values. | |
| 324 | */ | |
| 325 | while ($currentSpace !== false) | |
| 326 | { | |
| 327 | $fromSpace = substr($tagLeft, ($currentSpace +1)); | |
| 328 | $nextSpace = strpos($fromSpace, ' '); | |
| 329 | $openQuotes = strpos($fromSpace, '"'); | |
| 330 | $closeQuotes = strpos(substr($fromSpace, ($openQuotes +1)), '"') + $openQuotes +1; | |
| 331 | ||
| 332 | // Do we have an attribute to process? [check for equal sign] | |
| 333 | if (strpos($fromSpace, '=') !== false) { | |
| 334 | /* | |
| 335 | * If the attribute value is wrapped in quotes we need to | |
| 336 | * grab the substring from the closing quote, otherwise grab | |
| 337 | * till the next space | |
| 338 | */ | |