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: */
22: class 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: