1<?php 2/** 3 * Smarty plugin 4 * 5 * @package Smarty 6 * @subpackage Security 7 * @author Uwe Tews 8 */ 9 10/* 11 * FIXME: Smarty_Security API 12 * - getter and setter instead of public properties would allow cultivating an internal cache properly 13 * - current implementation of isTrustedResourceDir() assumes that Smarty::$template_dir and Smarty::$config_dir are immutable 14 * the cache is killed every time either of the variables change. That means that two distinct 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 * @throws SmartyCompilerException if php function is not trusted 271 */ 272 public function isTrustedPhpFunction($function_name, $compiler) 273 { 274 if (isset($this->php_functions) && 275 (empty($this->php_functions) || in_array($function_name, $this->php_functions)) 276 ) { 277 return true; 278 } 279 280 $compiler->trigger_template_error("PHP function '{$function_name}' not allowed by security setting"); 281 282 return false; // should not, but who knows what happens to the compiler in the future? 283 } 284 285 /** 286 * Check if static class is trusted. 287 * 288 * @param string $class_name 289 * @param object $compiler compiler object 290 * 291 * @return boolean true if class is trusted 292 * @throws SmartyCompilerException if static class is not trusted 293 */ 294 public function isTrustedStaticClass($class_name, $compiler) 295 { 296 if (isset($this->static_classes) && 297 (empty($this->static_classes) || in_array($class_name, $this->static_classes)) 298 ) { 299 return true; 300 } 301 302 $compiler->trigger_template_error("access to static class '{$class_name}' not allowed by security setting"); 303 304 return false; // should not, but who knows what happens to the compiler in the future? 305 } 306 307 /** 308 * Check if static class method/property is trusted. 309 * 310 * @param string $class_name 311 * @param string $params 312 * @param object $compiler compiler object 313 * 314 * @return boolean true if class method is trusted 315 * @throws SmartyCompilerException if static class method is not trusted 316 */ 317 public function isTrustedStaticClassAccess($class_name, $params, $compiler) 318 { 319 if (!isset($params[2])) { 320 // fall back 321 return $this->isTrustedStaticClass($class_name, $compiler); 322 } 323 if ($params[2] == 'method') { 324 $allowed = $this->trusted_static_methods; 325 $name = substr($params[0], 0, strpos($params[0], '(')); 326 } else { 327 $allowed = $this->trusted_static_properties; 328 // strip '$' 329 $name = substr($params[0], 1); 330 } 331 if (isset($allowed)) { 332 if (empty($allowed)) { 333 // fall back 334 return $this->isTrustedStaticClass($class_name, $compiler); 335 } 336 if (isset($allowed[$class_name]) && 337 (empty($allowed[$class_name]) || in_array($name, $allowed[$class_name])) 338 ) { 339 return true; 340 } 341 } 342 $compiler->trigger_template_error("access to static class '{$class_name}' {$params[2]} '{$name}' not allowed by security setting"); 343 return false; // should not, but who knows what happens to the compiler in the future? 344 } 345 346 /** 347 * Check if PHP modifier is trusted. 348 * 349 * @param string $modifier_name 350 * @param object $compiler compiler object 351 * 352 * @return boolean true if modifier is trusted 353 * @throws SmartyCompilerException if modifier is not trusted 354 */ 355 public function isTrustedPhpModifier($modifier_name, $compiler) 356 { 357 if (isset($this->php_modifiers) && 358 (empty($this->php_modifiers) || in_array($modifier_name, $this->php_modifiers)) 359 ) { 360 return true; 361 } 362 363 $compiler->trigger_template_error("modifier '{$modifier_name}' not allowed by security setting"); 364 365 return false; // should not, but who knows what happens to the compiler in the future? 366 } 367 368 /** 369 * Check if tag is trusted. 370 * 371 * @param string $tag_name 372 * @param object $compiler compiler object 373 * 374 * @return boolean true if tag is trusted 375 * @throws SmartyCompilerException if modifier is not trusted 376 */ 377 public function isTrustedTag($tag_name, $compiler) 378 { 379 // check for internal always required tags 380 if (in_array($tag_name, array('assign', 'call', 'private_filter', 'private_block_plugin', 381 'private_function_plugin', 'private_object_block_function', 382 'private_object_function', 'private_registered_function', 383 'private_registered_block', 'private_special_variable', 384 'private_print_expression', 'private_modifier'))) { 385 return true; 386 } 387 // check security settings 388 if (empty($this->allowed_tags)) { 389 if (empty($this->disabled_tags) || !in_array($tag_name, $this->disabled_tags)) { 390 return true; 391 } else { 392 $compiler->trigger_template_error("tag '{$tag_name}' disabled by security setting", null, true); 393 } 394 } elseif (in_array($tag_name, $this->allowed_tags) && !in_array($tag_name, $this->disabled_tags)) { 395 return true; 396 } else { 397 $compiler->trigger_template_error("tag '{$tag_name}' not allowed by security setting", null, true); 398 } 399 400 return false; // should not, but who knows what happens to the compiler in the future? 401 } 402 403 /** 404 * Check if special $smarty variable is trusted. 405 * 406 * @param string $var_name 407 * @param object $compiler compiler object 408 * 409 * @return boolean true if tag is trusted 410 * @throws SmartyCompilerException if modifier is not trusted 411 */ 412 public function isTrustedSpecialSmartyVar($var_name, $compiler) 413 { 414 if (!in_array($var_name, $this->disabled_special_smarty_vars)) { 415 return true; 416 } else { 417 $compiler->trigger_template_error("special variable '\$smarty.{$var_name}' not allowed by security setting", null, true); 418 } 419 420 return false; // should not, but who knows what happens to the compiler in the future? 421 } 422 423 /** 424 * Check if modifier plugin is trusted. 425 * 426 * @param string $modifier_name 427 * @param object $compiler compiler object 428 * 429 * @return boolean true if tag is trusted 430 * @throws SmartyCompilerException if modifier is not trusted 431 */ 432 public function isTrustedModifier($modifier_name, $compiler) 433 { 434 // check for internal always allowed modifier 435 if (in_array($modifier_name, array('default'))) { 436 return true; 437 } 438 // check security settings 439 if (empty($this->allowed_modifiers)) { 440 if (empty($this->disabled_modifiers) || !in_array($modifier_name, $this->disabled_modifiers)) { 441 return true; 442 } else { 443 $compiler->trigger_template_error("modifier '{$modifier_name}' disabled by security setting", null, true); 444 } 445 } elseif (in_array($modifier_name, $this->allowed_modifiers) && 446 !in_array($modifier_name, $this->disabled_modifiers) 447 ) { 448 return true; 449 } else { 450 $compiler->trigger_template_error("modifier '{$modifier_name}' not allowed by security setting", null, true); 451 } 452 453 return false; // should not, but who knows what happens to the compiler in the future? 454 } 455 456 /** 457 * Check if constants are enabled or trusted 458 * 459 * @param string $const constant name 460 * @param object $compiler compiler object 461 * 462 * @return bool 463 */ 464 public function isTrustedConstant($const, $compiler) 465 { 466 if (in_array($const, array('true', 'false', 'null'))) { 467 return true; 468 } 469 if (!empty($this->trusted_constants)) { 470 if (!in_array($const, $this->trusted_constants)) { 471 $compiler->trigger_template_error("Security: access to constant '{$const}' not permitted"); 472 return false; 473 } 474 return true; 475 } 476 if ($this->allow_constants) { 477 return true; 478 } 479 $compiler->trigger_template_error("Security: access to constants not permitted"); 480 return false; 481 } 482 483 /** 484 * Check if stream is trusted. 485 * 486 * @param string $stream_name 487 * 488 * @return boolean true if stream is trusted 489 * @throws SmartyException if stream is not trusted 490 */ 491 public function isTrustedStream($stream_name) 492 { 493 if (isset($this->streams) && (empty($this->streams) || in_array($stream_name, $this->streams))) { 494 return true; 495 } 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 foreach ($this->_include_dir as $directory) { 513 unset($this->_resource_dir[$directory]); 514 } 515 if ($this->smarty->use_include_path) { 516 $this->_include_dir = array(); 517 $_dirs = $this->smarty->ext->_getIncludePath->getIncludePathDirs($this->smarty); 518 foreach ($_dirs as $directory) { 519 $this->_include_dir[] = $directory; 520 $this->_resource_dir[$directory] = true; 521 } 522 } 523 $this->_include_path_status = $this->smarty->use_include_path; 524 } 525 if ($isConfig !== true && 526 (!isset($this->smarty->_cache['template_dir_new']) || $this->smarty->_cache['template_dir_new']) 527 ) { 528 $_dir = $this->smarty->getTemplateDir(); 529 if ($this->_template_dir !== $_dir) { 530 foreach ($this->_template_dir as $directory) { 531 unset($this->_resource_dir[$directory]); 532 } 533 foreach ($_dir as $directory) { 534 $this->_resource_dir[$directory] = true; 535 } 536 $this->_template_dir = $_dir; 537 } 538 $this->smarty->_cache['template_dir_new'] = false; 539 } 540 if ($isConfig !== false && 541 (!isset($this->smarty->_cache['config_dir_new']) || $this->smarty->_cache['config_dir_new']) 542 ) { 543 $_dir = $this->smarty->getConfigDir(); 544 if ($this->_config_dir !== $_dir) { 545 foreach ($this->_config_dir as $directory) { 546 unset($this->_resource_dir[$directory]); 547 } 548 foreach ($_dir as $directory) { 549 $this->_resource_dir[$directory] = true; 550 } 551 $this->_config_dir = $_dir; 552 } 553 $this->smarty->_cache['config_dir_new'] = false; 554 } 555 if ($this->_secure_dir !== (array) $this->secure_dir) { 556 foreach ($this->_secure_dir as $directory) { 557 unset($this->_resource_dir[$directory]); 558 } 559 foreach ((array) $this->secure_dir as $directory) { 560 $directory = $this->smarty->_realpath($directory . DS, true); 561 $this->_resource_dir[$directory] = true; 562 } 563 $this->_secure_dir = (array) $this->secure_dir; 564 } 565 $this->_resource_dir = $this->_checkDir($filepath, $this->_resource_dir); 566 return true; 567 } 568 569 /** 570 * Check if URI (e.g. {fetch} or {html_image}) is trusted 571 * To simplify things, isTrustedUri() resolves all input to "{$PROTOCOL}://{$HOSTNAME}". 572 * So "http://username:password@hello.world.example.org:8080/some-path?some=query-string" 573 * is reduced to "http://hello.world.example.org" prior to applying the patters from {@link $trusted_uri}. 574 * 575 * @param string $uri 576 * 577 * @return boolean true if URI is trusted 578 * @throws SmartyException if URI is not trusted 579 * @uses $trusted_uri for list of patterns to match against $uri 580 */ 581 public function isTrustedUri($uri) 582 { 583 $_uri = parse_url($uri); 584 if (!empty($_uri['scheme']) && !empty($_uri['host'])) { 585 $_uri = $_uri['scheme'] . '://' . $_uri['host']; 586 foreach ($this->trusted_uri as $pattern) { 587 if (preg_match($pattern, $_uri)) { 588 return true; 589 } 590 } 591 } 592 593 throw new SmartyException("URI '{$uri}' not allowed by security setting"); 594 } 595 596 /** 597 * Check if directory of file resource is trusted. 598 * 599 * @param string $filepath 600 * 601 * @return boolean true if directory is trusted 602 * @throws SmartyException if PHP directory is not trusted 603 */ 604 public function isTrustedPHPDir($filepath) 605 { 606 if (empty($this->trusted_dir)) { 607 throw new SmartyException("directory '{$filepath}' not allowed by security setting (no trusted_dir specified)"); 608 } 609 610 // check if index is outdated 611 if (!$this->_trusted_dir || $this->_trusted_dir !== $this->trusted_dir) { 612 $this->_php_resource_dir = array(); 613 614 $this->_trusted_dir = $this->trusted_dir; 615 foreach ((array) $this->trusted_dir as $directory) { 616 $directory = $this->smarty->_realpath($directory . DS, true); 617 $this->_php_resource_dir[$directory] = true; 618 } 619 } 620 621 $this->_php_resource_dir = $this->_checkDir($this->smarty->_realpath($filepath, true), $this->_php_resource_dir); 622 return true; 623 } 624 625 /** 626 * Start template processing 627 * 628 * @param $template 629 * 630 * @throws SmartyException 631 */ 632 public function startTemplate($template) 633 { 634 if ($this->max_template_nesting > 0 && $this->_current_template_nesting ++ >= $this->max_template_nesting) { 635 throw new SmartyException("maximum template nesting level of '{$this->max_template_nesting}' exceeded when calling '{$template->template_resource}'"); 636 } 637 } 638 639 /** 640 * Exit template processing 641 * 642 * @internal param $template 643 */ 644 public function exitTemplate() 645 { 646 if ($this->max_template_nesting > 0) { 647 $this->_current_template_nesting --; 648 } 649 } 650 651 /** 652 * Check if file is inside a valid directory 653 * 654 * @param string $filepath 655 * @param array $dirs valid directories 656 * 657 * @return array 658 * @throws \SmartyException 659 */ 660 private function _checkDir($filepath, $dirs) 661 { 662 $directory = dirname($filepath) . DS; 663 $_directory = array(); 664 while (true) { 665 // remember the directory to add it to _resource_dir in case we're successful 666 $_directory[$directory] = true; 667 // test if the directory is trusted 668 if (isset($dirs[$directory])) { 669 // merge sub directories of current $directory into _resource_dir to speed up subsequent lookup 670 $dirs = array_merge($dirs, $_directory); 671 672 return $dirs; 673 } 674 // abort if we've reached root 675 if (!preg_match('#[\\\/][^\\\/]+[\\\/]$#', $directory)) { 676 break; 677 } 678 // bubble up one level 679 $directory = preg_replace('#[\\\/][^\\\/]+[\\\/]$#', DS, $directory); 680 } 681 682 // give up 683 throw new SmartyException("directory '{$filepath}' not allowed by security setting"); 684 } 685 686 /** 687 * Loads security class and enables security 688 * 689 * @param \Smarty $smarty 690 * @param string|Smarty_Security $security_class if a string is used, it must be class-name 691 * 692 * @return \Smarty current Smarty instance for chaining 693 * @throws \SmartyException when an invalid class name is provided 694 */ 695 public static function enableSecurity(Smarty $smarty, $security_class) 696 { 697 if ($security_class instanceof Smarty_Security) { 698 $smarty->security_policy = $security_class; 699 return; 700 } elseif (is_object($security_class)) { 701 throw new SmartyException("Class '" . get_class($security_class) . "' must extend Smarty_Security."); 702 } 703 if ($security_class == null) { 704 $security_class = $smarty->security_class; 705 } 706 if (!class_exists($security_class)) { 707 throw new SmartyException("Security class '$security_class' is not defined"); 708 } elseif ($security_class !== 'Smarty_Security' && !is_subclass_of($security_class, 'Smarty_Security')) { 709 throw new SmartyException("Class '$security_class' must extend Smarty_Security."); 710 } else { 711 $smarty->security_policy = new $security_class($smarty); 712 } 713 return; 714 } 715} 716