Release 2.46 (#5757)

This commit is contained in:
Kevin Papst
2026-01-07 00:59:47 +01:00
committed by GitHub
parent 9e87fc131b
commit 6a86afb5fd
55 changed files with 1582 additions and 1195 deletions

View File

@@ -17,11 +17,11 @@ final class Constants
/**
* The current release version
*/
public const VERSION = '2.45.0';
public const VERSION = '2.46.0';
/**
* The current release: major * 10000 + minor * 100 + patch
*/
public const VERSION_ID = 24500;
public const VERSION_ID = 24600;
/**
* The software name
*/

View File

@@ -23,7 +23,8 @@ trait ColorTrait
* The assigned color in HTML hex format, e.g. #dd1d00
*/
#[ORM\Column(name: 'color', type: Types::STRING, length: 7, nullable: true)]
#[Serializer\Exclude]
#[Serializer\Expose]
#[Serializer\Groups(['Default'])]
#[Exporter\Expose(label: 'color')]
#[Constraints\HexColor]
private ?string $color = null;
@@ -50,10 +51,10 @@ trait ColorTrait
abstract public function getName(): ?string;
/**
* Internal value: this color will never be empty and is generated by the tag name if not set explicit.
* Color will never be empty and is generated from the entity tag name if not set explicit.
*/
#[Serializer\VirtualProperty]
#[Serializer\SerializedName('color')]
#[Serializer\SerializedName('color-safe')]
#[Serializer\Groups(['Default'])]
public function getColorSafe(): string
{

View File

@@ -117,7 +117,11 @@ class HtmlRenderer implements ExportRendererInterface
'userPreferences' => $userPreferences,
], $this->getOptions($query)));
// allows to run in development mode, otherwise toolbar would be blocked
$sandbox->disableSandbox();
$response = new Response();
$response->headers->set('Content-Type', 'text/html');
$response->setContent($content);
return $response;

View File

@@ -16,10 +16,6 @@ final class ChainPolicy implements SecurityPolicyInterface
/** @var array<SecurityPolicyInterface> */
private array $policies = [];
public function __construct()
{
}
public function addPolicy(SecurityPolicyInterface $policy): void
{
$this->policies[] = $policy;

View File

@@ -9,6 +9,13 @@
namespace App\Twig\SecurityPolicy;
use App\Entity\User;
use App\Pdf\PdfContext;
use Symfony\Bridge\Twig\AppVariable;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\ServerBag;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
use Twig\Sandbox\SecurityNotAllowedMethodError;
use Twig\Sandbox\SecurityPolicyInterface;
/**
@@ -22,6 +29,49 @@ final class DefaultPolicy implements SecurityPolicyInterface
public function checkMethodAllowed($obj, $method): void
{
if ($obj instanceof ServerBag) {
throw new SecurityNotAllowedMethodError('Tried to access server environment', ServerBag::class, $method);
}
if ($obj instanceof SessionInterface) {
throw new SecurityNotAllowedMethodError('Tried to access session', SessionInterface::class, $method);
}
$lcm = strtolower($method);
if ($obj instanceof PdfContext) {
if ($lcm !== 'setoption') {
throw new SecurityNotAllowedMethodError('Tried to access forbidden method on PdfContext', PdfContext::class, $method);
}
return;
}
if (!str_starts_with($lcm, 'has') && !str_starts_with($lcm, 'is') && !str_starts_with($lcm, 'get') && $lcm !== '__tostring') {
throw new SecurityNotAllowedMethodError('Tried to access non-read method', $obj::class, $method);
}
if ($obj instanceof Request) {
if (!str_starts_with($lcm, 'get')) {
throw new SecurityNotAllowedMethodError('Tried to call setter() of app variable', AppVariable::class, $method);
}
return;
}
if ($obj instanceof AppVariable) {
if (!\in_array($lcm, ['getrequest', 'getuser', 'getlocale'], true)) {
throw new SecurityNotAllowedMethodError('Tried to access forbidden app variable method', User::class, $method);
}
return;
}
if ($obj instanceof User) {
if (\in_array($lcm, ['getpassword', 'gettotpsecret', 'getplainpassword', 'getconfirmationtoken', 'gettotpauthenticationconfiguration'], true)) {
throw new SecurityNotAllowedMethodError('Tried to access user secrets', User::class, $method);
}
}
}
public function checkPropertyAllowed($obj, $property): void

View File

@@ -1,109 +0,0 @@
<?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 readonly array $forbiddenTags = [],
private readonly array $forbiddenFilters = [],
array $forbiddenMethods = [],
private readonly array $forbiddenProperties = [],
private readonly 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) { // @phpstan-ignore instanceof.internalClass
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);
}
}
}

View File

@@ -22,13 +22,13 @@ use Twig\Template;
*/
final class InvoicePolicy implements SecurityPolicyInterface
{
private ChainPolicy $policy;
private SecurityPolicyInterface $default;
private SecurityPolicyInterface $security;
public function __construct()
{
$this->policy = new ChainPolicy();
$this->policy->addPolicy(new DefaultPolicy());
$this->policy->addPolicy(new SecurityPolicy(
$this->default = new DefaultPolicy();
$this->security = new SecurityPolicy(
['block', 'if', 'for', 'set', 'extends', 'import'],
[
// =================================================================
@@ -194,12 +194,13 @@ final class InvoicePolicy implements SecurityPolicyInterface
'month_names',
'locale_format',
]
));
);
}
public function checkSecurity($tags, $filters, $functions): void
{
$this->policy->checkSecurity($tags, $filters, $functions);
$this->default->checkSecurity($tags, $filters, $functions);
$this->security->checkSecurity($tags, $filters, $functions);
}
public function checkMethodAllowed($obj, $method): void
@@ -208,21 +209,20 @@ final class InvoicePolicy implements SecurityPolicyInterface
return;
}
$this->default->checkMethodAllowed($obj, $method);
$lm = strtolower($method);
if (str_starts_with($lm, 'get') || str_starts_with($lm, 'is') || str_starts_with($lm, 'has')) {
if (str_starts_with($lm, 'get') || str_starts_with($lm, 'is') || str_starts_with($lm, 'has') || $lm === '__tostring') {
return;
}
if ($lm === '__tostring') {
return;
}
$this->policy->checkMethodAllowed($obj, $method);
$this->security->checkMethodAllowed($obj, $method);
}
public function checkPropertyAllowed($obj, $property): void
{
$this->policy->checkPropertyAllowed($obj, $property);
$this->default->checkPropertyAllowed($obj, $property);
$this->security->checkPropertyAllowed($obj, $property);
}
}