Skip to content
Permalink
Browse files

added sandbox mode

  • Loading branch information
dg committed May 15, 2016
1 parent 2eee071 commit 90165f2797ec78fe04872aaab8e64cbd92892c6b
@@ -95,6 +95,9 @@ class Compiler
/** @var array of [name => serialized value] */
private $properties = [];

/** @var Policy|null */
private $policy;


/**
* Adds new macro with Macro flags.
@@ -204,6 +207,20 @@ public function compile(array $tokens, string $className): string
}


/** @return static */
public function setPolicy(?Policy $policy)
{
$this->policy = $policy;
return $this;
}


public function getPolicy(): ?Policy
{
return $this->policy;
}


/** @return static */
public function setContentType(string $type)
{
@@ -707,6 +724,9 @@ public function expandMacro(string $name, string $args, string $modifiers = null
$hint = (($t = Helpers::getSuggestion(array_keys($this->macros), $name)) ? ", did you mean {{$t}}?" : '')
. (in_array($this->context, [self::CONTEXT_HTML_JS, self::CONTEXT_HTML_CSS], true) ? ' (in JavaScript or CSS, try to put a space after bracket or use n:syntax=off)' : '');
throw new CompileException("Unknown macro {{$name}}$hint");

} elseif ($this->policy && !$this->policy->isMacroAllowed($name)) {
throw new SecurityViolation("Macro {{$name}} is not allowed.");
}

$modifiers = (string) $modifiers;
@@ -26,6 +26,9 @@ class PhpWriter
/** @var array|null */
private $context;

/** @var Policy|null */
private $policy;

/** @var array */
private $functions = [];

@@ -35,6 +38,7 @@ public static function using(MacroNode $node, Compiler $compiler = null): self
$me = new static($node->tokenizer, null, $node->context);
$me->modifiers = &$node->modifiers;
$me->functions = $compiler ? $compiler->getFunctions() : [];
$me->policy = $compiler ? $compiler->getPolicy() : null;
return $me;
}

@@ -165,6 +169,7 @@ public function preprocess(MacroTokens $tokens = null): MacroTokens
$tokens = $this->removeCommentsPass($tokens);
$tokens = $this->replaceFunctionsPass($tokens);
$tokens = $this->optionalChainingPass($tokens);
$tokens = $this->sandboxPass($tokens);
$tokens = $this->shortTernaryPass($tokens);
$tokens = $this->inlineModifierPass($tokens);
$tokens = $this->inOperatorPass($tokens);
@@ -432,6 +437,109 @@ public function inOperatorPass(MacroTokens $tokens): MacroTokens
}


/**
* Applies sandbox policy.
*/
public function sandboxPass(MacroTokens $tokens): MacroTokens
{
static $keywords = [
'array' => 1, 'catch' => 1, 'clone' => 1, 'empty' => 1, 'for' => 1,
'foreach' => 1, 'function' => 1, 'if' => 1, 'elseif', 'isset' => 1, 'list' => 1, 'unset' => 1,
];

if (!$this->policy) {
return $tokens;
}

$startDepth = $tokens->depth;
$res = new MacroTokens;

while ($tokens->depth >= $startDepth && $tokens->nextToken()) {
$symbol = false;
if ($tokens->isCurrent('[', '(')) {
$expr = new MacroTokens(array_merge([$tokens->currentToken()], $this->sandboxPass($tokens)->tokens));

} elseif ($tokens->isCurrent($tokens::T_SYMBOL, '\\') && empty($keywords[$tokens->currentValue()])) {
$expr = new MacroTokens(array_merge([$tokens->currentToken()], $tokens->nextAll($tokens::T_SYMBOL, '\\')));
$symbol = true;
} elseif ($tokens->isCurrent($tokens::T_VARIABLE, $tokens::T_STRING)) {
$expr = new MacroTokens([$tokens->currentToken()]);
} elseif ($tokens->isCurrent('$')) {
$expr = new MacroTokens(array_merge([$tokens->currentToken()], $tokens->nextAll($tokens::T_VARIABLE, '$')));
} else {
$res->append($tokens->currentToken());
continue;
}

do {
if ($tokens->nextToken('(')) {
if ($symbol) {
$name = $expr->joinAll();
if (!$this->policy->isFunctionAllowed($name)) {
throw new SecurityViolation("Function $name() is not allowed.");
}
$symbol = false;
$expr->append('(');
} else {
$expr->prepend('$this->call(');
$expr->append(')(');
}

} elseif ($tokens->nextToken('->', '::')) {
$op = $tokens->currentValue();
if ($symbol) {
$expr->append('::class');
$symbol = false;
}
$expr->append(', ');

if ($tokens->nextToken('{')) {
$member = array_merge([$tokens->currentToken()], $this->sandboxPass($tokens)->tokens);
$expr->append('(string) ');
$expr->tokens = array_merge($expr->tokens, array_slice($member, 1, -1));
} elseif ($tokens->nextToken($tokens::T_SYMBOL)) {
$member = [$tokens->currentToken()];
$expr->append(var_export($tokens->currentValue(), true));
} elseif ($tokens->nextToken($tokens::T_VARIABLE)) {
$member = [$tokens->currentToken()];
if ($op === '::' && !$tokens->isNext('(')) {
$expr->append(var_export(substr($tokens->currentValue(), 1), true));
} else {
$expr->append($tokens->currentValue());
}
} else {
$member = $tokens->nextAll($tokens::T_VARIABLE, '$');
if ($op === '::' && !$tokens->isNext('(')) {
$expr->tokens = array_merge($expr->tokens, array_slice($member, 1));
} else {
$expr->tokens = array_merge($expr->tokens, $member);
}
}

if ($tokens->nextToken('(')) {
$expr->prepend('$this->call([');
$expr->append('])(');
} else {
$expr->prepend('$this->prop(');
$expr->append(')' . $op);
$expr->tokens = array_merge($expr->tokens, $member);
}

} elseif ($tokens->nextToken('[', '{')) {
$expr->tokens = array_merge($expr->tokens, [$tokens->currentToken()], $this->sandboxPass($tokens)->tokens);

} else {
break;
}
} while (true);

$res->tokens = array_merge($res->tokens, $expr->tokens);
}

return $res;
}


