1<?php 2 3/* 4 * This file is part of Twig. 5 * 6 * (c) Fabien Potencier 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace Twig\Extension; 13 14use Twig\NodeVisitor\SandboxNodeVisitor; 15use Twig\Sandbox\SecurityNotAllowedMethodError; 16use Twig\Sandbox\SecurityNotAllowedPropertyError; 17use Twig\Sandbox\SecurityPolicyInterface; 18use Twig\Sandbox\SourcePolicyInterface; 19use Twig\Source; 20use Twig\TokenParser\SandboxTokenParser; 21 22final class SandboxExtension extends AbstractExtension 23{ 24 private $sandboxedGlobally; 25 private $sandboxed; 26 private $policy; 27 private $sourcePolicy; 28 29 public function __construct(SecurityPolicyInterface $policy, $sandboxed = false, ?SourcePolicyInterface $sourcePolicy = null) 30 { 31 if (null !== $sourcePolicy) { 32 trigger_deprecation('twig/twig', '3.27.0', 'The "%s" interface is deprecated with no replacement, do not pass an instance to "%s".', SourcePolicyInterface::class, self::class); 33 } 34 35 $this->policy = $policy; 36 $this->sandboxedGlobally = $sandboxed; 37 $this->sourcePolicy = $sourcePolicy; 38 } 39 40 public function getTokenParsers(): array 41 { 42 return [new SandboxTokenParser()]; 43 } 44 45 public function getNodeVisitors(): array 46 { 47 return [new SandboxNodeVisitor()]; 48 } 49 50 public function enableSandbox(): void 51 { 52 $this->sandboxed = true; 53 } 54 55 public function disableSandbox(): void 56 { 57 $this->sandboxed = false; 58 } 59 60 public function isSandboxed(?Source $source = null): bool 61 { 62 return $this->sandboxedGlobally || $this->sandboxed || $this->isSourceSandboxed($source); 63 } 64 65 public function isSandboxedGlobally(): bool 66 { 67 return $this->sandboxedGlobally; 68 } 69 70 private function isSourceSandboxed(?Source $source): bool 71 { 72 if (null === $source || null === $this->sourcePolicy) { 73 return false; 74 } 75 76 return $this->sourcePolicy->enableSandbox($source); 77 } 78 79 public function setSecurityPolicy(SecurityPolicyInterface $policy): void 80 { 81 $this->policy = $policy; 82 } 83 84 public function getSecurityPolicy(): SecurityPolicyInterface 85 { 86 return $this->policy; 87 } 88 89 public function checkSecurity($tags, $filters, $functions, ?Source $source = null): void 90 { 91 if ($this->isSandboxed($source)) { 92 $this->policy->checkSecurity($tags, $filters, $functions); 93 } 94 } 95 96 public function checkMethodAllowed($obj, $method, int $lineno = -1, ?Source $source = null): void 97 { 98 if ($this->isSandboxed($source)) { 99 try { 100 $this->policy->checkMethodAllowed($obj, $method); 101 } catch (SecurityNotAllowedMethodError $e) { 102 $e->setSourceContext($source); 103 $e->setTemplateLine($lineno); 104 105 throw $e; 106 } 107 } 108 } 109 110 public function checkPropertyAllowed($obj, $property, int $lineno = -1, ?Source $source = null): void 111 { 112 if ($this->isSandboxed($source)) { 113 try { 114 $this->policy->checkPropertyAllowed($obj, $property); 115 } catch (SecurityNotAllowedPropertyError $e) { 116 $e->setSourceContext($source); 117 $e->setTemplateLine($lineno); 118 119 throw $e; 120 } 121 } 122 } 123 124 /** 125 * @throws SecurityNotAllowedMethodError 126 */ 127 public function ensureToStringAllowed($obj, int $lineno = -1, ?Source $source = null) 128 { 129 return $this->doEnsureToStringAllowed($obj, $lineno, $source, new \SplObjectStorage()); 130 } 131 132 /** 133 * Materialises a spread operand and runs the policy on every element. 134 * 135 * @internal 136 * 137 * @throws SecurityNotAllowedMethodError 138 */ 139 public function ensureSpreadAllowed(iterable $obj, int $lineno = -1, ?Source $source = null): array 140 { 141 $seen = new \SplObjectStorage(); 142 if ($obj instanceof \Traversable) { 143 $seen[$obj] = true; 144 $obj = iterator_to_array($obj); 145 } 146 147 $this->ensureToStringAllowedForArray($obj, $lineno, $source, $seen); 148 149 return $obj; 150 } 151 152 private function doEnsureToStringAllowed($obj, int $lineno, ?Source $source, \SplObjectStorage $seen) 153 { 154 if (\is_array($obj)) { 155 $this->ensureToStringAllowedForArray($obj, $lineno, $source, $seen); 156 157 return $obj; 158 } 159 160 if (!$this->isSandboxed($source)) { 161 return $obj; 162 } 163 164 if ($obj instanceof \Stringable) { 165 try { 166 $this->policy->checkMethodAllowed($obj, '__toString'); 167 } catch (SecurityNotAllowedMethodError $e) { 168 $e->setSourceContext($source); 169 $e->setTemplateLine($lineno); 170 171 throw $e; 172 } 173 } 174 175 // Elements yielded by a Traversable may be string-coerced downstream 176 // (e.g. by `join`/`replace`), bypassing the policy. Check them now. 177 if ($obj instanceof \Traversable) { 178 if (isset($seen[$obj])) { 179 return $obj; 180 } 181 $seen[$obj] = true; 182 183 // IteratorAggregate::getIterator() is idempotent, so we can walk 184 // the elements and return the original object: host code typed 185 // against a specific class (e.g. FormView) keeps working. 186 if ($obj instanceof \IteratorAggregate) { 187 foreach ($obj as $v) { 188 $this->doEnsureToStringAllowed($v, $lineno, $source, $seen); 189 } 190 191 return $obj; 192 } 193 194 // Single-pass Iterator/Generator: materialise to validate. 195 $array = iterator_to_array($obj); 196 $this->ensureToStringAllowedForArray($array, $lineno, $source, $seen); 197 198 if (!$obj instanceof \Stringable) { 199 return $array; 200 } 201 } 202 203 return $obj; 204 } 205 206 private function ensureToStringAllowedForArray(array $obj, int $lineno, ?Source $source, \SplObjectStorage $seen, array &$stack = []): void 207 { 208 foreach ($obj as $k => $v) { 209 if (!$v) { 210 continue; 211 } 212 213 if (!\is_array($v)) { 214 $this->doEnsureToStringAllowed($v, $lineno, $source, $seen); 215 continue; 216 } 217 218 if ($r = \ReflectionReference::fromArrayElement($obj, $k)) { 219 if (isset($stack[$r->getId()])) { 220 continue; 221 } 222 223 $stack[$r->getId()] = true; 224 } 225 226 $this->ensureToStringAllowedForArray($v, $lineno, $source, $seen, $stack); 227 } 228 } 229} 230