Release 2.1.0 (#4321)
* fix deprecations * remove unused config * replace invalid annotation type with attribute * use AsDoctrineListener to fix deprecation * new ModifiedSubscriber to support custom logic and fix deprecation * removed inheritdoc comment * new ModifiedSubscriber to support custom logic and fix deprecation * cleanup event dispatcher interface * re-order annotation params * one more doctrine based deprecation * fix query to count active timesheets * link to "all times" to identify active timesheets * link icon instead of text * fix "skin" translation in wizard * use duration filter to show duration * added login link command and controller * bump tabler theme to 1.0 * added wizard to force password reset by user * allow to configure that new accounts need to reset their password * prevent uploading twig templates by default * bump composer packages * enable sandbox and basic security measures for custom twig templates for invoice and export * bump to symfony 6.3.5 * allow to export single user reports to excel * removed broken method to reload twig cache * added api parameter to fetch user collection fully serialized * allow to replace or append description via timesheet batch update * show api username above form
This commit is contained in:
@@ -21,9 +21,6 @@ final class Extensions extends AbstractExtension
|
||||
{
|
||||
public const REPORT_DATE = 'Y-m-d';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFilters(): array
|
||||
{
|
||||
return [
|
||||
@@ -37,9 +34,6 @@ final class Extensions extends AbstractExtension
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -33,9 +33,6 @@ final class LocaleFormatExtensions extends AbstractExtension implements LocaleAw
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFilters(): array
|
||||
{
|
||||
return [
|
||||
@@ -74,9 +71,6 @@ final class LocaleFormatExtensions extends AbstractExtension implements LocaleAw
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return [
|
||||
|
||||
@@ -27,9 +27,6 @@ final class PaginationExtension extends AbstractExtension
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFunctions(): array
|
||||
{
|
||||
return [
|
||||
|
||||
48
src/Twig/SecurityPolicy/ChainPolicy.php
Normal file
48
src/Twig/SecurityPolicy/ChainPolicy.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Twig\SecurityPolicy;
|
||||
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
|
||||
final class ChainPolicy implements SecurityPolicyInterface
|
||||
{
|
||||
/** @var array<SecurityPolicyInterface> */
|
||||
private array $policies = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function addPolicy(SecurityPolicyInterface $policy): void
|
||||
{
|
||||
$this->policies[] = $policy;
|
||||
}
|
||||
|
||||
public function checkSecurity($tags, $filters, $functions): void
|
||||
{
|
||||
foreach ($this->policies as $policy) {
|
||||
$policy->checkSecurity($tags, $filters, $functions);
|
||||
}
|
||||
}
|
||||
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
{
|
||||
foreach ($this->policies as $policy) {
|
||||
$policy->checkMethodAllowed($obj, $method);
|
||||
}
|
||||
}
|
||||
|
||||
public function checkPropertyAllowed($obj, $property): void
|
||||
{
|
||||
foreach ($this->policies as $policy) {
|
||||
$policy->checkPropertyAllowed($obj, $property);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/Twig/SecurityPolicy/DefaultPolicy.php
Normal file
30
src/Twig/SecurityPolicy/DefaultPolicy.php
Normal file
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Twig\SecurityPolicy;
|
||||
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
|
||||
/**
|
||||
* The Twig environment needs the sandbox extension, which itself needs a policy to start working.
|
||||
*/
|
||||
final class DefaultPolicy implements SecurityPolicyInterface
|
||||
{
|
||||
public function checkSecurity($tags, $filters, $functions): void
|
||||
{
|
||||
}
|
||||
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
{
|
||||
}
|
||||
|
||||
public function checkPropertyAllowed($obj, $property): void
|
||||
{
|
||||
}
|
||||
}
|
||||
41
src/Twig/SecurityPolicy/ExportPolicy.php
Normal file
41
src/Twig/SecurityPolicy/ExportPolicy.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Twig\SecurityPolicy;
|
||||
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
|
||||
/**
|
||||
* Represents the security policy for custom Twig export templates.
|
||||
*/
|
||||
final class ExportPolicy implements SecurityPolicyInterface
|
||||
{
|
||||
private ChainPolicy $policy;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->policy = new ChainPolicy();
|
||||
$this->policy->addPolicy(new DefaultPolicy());
|
||||
}
|
||||
|
||||
public function checkSecurity($tags, $filters, $functions): void
|
||||
{
|
||||
$this->policy->checkSecurity($tags, $filters, $functions);
|
||||
}
|
||||
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
{
|
||||
$this->policy->checkMethodAllowed($obj, $method);
|
||||
}
|
||||
|
||||
public function checkPropertyAllowed($obj, $property): void
|
||||
{
|
||||
$this->policy->checkPropertyAllowed($obj, $property);
|
||||
}
|
||||
}
|
||||
109
src/Twig/SecurityPolicy/ForbiddenPolicy.php
Normal file
109
src/Twig/SecurityPolicy/ForbiddenPolicy.php
Normal file
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Twig\SecurityPolicy;
|
||||
|
||||
use Twig\Markup;
|
||||
use Twig\Sandbox\SecurityNotAllowedFilterError;
|
||||
use Twig\Sandbox\SecurityNotAllowedFunctionError;
|
||||
use Twig\Sandbox\SecurityNotAllowedMethodError;
|
||||
use Twig\Sandbox\SecurityNotAllowedPropertyError;
|
||||
use Twig\Sandbox\SecurityNotAllowedTagError;
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
use Twig\Template;
|
||||
|
||||
/**
|
||||
* A blocking approach for Twig templates.
|
||||
*/
|
||||
final class ForbiddenPolicy implements SecurityPolicyInterface
|
||||
{
|
||||
/** @var array<string, array<string>> */
|
||||
private array $forbiddenMethods = [];
|
||||
|
||||
/**
|
||||
* @param array<string> $forbiddenTags
|
||||
* @param array<string> $forbiddenFilters
|
||||
* @param array<string, array<string>> $forbiddenMethods
|
||||
* @param array<string, array<string>> $forbiddenProperties
|
||||
* @param array<string> $forbiddenFunctions
|
||||
*/
|
||||
public function __construct(
|
||||
private array $forbiddenTags = [],
|
||||
private array $forbiddenFilters = [],
|
||||
array $forbiddenMethods = [],
|
||||
private array $forbiddenProperties = [],
|
||||
private array $forbiddenFunctions = []
|
||||
)
|
||||
{
|
||||
$this->forbiddenMethods = [];
|
||||
foreach ($forbiddenMethods as $class => $m) {
|
||||
$this->forbiddenMethods[$class] = array_map(function ($value) { return strtr($value, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz'); }, \is_array($m) ? $m : [$m]);
|
||||
}
|
||||
}
|
||||
|
||||
public function checkSecurity($tags, $filters, $functions): void
|
||||
{
|
||||
foreach ($tags as $tag) {
|
||||
if (\in_array($tag, $this->forbiddenTags)) {
|
||||
throw new SecurityNotAllowedTagError(sprintf('Tag "%s" is not allowed.', $tag), $tag);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($filters as $filter) {
|
||||
if (\in_array($filter, $this->forbiddenFilters)) {
|
||||
throw new SecurityNotAllowedFilterError(sprintf('Filter "%s" is not allowed.', $filter), $filter);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($functions as $function) {
|
||||
if (\in_array($function, $this->forbiddenFunctions)) {
|
||||
throw new SecurityNotAllowedFunctionError(sprintf('Function "%s" is not allowed.', $function), $function);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
{
|
||||
if ($obj instanceof Template || $obj instanceof Markup) {
|
||||
return;
|
||||
}
|
||||
|
||||
$forbidden = false;
|
||||
$method = strtr($method, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz');
|
||||
foreach ($this->forbiddenMethods as $class => $methods) {
|
||||
if ($obj instanceof $class) {
|
||||
$forbidden = \in_array($method, $methods);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($forbidden) {
|
||||
$class = \get_class($obj);
|
||||
throw new SecurityNotAllowedMethodError(sprintf('Calling "%s" method on a "%s" object is not allowed.', $method, $class), $class, $method);
|
||||
}
|
||||
}
|
||||
|
||||
public function checkPropertyAllowed($obj, $property): void
|
||||
{
|
||||
$forbidden = false;
|
||||
foreach ($this->forbiddenProperties as $class => $properties) {
|
||||
if ($obj instanceof $class) {
|
||||
$forbidden = \in_array($property, \is_array($properties) ? $properties : [$properties]);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($forbidden) {
|
||||
$class = \get_class($obj);
|
||||
throw new SecurityNotAllowedPropertyError(sprintf('Calling "%s" property on a "%s" object is not allowed.', $property, $class), $class, $property);
|
||||
}
|
||||
}
|
||||
}
|
||||
86
src/Twig/SecurityPolicy/InvoicePolicy.php
Normal file
86
src/Twig/SecurityPolicy/InvoicePolicy.php
Normal file
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Twig\SecurityPolicy;
|
||||
|
||||
use App\Invoice\InvoiceModel;
|
||||
use App\Pdf\PdfContext;
|
||||
use Symfony\Component\String\UnicodeString;
|
||||
use Twig\Markup;
|
||||
use Twig\Sandbox\SecurityPolicy;
|
||||
use Twig\Sandbox\SecurityPolicyInterface;
|
||||
use Twig\Template;
|
||||
|
||||
/**
|
||||
* Represents the security policy for custom Twig invoice templates.
|
||||
*/
|
||||
final class InvoicePolicy implements SecurityPolicyInterface
|
||||
{
|
||||
private ChainPolicy $policy;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->policy = new ChainPolicy();
|
||||
$this->policy->addPolicy(new DefaultPolicy());
|
||||
$this->policy->addPolicy(new SecurityPolicy(
|
||||
['block', 'if', 'for', 'set', 'extends'],
|
||||
[
|
||||
// Twig core filters
|
||||
'map', 'escape', 'trans', 'default', 'nl2br', 'trim', 'raw',
|
||||
'join', 'u', 'slice', 'date', 'month_name', 'first', 'country_name',
|
||||
'replace', 'length', 'number_format', 'split',
|
||||
|
||||
// Kimai filters
|
||||
'md2html', 'desc2html', 'comment2html', 'comment1line', 'multiline_indent', 'nl2str',
|
||||
'date_short', 'duration', 'amount', 'money', 'duration_decimal',
|
||||
],
|
||||
[
|
||||
PdfContext::class => ['setoption'],
|
||||
InvoiceModel::class => ['toarray'],
|
||||
],
|
||||
[], // properties
|
||||
[
|
||||
// Twig core functions
|
||||
'cycle', 'asset', 'range',
|
||||
|
||||
// Kimai functions
|
||||
'encore_entry_css_source', 'qr_code_data_uri', 'config',
|
||||
]
|
||||
));
|
||||
}
|
||||
|
||||
public function checkSecurity($tags, $filters, $functions): void
|
||||
{
|
||||
$this->policy->checkSecurity($tags, $filters, $functions);
|
||||
}
|
||||
|
||||
public function checkMethodAllowed($obj, $method): void
|
||||
{
|
||||
if ($obj instanceof Template || $obj instanceof Markup || $obj instanceof UnicodeString) {
|
||||
return;
|
||||
}
|
||||
|
||||
$lm = strtolower($method);
|
||||
|
||||
if (str_starts_with($lm, 'get') || str_starts_with($lm, 'is') || str_starts_with($lm, 'has')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($lm === '__tostring') {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->policy->checkMethodAllowed($obj, $method);
|
||||
}
|
||||
|
||||
public function checkPropertyAllowed($obj, $property): void
|
||||
{
|
||||
$this->policy->checkPropertyAllowed($obj, $property);
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,11 @@
|
||||
|
||||
namespace App\Twig;
|
||||
|
||||
use App\Twig\SecurityPolicy\InvoicePolicy;
|
||||
use Symfony\Bridge\Twig\Extension\TranslationExtension;
|
||||
use Symfony\Contracts\Translation\LocaleAwareInterface;
|
||||
use Twig\Environment;
|
||||
use Twig\Extension\SandboxExtension;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@@ -30,6 +32,13 @@ trait TwigRendererTrait
|
||||
$previousFormatLocale = $this->switchFormatLocale($twig, $formatLocale);
|
||||
}
|
||||
|
||||
// enable basic security measures
|
||||
if (!$twig->hasExtension(SandboxExtension::class)) {
|
||||
$sandbox = new SandboxExtension(new InvoicePolicy());
|
||||
$sandbox->enableSandbox();
|
||||
$twig->addExtension($sandbox);
|
||||
}
|
||||
|
||||
$content = $twig->render($template, $options);
|
||||
|
||||
if ($previousTranslation !== null) {
|
||||
|
||||
Reference in New Issue
Block a user