/**
* Process inline filters ($var|filter)
*/
@@ -531,7 +639,11 @@ public function modifierPass(MacroTokens $tokens, $var, bool $isContent = false)
$res->prepend('LR\Filters::safeUrl(');
$inside = true;
} else {
$name = strtolower($tokens->currentValue());
$name = $tokens->currentValue();
if ($this->policy && !$this->policy->isFilterAllowed($name)) {
throw new SecurityViolation("Filter |$name is not allowed.");
}
$name = strtolower($name);
$res->prepend($isContent
? '$this->filters->filterContent(' . var_export($name, true) . ', $_fi, '
: '($this->filters->' . $name . ')('
@@ -63,6 +63,12 @@ class Engine
/** @var bool */
private $strictTypes = false;

/** @var Policy|null */
private $policy;

/** @var bool */
private $sandboxed = false;


public function __construct()
{
@@ -104,7 +110,7 @@ public function createTemplate(string $name, array $params = []): Runtime\Templa
$this->loadTemplate($name);
}
$this->providers['_fn'] = $this->functions;
return new $class($this, $params, $this->filters, $this->providers, $name);
return new $class($this, $params, $this->filters, $this->providers, $name, $this->sandboxed ? $this->policy : null);
}


@@ -128,11 +134,12 @@ public function compile(string $name): string
$code = $this->getCompiler()
->setContentType($this->contentType)
->setFunctions(array_keys((array) $this->functions))
->setPolicy($this->sandboxed ? $this->policy : null)
->compile($tokens, $this->getTemplateClass($name));

} catch (\Exception $e) {
if (!$e instanceof CompileException) {
$e = new CompileException("Thrown exception '{$e->getMessage()}'", 0, $e);
$e = new CompileException($e instanceof SecurityViolation ? $e->getMessage() : "Thrown exception '{$e->getMessage()}'", 0, $e);
}
$line = isset($tokens) ? $this->getCompiler()->getLine() : $this->getParser()->getLine();
throw $e->setSource($source, $line, $name);
@@ -232,7 +239,7 @@ public function getCacheFile(string $name): string

public function getTemplateClass(string $name): string
{
$key = $this->getLoader()->getUniqueId($name) . "\00" . self::VERSION;
$key = $this->getLoader()->getUniqueId($name) . "\00" . self::VERSION . "\00" . (int) $this->sandboxed;
return 'Template' . substr(md5($key), 0, 10);
}

@@ -310,6 +317,22 @@ public function getProviders(): array
}


/** @return static */
public function setPolicy(?Policy $policy)
{
$this->policy = $policy;
return $this;
}


/** @return static */
public function setSandboxMode(bool $on = true)
{
$this->sandboxed = $on;
return $this;
}


