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\Sandbox;
13
14use Twig\Markup;
15use Twig\Template;
16
17/**
18 * Represents a security policy which need to be enforced when sandbox mode is enabled.
19 *
20 * @author Fabien Potencier <fabien@symfony.com>
21 */
22final class SecurityPolicy implements SecurityPolicyInterface
23{
24    private $allowedTags;
25    private $allowedFilters;
26    private $allowedMethods;
27    private $allowedProperties;
28    private $allowedFunctions;
29    private bool $strict = false;
30
31    public function __construct(array $allowedTags = [], array $allowedFilters = [], array $allowedMethods = [], array $allowedProperties = [], array $allowedFunctions = [])
32    {
33        $this->allowedTags = $allowedTags;
34        $this->allowedFilters = $allowedFilters;
35        $this->setAllowedMethods($allowedMethods);
36        $this->allowedProperties = $allowedProperties;
37        $this->allowedFunctions = $allowedFunctions;
38    }
39
40    public function setAllowedTags(array $tags): void
41    {
42        $this->allowedTags = $tags;
43    }
44
45    public function setAllowedFilters(array $filters): void
46    {
47        $this->allowedFilters = $filters;
48    }
49
50    public function setAllowedMethods(array $methods): void
51    {
52        $this->allowedMethods = [];
53        foreach ($methods as $class => $m) {
54            $this->allowedMethods[$class] = array_map('strtolower', \is_array($m) ? $m : [$m]);
55        }
56    }
57
58    public function setAllowedProperties(array $properties): void
59    {
60        $this->allowedProperties = $properties;
61    }
62
63    public function setAllowedFunctions(array $functions): void
64    {
65        $this->allowedFunctions = $functions;
66    }
67
68    /**
69     * Toggles strict mode.
70     *
71     * In strict mode, the tags and functions that are historically always allowed in a
72     * sandbox (the ``extends`` and ``use`` tags, the ``parent``, ``block``, and
73     * ``attribute`` functions) are no longer implicitly allowed and must be added to the
74     * relevant allow-list to be usable. Use this flag in 3.x to opt-in to the forthcoming
75     * 4.0 behavior and silence the related deprecations.
76     */
77    public function setStrict(bool $strict): void
78    {
79        $this->strict = $strict;
80    }
81
82    public function checkSecurity($tags, $filters, $functions): void
83    {
84        foreach ($tags as $tag) {
85            if (!\in_array($tag, $this->allowedTags, true)) {
86                if (!$this->strict && 'extends' === $tag) {
87                    trigger_deprecation('twig/twig', '3.12', 'The "extends" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed (or enable strict mode on the security policy to opt-in to the 4.0 behavior now).');
88                } elseif (!$this->strict && 'use' === $tag) {
89                    trigger_deprecation('twig/twig', '3.12', 'The "use" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed (or enable strict mode on the security policy to opt-in to the 4.0 behavior now).');
90                } else {
91                    throw new SecurityNotAllowedTagError(\sprintf('Tag "%s" is not allowed.', $tag), $tag);
92                }
93            }
94        }
95
96        foreach ($filters as $filter) {
97            if (!\in_array($filter, $this->allowedFilters, true)) {
98                throw new SecurityNotAllowedFilterError(\sprintf('Filter "%s" is not allowed.', $filter), $filter);
99            }
100        }
101
102        foreach ($functions as $function) {
103            if (!\in_array($function, $this->allowedFunctions, true)) {
104                if (!$this->strict && 'parent' === $function) {
105                    trigger_deprecation('twig/twig', '3.27', 'The "parent" function is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed (or enable strict mode on the security policy to opt-in to the 4.0 behavior now).');
106                } elseif (!$this->strict && 'block' === $function) {
107                    trigger_deprecation('twig/twig', '3.27', 'The "block" function is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed (or enable strict mode on the security policy to opt-in to the 4.0 behavior now).');
108                } elseif (!$this->strict && 'attribute' === $function) {
109                    trigger_deprecation('twig/twig', '3.27', 'The "attribute" function is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed (or enable strict mode on the security policy to opt-in to the 4.0 behavior now).');
110                } else {
111                    throw new SecurityNotAllowedFunctionError(\sprintf('Function "%s" is not allowed.', $function), $function);
112                }
113            }
114        }
115    }
116
117    public function checkMethodAllowed($obj, $method): void
118    {
119        if ($obj instanceof Template || $obj instanceof Markup) {
120            return;
121        }
122
123        $allowed = false;
124        $method = strtolower($method);
125        foreach ($this->allowedMethods as $class => $methods) {
126            if ($obj instanceof $class && \in_array($method, $methods, true)) {
127                $allowed = true;
128                break;
129            }
130        }
131
132        if (!$allowed) {
133            $class = $obj::class;
134            throw new SecurityNotAllowedMethodError(\sprintf('Calling "%s" method on a "%s" object is not allowed.', $method, $class), $class, $method);
135        }
136    }
137
138    public function checkPropertyAllowed($obj, $property): void
139    {
140        $allowed = false;
141        foreach ($this->allowedProperties as $class => $properties) {
142            if ($obj instanceof $class && \in_array($property, \is_array($properties) ? $properties : [$properties], true)) {
143                $allowed = true;
144                break;
145            }
146        }
147
148        if (!$allowed) {
149            $class = $obj::class;
150            throw new SecurityNotAllowedPropertyError(\sprintf('Calling "%s" property on a "%s" object is not allowed.', $property, $class), $class, $property);
151        }
152    }
153}
154