mirror of
https://github.com/twigphp/Twig.git
synced 2026-09-06 23:48:50 +00:00
Enforce the sandbox Markup exception for every security policy
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
# 4.0.0 (2026-XX-XX)
|
||||
|
||||
* Add the `isAlwaysAllowedInSandbox()` method to `Twig\TwigCallableInterface` and `Twig\TokenParser\TokenParserInterface`
|
||||
* Always allow printing a `Markup` object in a sandbox, whatever the security policy is
|
||||
* Remove the `Twig\Sandbox\SourcePolicyInterface` interface and the corresponding argument of `Twig\Extension\SandboxExtension::__construct()`
|
||||
* Enforce the `parent`, `block`, and `attribute` functions against the sandbox `allowedFunctions` allow-list
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
namespace Twig\Extension;
|
||||
|
||||
use Twig\Markup;
|
||||
use Twig\NodeVisitor\SandboxNodeVisitor;
|
||||
use Twig\Sandbox\SecurityNotAllowedMethodError;
|
||||
use Twig\Sandbox\SecurityNotAllowedPropertyError;
|
||||
@@ -139,7 +140,9 @@ final class SandboxExtension extends AbstractExtension
|
||||
return $obj;
|
||||
}
|
||||
|
||||
if ($obj instanceof \Stringable) {
|
||||
// Markup carries content that Twig already considers safe, so its
|
||||
// __toString() is always allowed, whatever the security policy is.
|
||||
if ($obj instanceof \Stringable && !$obj instanceof Markup) {
|
||||
try {
|
||||
$this->policy->checkMethodAllowed($obj, '__toString');
|
||||
} catch (SecurityNotAllowedMethodError $e) {
|
||||
|
||||
+5
-8
@@ -14,14 +14,11 @@ namespace Twig;
|
||||
/**
|
||||
* Marks a content as safe.
|
||||
*
|
||||
* Instances of this class (and any subclass) are trusted by the Twig
|
||||
* sandbox: method calls and property accesses on a Markup instance bypass
|
||||
* the SecurityPolicy method/property allowlists. This is by design: Markup
|
||||
* represents content that has already been deemed safe to output.
|
||||
*
|
||||
* As a consequence, when extending this class, you are responsible for
|
||||
* ensuring that every method and property exposed by your subclass is
|
||||
* safe to call from a sandboxed template.
|
||||
* Instances of this class are trusted by the Twig sandbox when output as
|
||||
* strings: their __toString() method is always allowed. This is by design as
|
||||
* Markup represents content that has already been deemed safe to output.
|
||||
* Regular method calls and property accesses are still controlled by the
|
||||
* SecurityPolicy method/property allowlists.
|
||||
*
|
||||
* @author Fabien Potencier <fabien@symfony.com>
|
||||
*/
|
||||
|
||||
@@ -11,9 +11,6 @@
|
||||
|
||||
namespace Twig\Sandbox;
|
||||
|
||||
use Twig\Markup;
|
||||
use Twig\Template;
|
||||
|
||||
/**
|
||||
* Represents a security policy which need to be enforced when sandbox mode is enabled.
|
||||
*
|
||||
@@ -115,10 +112,6 @@ final class SecurityPolicy implements SecurityPolicyInterface
|
||||
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
{
|
||||
if ($obj instanceof Template || $obj instanceof Markup) {
|
||||
return;
|
||||
}
|
||||
|
||||
$allowed = false;
|
||||
$method = strtolower($method);
|
||||
foreach ($this->allowedMethods as $class => $methods) {
|
||||
|
||||
@@ -28,6 +28,7 @@ use Twig\Error\SyntaxError;
|
||||
use Twig\Extension\SandboxExtension;
|
||||
use Twig\Extension\StringLoaderExtension;
|
||||
use Twig\Loader\ArrayLoader;
|
||||
use Twig\Markup;
|
||||
use Twig\Node\Expression\ConstantExpression;
|
||||
use Twig\Node\Node;
|
||||
use Twig\Node\Nodes;
|
||||
@@ -40,9 +41,11 @@ use Twig\Sandbox\SecurityNotAllowedMethodError;
|
||||
use Twig\Sandbox\SecurityNotAllowedPropertyError;
|
||||
use Twig\Sandbox\SecurityNotAllowedTagError;
|
||||
use Twig\Sandbox\SecurityPolicy;
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
use Twig\Source;
|
||||
use Twig\Token;
|
||||
use Twig\TokenParser\AbstractTokenParser;
|
||||
use Twig\TokenParser\TokenParserInterface;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
use Twig\TwigTest;
|
||||
@@ -661,6 +664,39 @@ class SandboxTest extends TestCase
|
||||
$this->assertEquals(1, FooObject::$called['__toString'], 'Sandbox only calls method once');
|
||||
}
|
||||
|
||||
public function testSandboxAllowsPrintingMarkup()
|
||||
{
|
||||
$twig = $this->getEnvironment(true, [], ['index' => '{{ markup }}']);
|
||||
|
||||
$this->assertSame('<b>safe</b>', $twig->load('index')->render(['markup' => new Markup('<b>safe</b>', 'UTF-8')]));
|
||||
}
|
||||
|
||||
public function testSandboxAllowsPrintingMarkupWithACustomPolicyThatAllowsNothing()
|
||||
{
|
||||
$loader = new ArrayLoader(['index' => '{{ markup }}']);
|
||||
$twig = new Environment($loader, ['cache' => false, 'autoescape' => false]);
|
||||
$twig->addExtension(new SandboxExtension(new DenyEverythingSecurityPolicy(), true));
|
||||
|
||||
$this->assertSame('<b>safe</b>', $twig->load('index')->render(['markup' => new Markup('<b>safe</b>', 'UTF-8')]));
|
||||
}
|
||||
|
||||
public function testSandboxAppliesThePolicyToTemplateMethods()
|
||||
{
|
||||
$template = $this->getEnvironment(true, [], ['index' => 'foo'])->load('index')->unwrap();
|
||||
$policy = new SecurityPolicy();
|
||||
|
||||
$this->expectException(SecurityNotAllowedMethodError::class);
|
||||
$policy->checkMethodAllowed($template, 'getTemplateName');
|
||||
}
|
||||
|
||||
public function testSandboxAppliesThePolicyToMarkupMethods()
|
||||
{
|
||||
$twig = $this->getEnvironment(true, [], ['index' => '{{ markup.getCharset() }}']);
|
||||
|
||||
$this->expectException(SecurityNotAllowedMethodError::class);
|
||||
$twig->load('index')->render(['markup' => new Markup('<b>safe</b>', 'UTF-8')]);
|
||||
}
|
||||
|
||||
public function testSandboxUnallowedFunction()
|
||||
{
|
||||
$twig = $this->getEnvironment(true, [], self::$templates);
|
||||
@@ -1730,3 +1766,20 @@ class GatedSandboxTokenParser extends AbstractTokenParser
|
||||
return 'gated_tag';
|
||||
}
|
||||
}
|
||||
|
||||
class DenyEverythingSecurityPolicy implements SecurityPolicyInterface
|
||||
{
|
||||
public function checkSecurity($tags, $filters, $functions): void
|
||||
{
|
||||
}
|
||||
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
{
|
||||
throw new SecurityNotAllowedMethodError(\sprintf('Calling "%s" method on a "%s" object is not allowed.', $method, $obj::class), $obj::class, $method);
|
||||
}
|
||||
|
||||
public function checkPropertyAllowed($obj, $property): void
|
||||
{
|
||||
throw new SecurityNotAllowedPropertyError(\sprintf('Calling "%s" property on a "%s" object is not allowed.', $property, $obj::class), $obj::class, $property);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user