1<?php 2/** 3 * Smarty plugin 4 * 5 * @package Smarty 6 * @subpackage Security 7 * @author Uwe Tews 8 */ 9/** 10 * FIXME: Smarty_Security API 11 * - getter and setter instead of public properties would allow cultivating an internal cache properly 12 * - current implementation of isTrustedResourceDir() assumes that Smarty::$template_dir and Smarty::$config_dir 13 * are immutable the cache is killed every time either of the variables change. That means that two distinct 14 * Smarty objects with differing 15 * $template_dir or $config_dir should NOT share the same Smarty_Security instance, 16 * as this would lead to (severe) performance penalty! how should this be handled? 17 */ 18 19/** 20 * This class does contain the security settings 21 */ 22class Smarty_Security 23{ 24 /** 25 * This determines how Smarty handles "<?php ... ?>" tags in templates. 26 * possible values: 27 * <ul> 28 * <li>Smarty::PHP_PASSTHRU -> echo PHP tags as they are</li> 29 * <li>Smarty::PHP_QUOTE -> escape tags as entities</li> 30 * <li>Smarty::PHP_REMOVE -> remove php tags</li> 31 * <li>Smarty::PHP_ALLOW -> execute php tags</li> 32 * </ul> 33 * 34 * @var integer 35 */ 36 public $php_handling = Smarty::PHP_PASSTHRU; 37 38 /** 39 * This is the list of template directories that are considered secure. 40 * $template_dir is in this list implicitly. 41 * 42 * @var array 43 */ 44 public $secure_dir = array(); 45 46 /** 47 * This is an array of directories where trusted php scripts reside. 48 * {@link $security} is disabled during their inclusion/execution. 49 * 50 * @var array 51 */ 52 public $trusted_dir = array(); 53 54 /** 55 * List of regular expressions (PCRE) that include trusted URIs 56 * 57 * @var array 58 */ 59 public $trusted_uri = array(); 60 61 /** 62 * List of trusted constants names 63 * 64 * @var array 65 */ 66 public $trusted_constants = array(); 67 68 /** 69 * This is an array of trusted static classes. 70 * If empty access to all static classes is allowed. 71 * If set to 'none' none is allowed. 72 * 73 * @var array 74 */ 75 public $static_classes = array(); 76 77 /** 78 * This is an nested array of trusted classes and static methods. 79 * If empty access to all static classes and methods is allowed. 80 * Format: 81 * array ( 82 * 'class_1' => array('method_1', 'method_2'), // allowed methods listed 83 * 'class_2' => array(), // all methods of class allowed 84 * ) 85 * If set to null none is allowed. 86 * 87 * @var array 88 */ 89 public $trusted_static_methods = array(); 90 91 /** 92 * This is an array of trusted static properties. 93 * If empty access to all static classes and properties is allowed. 94 * Format: 95 * array ( 96 * 'class_1' => array('prop_1', 'prop_2'), // allowed properties listed 97 * 'class_2' => array(), // all properties of class allowed 98 * ) 99 * If set to null none is allowed. 100 * 101 * @var array 102 */ 103 public $trusted_static_properties = array(); 104 105 /** 106 * This is an array of trusted PHP functions. 107 * If empty all functions are allowed. 108 * To disable all PHP functions set $php_functions = null. 109 * 110 * @var array 111 */ 112 public $php_functions = array('isset', 'empty', 'count', 'sizeof', 'in_array', 'is_array', 'time',); 113 114 /** 115 * This is an array of trusted PHP modifiers. 116 * If empty all modifiers are allowed. 117 * To disable all modifier set $php_modifiers = null. 118 * 119 * @var array 120 */ 121 public $php_modifiers = array('escape', 'count', 'nl2br',); 122 123 /** 124 * This is an array of allowed tags. 125 * If empty no restriction by allowed_tags. 126 * 127 * @var array 128 */ 129 public $allowed_tags = array(); 130 131 /** 132 * This is an array of disabled tags. 133 * If empty no restriction by disabled_tags. 134 * 135 * @var array 136 */ 137 public $disabled_tags = array(); 138 139 /** 140 * This is an array of allowed modifier plugins. 141 * If empty no restriction by allowed_modifiers. 142 * 143 * @var array 144 */ 145 public $allowed_modifiers = array(); 146 147 /** 148 * This is an array of disabled modifier plugins. 149 * If empty no restriction by disabled_modifiers. 150 * 151 * @var array 152 */ 153 public $disabled_modifiers = array(); 154 155 /** 156 * This is an array of disabled special $smarty variables. 157 * 158 * @var array 159 */ 160 public $disabled_special_smarty_vars = array(); 161 162 /** 163 * This is an array of trusted streams. 164 * If empty all streams are allowed. 165 * To disable all streams set $streams = null. 166 * 167 * @var array 168 */ 169 public $streams = array('file'); 170 171 /** 172 * + flag if constants can be accessed from template 173 * 174 * @var boolean 175 */ 176 public $allow_constants = true; 177 178 /** 179 * + flag if super globals can be accessed from template 180 * 181 * @var boolean 182 */ 183 public $allow_super_globals = true; 184 185 /** 186 * max template nesting level 187 * 188 * @var int 189 */ 190 public $max_template_nesting = 0; 191 192 /** 193 * current template nesting level 194 * 195 * @var int 196 */ 197 private $_current_template_nesting = 0; 198 199 /** 200 * Cache for $resource_dir lookup 201 * 202 * @var array 203 */ 204 protected $_resource_dir = array(); 205 206 /** 207 * Cache for $template_dir lookup 208 * 209 * @var array 210 */ 211 protected $_template_dir = array(); 212 213 /** 214 * Cache for $config_dir lookup 215 * 216 * @var array 217 */ 218 protected $_config_dir = array(); 219 220 /** 221 * Cache for $secure_dir lookup 222 * 223 * @var array 224 */ 225 protected $_secure_dir = array(); 226 227 /** 228 * Cache for $php_resource_dir lookup 229 * 230 * @var array 231 */ 232 protected $_php_resource_dir = null; 233 234 /** 235 * Cache for $trusted_dir lookup 236 * 237 * @var array 238 */ 239 protected $_trusted_dir = null; 240 241 /** 242 * Cache for include path status 243 * 244 * @var bool 245 */ 246 protected $_include_path_status = false; 247 248 /** 249 * Cache for $_include_array lookup 250 * 251 * @var array 252 */ 253 protected $_include_dir = array(); 254 255 /** 256 * @param Smarty $smarty 257 */ 258 public function __construct($smarty) 259 { 260 $this->smarty = $smarty; 261 } 262 263 /** 264 * Check if PHP function is trusted. 265 * 266 * @param string $function_name 267 * @param object $compiler compiler object 268 * 269 * @return boolean true if function is trusted 270 */ 271 public function isTrustedPhpFunction($function_name, $compiler) 272 { 273 if (isset($this->php_functions) 274 && (empty($this->php_functions) || in_array($function_name, $this->php_functions)) 275 ) { 276 return true; 277 } 278 $compiler->trigger_template_error("PHP function '{$function_name}' not allowed by security setting"); 279 return false; // should not, but who knows what happens to the compiler in the future? 280 } 281 282 /** 283 * Check if static class is trusted. 284 * 285 * @param string $class_name 286 * @param object $compiler compiler object 287 * 288 * @return boolean true if class is trusted 289 */ 290 public function isTrustedStaticClass($class_name, $compiler) 291 { 292 if (isset($this->static_classes) 293 && (empty($this->static_classes) || in_array($class_name, $this->static_classes)) 294 ) { 295 return true; 296 } 297 $compiler->trigger_template_error("access to static class '{$class_name}' not allowed by security setting"); 298 return false; // should not, but who knows what happens to the compiler in the future? 299 } 300 301 /** 302 * Check if static class method/property is trusted. 303 * 304 * @param string $class_name 305 * @param string $params 306 * @param object $compiler compiler object 307 * 308 * @return boolean true if class method is trusted 309 */ 310 public function isTrustedStaticClassAccess($class_name, $params, $compiler) 311 { 312 if (!isset($params[ 2 ])) { 313 // fall back 314 return $this->isTrustedStaticClass($class_name, $compiler); 315 } 316 if ($params[ 2 ] === 'method') { 317 $allowed = $this->trusted_static_methods; 318 $name = substr($params[ 0 ], 0, strpos($params[ 0 ], '(')); 319 } else { 320 $allowed = $this->trusted_static_properties; 321 // strip '$' 322 $name = substr($params[ 0 ], 1); 323 } 324 if (isset($allowed)) { 325 if (empty($allowed)) { 326 // fall back 327 return $this->isTrustedStaticClass($class_name, $compiler); 328 } 329 if (isset($allowed[ $class_name ]) 330 && (empty($allowed[ $class_name ]) || in_array($name, $allowed[ $class_name ])) 331 ) { 332 return true; 333 } 334 } 335 $compiler->trigger_template_error("access to static class '{$class_name}' {$params[2]} '{$name}' not allowed by security setting"); 336 return false; // should not, but who knows what happens to the compiler in the future? 337 } 338 339 /** 340 * Check if PHP modifier is trusted. 341 * 342 * @param string $modifier_name 343 * @param object $compiler compiler object 344 * 345 * @return boolean true if modifier is trusted 346 */ 347 public function isTrustedPhpModifier($modifier_name, $compiler) 348 { 349 if (isset($this->php_modifiers) 350 && (empty($this->php_modifiers) || in_array($modifier_name, $this->php_modifiers)) 351 ) { 352 return true; 353 } 354 $compiler->trigger_template_error("modifier '{$modifier_name}' not allowed by security setting"); 355 return false; // should not, but who knows what happens to the compiler in the future? 356 } 357 358 /** 359 * Check if tag is trusted. 360 * 361 * @param string $tag_name 362 * @param object $compiler compiler object 363 * 364 * @return boolean true if tag is trusted 365 */ 366 public function isTrustedTag($tag_name, $compiler) 367 { 368 // check for internal always required tags 369 if (in_array( 370 $tag_name, 371 array( 372 'assign', 'call', 'private_filter', 'private_block_plugin', 'private_function_plugin', 373 'private_object_block_function', 'private_object_function', 'private_registered_function', 374 'private_registered_block', 'private_special_variable', 'private_print_expression', 375 'private_modifier' 376 ) 377 ) 378 ) { 379 return true; 380 } 381 // check security settings 382 if (empty($this->allowed_tags)) { 383 if (empty($this->disabled_tags) || !in_array($tag_name, $this->disabled_tags)) { 384 return true; 385 } else { 386 $compiler->trigger_template_error("tag '{$tag_name}' disabled by security setting", null, true); 387 } 388 } elseif (in_array($tag_name, $this->allowed_tags) && !in_array($tag_name, $this->disabled_tags)) { 389 return true; 390 } else { 391 $compiler->trigger_template_error("tag '{$tag_name}' not allowed by security setting", null, true); 392 } 393 return false; // should not, but who knows what happens to the compiler in the future? 394 } 395 396 /** 397 * Check if special $smarty variable is trusted. 398 * 399 * @param string $var_name 400 * @param object $compiler compiler object 401 * 402 * @return boolean true if tag is trusted 403 */ 404 public function isTrustedSpecialSmartyVar($var_name, $compiler) 405 { 406 if (!in_array($var_name, $this->disabled_special_smarty_vars)) { 407 return true; 408 } else { 409 $compiler->trigger_template_error( 410 "special variable '\$smarty.{$var_name}' not allowed by security setting", 411 null, 412 true 413 ); 414 } 415 return false; // should not, but who knows what happens to the compiler in the future? 416 } 417 418 /** 419 * Check if modifier plugin is trusted. 420 * 421 * @param string $modifier_name 422 * @param object $compiler compiler object 423 * 424 * @return boolean true if tag is trusted 425 */ 426 public function isTrustedModifier($modifier_name, $compiler) 427 { 428 // check for internal always allowed modifier 429 if (in_array($modifier_name, array('default'))) { 430 return true; 431 } 432 // check security settings 433 if (empty($this->allowed_modifiers)) { 434 if (empty($this->disabled_modifiers) || !in_array($modifier_name, $this->disabled_modifiers)) { 435 return true; 436 } else { 437 $compiler->trigger_template_error( 438 "modifier '{$modifier_name}' disabled by security setting", 439 null, 440 true 441 ); 442 } 443 } elseif (in_array($modifier_name, $this->allowed_modifiers) 444 && !in_array($modifier_name, $this->disabled_modifiers) 445 ) { 446 return true; 447 } else { 448 $compiler->trigger_template_error( 449 "modifier '{$modifier_name}' not allowed by security setting", 450 null, 451 true 452 ); 453 } 454 return false; // should not, but who knows what happens to the compiler in the future? 455 } 456 457 /** 458 * Check if constants are enabled or trusted 459 * 460 * @param string $const constant name 461 * @param object $compiler compiler object 462 * 463 * @return bool 464 */ 465 public function isTrustedConstant($const, $compiler) 466 { 467 if (in_array($const, array('true', 'false', 'null'))) { 468 return true; 469 } 470 if (!empty($this->trusted_constants)) { 471 if (!in_array(strtolower($const), $this->trusted_constants)) { 472 $compiler->trigger_template_error("Security: access to constant '{$const}' not permitted"); 473 return false; 474 } 475 return true; 476 } 477 if ($this->allow_constants) { 478 return true; 479 } 480 $compiler->trigger_template_error("Security: access to constants not permitted"); 481 return false; 482 } 483 484 /** 485 * Check if stream is trusted. 486 * 487 * @param string $stream_name 488 * 489 * @return boolean true if stream is trusted 490 * @throws SmartyException if stream is not trusted 491 */ 492 public function isTrustedStream($stream_name) 493 { 494 if (isset($this->streams) && (empty($this->streams) || in_array($stream_name, $this->streams))) { 495 return true; 496 } 497 throw new SmartyException("stream '{$stream_name}' not allowed by security setting"); 498 } 499 500 /** 501 * Check if directory of file resource is trusted. 502 * 503 * @param string $filepath 504 * @param null|bool $isConfig 505 * 506 * @return bool true if directory is trusted 507 * @throws \SmartyException if directory is not trusted 508 */ 509 public function isTrustedResourceDir($filepath, $isConfig = null) 510 { 511 if ($this->_include_path_status !== $this->smarty->use_include_path) { 512 $_dir = 513 $this->smarty->use_include_path ? $this->smarty->ext->_getIncludePath->getIncludePathDirs($this->smarty) : array(); 514 if ($this->_include_dir !== $_dir) { 515 $this->_updateResourceDir($this->_include_dir, $_dir); 516 $this->_include_dir = $_dir; 517 } 518 $this->_include_path_status = $this->smarty->use_include_path; 519 } 520 $_dir = $this->smarty->getTemplateDir(); 521 if ($this->_template_dir !== $_dir) { 522 $this->_updateResourceDir($this->_template_dir, $_dir); 523 $this->_template_dir = $_dir; 524 } 525 $_dir = $this->smarty->getConfigDir(); 526 if ($this->_config_dir !== $_dir) { 527 $this->_updateResourceDir($this->_config_dir, $_dir); 528 $this->_config_dir = $_dir; 529 } 530 if ($this->_secure_dir !== $this->secure_dir) { 531 $this->secure_dir = (array)$this->secure_dir; 532 foreach ($this->secure_dir as $k => $d) { 533 $this->secure_dir[ $k ] = $this->smarty->_realpath($d . DIRECTORY_SEPARATOR, true); 534 } 535 $this->_updateResourceDir($this->_secure_dir, $this->secure_dir); 536 $this->_secure_dir = $this->secure_dir; 537 } 538 $addPath = $this->_checkDir($filepath, $this->_resource_dir); 539 if ($addPath !== false) { 540 $this->_resource_dir = array_merge($this->_resource_dir, $addPath); 541 } 542 return true; 543 } 544 545 /** 546 * Check if URI (e.g. {fetch} or {html_image}) is trusted 547 * To simplify things, isTrustedUri() resolves all input to "{$PROTOCOL}://{$HOSTNAME}". 548 * So "http://username:password@hello.world.example.org:8080/some-path?some=query-string" 549 * is reduced to "http://hello.world.example.org" prior to applying the patters from {@link $trusted_uri}. 550 * 551 * @param string $uri 552 * 553 * @return boolean true if URI is trusted 554 * @throws SmartyException if URI is not trusted 555 * @uses $trusted_uri for list of patterns to match against $uri 556 */ 557 public function isTrustedUri($uri) 558 { 559 $_uri = parse_url($uri); 560 if (!empty($_uri[ 'scheme' ]) && !empty($_uri[ 'host' ])) { 561 $_uri = $_uri[ 'scheme' ] . '://' . $_uri[ 'host' ]; 562 foreach ($this->trusted_uri as $pattern) { 563 if (preg_match($pattern, $_uri)) { 564 return true; 565 } 566 } 567 } 568 throw new SmartyException("URI '{$uri}' not allowed by security setting"); 569 } 570 571 /** 572 * Check if directory of file resource is trusted. 573 * 574 * @param string $filepath 575 * 576 * @return boolean true if directory is trusted 577 * @throws SmartyException if PHP directory is not trusted 578 */ 579 public function isTrustedPHPDir($filepath) 580 { 581 if (empty($this->trusted_dir)) { 582 throw new SmartyException("directory '{$filepath}' not allowed by security setting (no trusted_dir specified)"); 583 } 584 // check if index is outdated 585 if (!$this->_trusted_dir || $this->_trusted_dir !== $this->trusted_dir) { 586 $this->_php_resource_dir = array(); 587 $this->_trusted_dir = $this->trusted_dir; 588 foreach ((array)$this->trusted_dir as $directory) { 589 $directory = $this->smarty->_realpath($directory . '/', true); 590 $this->_php_resource_dir[ $directory ] = true; 591 } 592 } 593 $addPath = $this->_checkDir($filepath, $this->_php_resource_dir); 594 if ($addPath !== false) { 595 $this->_php_resource_dir = array_merge($this->_php_resource_dir, $addPath); 596 } 597 return true; 598 } 599 600 /** 601 * Remove old directories and its sub folders, add new directories 602 * 603 * @param array $oldDir 604 * @param array $newDir 605 */ 606 private function _updateResourceDir($oldDir, $newDir) 607 { 608 foreach ($oldDir as $directory) { 609 // $directory = $this->smarty->_realpath($directory, true); 610 $length = strlen($directory); 611 foreach ($this->_resource_dir as $dir) { 612 if (substr($dir, 0, $length) === $directory) { 613 unset($this->_resource_dir[ $dir ]); 614 } 615 } 616 } 617 foreach ($newDir as $directory) { 618 // $directory = $this->smarty->_realpath($directory, true); 619 $this->_resource_dir[ $directory ] = true; 620 } 621 } 622 623 /** 624 * Check if file is inside a valid directory 625 * 626 * @param string $filepath 627 * @param array $dirs valid directories 628 * 629 * @return array|bool 630 * @throws \SmartyException 631 */ 632 private function _checkDir($filepath, $dirs) 633 { 634 $directory = dirname($this->smarty->_realpath($filepath, true)) . DIRECTORY_SEPARATOR; 635 $_directory = array(); 636 if (!preg_match('#[\\\\/][.][.][\\\\/]#', $directory)) { 637 while (true) { 638 // test if the directory is trusted 639 if (isset($dirs[ $directory ])) { 640 return $_directory; 641 } 642 // abort if we've reached root 643 if (!preg_match('#[\\\\/][^\\\\/]+[\\\\/]$#', $directory)) { 644 // give up 645 break; 646 } 647 // remember the directory to add it to _resource_dir in case we're successful 648 $_directory[ $directory ] = true; 649 // bubble up one level 650 $directory = preg_replace('#[\\\\/][^\\\\/]+[\\\\/]$#', DIRECTORY_SEPARATOR, $directory); 651 } 652 } 653 // give up 654 throw new SmartyException(sprintf('Smarty Security: not trusted file path \'%s\' ', $filepath)); 655 } 656 657 /** 658 * Loads security class and enables security 659 * 660 * @param \Smarty $smarty 661 * @param string|Smarty_Security $security_class if a string is used, it must be class-name 662 * 663 * @return \Smarty current Smarty instance for chaining 664 * @throws \SmartyException when an invalid class name is provided 665 */ 666 public static function enableSecurity(Smarty $smarty, $security_class) 667 { 668 if ($security_class instanceof Smarty_Security) { 669 $smarty->security_policy = $security_class; 670 return $smarty; 671 } elseif (is_object($security_class)) { 672 throw new SmartyException("Class '" . get_class($security_class) . "' must extend Smarty_Security."); 673 } 674 if ($security_class === null) { 675 $security_class = $smarty->security_class; 676 } 677 if (!class_exists($security_class)) { 678 throw new SmartyException("Security class '$security_class' is not defined"); 679 } elseif ($security_class !== 'Smarty_Security' && !is_subclass_of($security_class, 'Smarty_Security')) { 680 throw new SmartyException("Class '$security_class' must extend Smarty_Security."); 681 } else { 682 $smarty->security_policy = new $security_class($smarty); 683 } 684 return $smarty; 685 } 686 687 /** 688 * Start template processing 689 * 690 * @param $template 691 * 692 * @throws SmartyException 693 */ 694 public function startTemplate($template) 695 { 696 if ($this->max_template_nesting > 0 && $this->_current_template_nesting++ >= $this->max_template_nesting) { 697 throw new SmartyException("maximum template nesting level of '{$this->max_template_nesting}' exceeded when calling '{$template->template_resource}'"); 698 } 699 } 700 701 /** 702 * Exit template processing 703 */ 704 public function endTemplate() 705 { 706 if ($this->max_template_nesting > 0) { 707 $this->_current_template_nesting--; 708 } 709 } 710 711 /** 712 * Register callback functions call at start/end of template rendering 713 * 714 * @param \Smarty_Internal_Template $template 715 */ 716 public function registerCallBacks(Smarty_Internal_Template $template) 717 { 718 $template->startRenderCallbacks[] = array($this, 'startTemplate'); 719 $template->endRenderCallbacks[] = array($this, 'endTemplate'); 720 } 721} 722