/** @return static */
public function setContentType(string $type)
{
@@ -62,6 +62,7 @@ public static function install(Latte\Compiler $compiler): void
$me->addMacro('capture', [$me, 'macroCapture'], [$me, 'macroCaptureEnd']);
$me->addMacro('spaceless', [$me, 'macroSpaceless'], [$me, 'macroSpaceless']);
$me->addMacro('include', [$me, 'macroInclude']);
$me->addMacro('sandbox', [$me, 'macroInclude']);
$me->addMacro('contentType', [$me, 'macroContentType'], null, null, self::ALLOWED_IN_HEAD);
$me->addMacro('php', [$me, 'macroExpr']);

@@ -212,6 +213,7 @@ public function macroTranslate(MacroNode $node, PhpWriter $writer)

/**
* {include "file" [,] [params]}
* {sandbox "file" [,] [params]}
*/
public function macroInclude(MacroNode $node, PhpWriter $writer)
{
@@ -225,7 +227,8 @@ public function macroInclude(MacroNode $node, PhpWriter $writer)
}
return $writer->write(
'/* line ' . $node->startLine . ' */
$this->createTemplate(%node.word, %node.array? + $this->params, "include")->renderToContentType(%raw);',
$this->createTemplate(%node.word, %node.array? + $this->params, %var)->renderToContentType(%raw);',
$node->name,
$node->modifiers
? $writer->write('function ($s, $type) { $_fi = new LR\FilterInfo($type); return %modifyContent($s); }')
: var_export($noEscape ? null : implode($node->context), true)
@@ -0,0 +1,24 @@
<?php

/**
* This file is part of the Latte (https://latte.nette.org)
* Copyright (c) 2008 David Grudl (https://davidgrudl.com)
*/

declare(strict_types=1);

namespace Latte;


interface Policy
{
function isMacroAllowed(string $macro): bool;

function isFilterAllowed(string $filter): bool;

function isFunctionAllowed(string $function): bool;

function isMethodAllowed(string $class, string $method): bool;

function isPropertyAllowed(string $class, string $property): bool;
}
@@ -11,6 +11,7 @@

use Latte;
use Latte\Engine;
use Latte\Policy;


/**
@@ -50,19 +51,23 @@ class Template
/** @var string */
private $name;

/** @var Policy|null */
private $policy;

/** @var Template|null @internal */
private $referringTemplate;

/** @var string|null @internal */
private $referenceType;


public function __construct(Engine $engine, array $params, FilterExecutor $filters, array $providers, string $name)
public function __construct(Engine $engine, array $params, FilterExecutor $filters, array $providers, string $name, ?Policy $policy)
{
$this->engine = $engine;
$this->params = $params;
$this->filters = $filters;
$this->name = $name;
$this->policy = $policy;
$this->global = (object) $providers;
foreach ($this->blocks as $nm => $method) {
$this->blockQueue[$nm][] = [$this, $method];
@@ -187,7 +192,11 @@ public function render(): void
public function createTemplate(string $name, array $params, string $referenceType): self
{
$name = $this->engine->getLoader()->getReferredName($name, $this->name);
$child = $this->engine->createTemplate($name, $params);
if ($referenceType === 'sandbox') {
$child = (clone $this->engine)->setSandboxMode()->createTemplate($name, $params);
} else {
$child = $this->engine->createTemplate($name, $params);
}
$child->referringTemplate = $this;
$child->referenceType = $referenceType;
$child->global = $this->global;
@@ -303,4 +312,41 @@ public function capture(callable $function): string
$this->global->coreCaptured = false;
}
}


/** @internal */
public function call($callable)
{
if (is_string($callable)) {
$parts = explode('::', $callable);
$allowed = count($parts) === 1
? $this->policy->isFunctionAllowed($parts[0])
: $this->policy->isMethodAllowed(...$parts);
} elseif (is_array($callable)) {
$allowed = $this->policy->isMethodAllowed(is_object($callable[0]) ? get_class($callable[0]) : $callable[0], $callable[1]);
} elseif (is_object($callable)) {
$allowed = $callable instanceof \Closure
? true
: $this->policy->isMethodAllowed(get_class($callable), '__invoke');
} else {
$allowed = false;
}

if (!$allowed) {
is_callable($callable, false, $text);
throw new Latte\SecurityViolation("Calling $text() is not allowed.");
}
return $callable;
}


/** @internal */
public function prop($obj, $prop)
{
$class = is_object($obj) ? get_class($obj) : $obj;
if (is_string($class) && !$this->policy->isPropertyAllowed($class, $prop)) {
throw new Latte\SecurityViolation("Access to '$prop' property on a $class object is not allowed.");
}
return $obj;
}
}

0 comments on commit 90165f2

Please sign in to comment.
You can’t perform that action at this time.