N°8017 - Security - dependabot - Symfony's VarDumper vulnerable to un… (#731)

Upgrade all Symfony components to last security fix (~6.4.0)
This commit is contained in:
Benjamin Dalsass
2025-08-06 08:54:56 +02:00
committed by GitHub
parent 603340b852
commit cdbcd14767
608 changed files with 5020 additions and 3793 deletions

View File

@@ -28,13 +28,13 @@ final class ProxyHelper
public static function generateLazyGhost(\ReflectionClass $class): string
{
if (\PHP_VERSION_ID >= 80200 && \PHP_VERSION_ID < 80300 && $class->isReadOnly()) {
throw new LogicException(sprintf('Cannot generate lazy ghost: class "%s" is readonly.', $class->name));
throw new LogicException(sprintf('Cannot generate lazy ghost with PHP < 8.3: class "%s" is readonly.', $class->name));
}
if ($class->isFinal()) {
throw new LogicException(sprintf('Cannot generate lazy ghost: class "%s" is final.', $class->name));
}
if ($class->isInterface() || $class->isAbstract()) {
throw new LogicException(sprintf('Cannot generate lazy ghost: "%s" is not a concrete class.', $class->name));
if ($class->isInterface() || $class->isAbstract() || $class->isTrait()) {
throw new LogicException(\sprintf('Cannot generate lazy ghost: "%s" is not a concrete class.', $class->name));
}
if (\stdClass::class !== $class->name && $class->isInternal()) {
throw new LogicException(sprintf('Cannot generate lazy ghost: class "%s" is internal.', $class->name));
@@ -58,7 +58,48 @@ final class ProxyHelper
throw new LogicException(sprintf('Cannot generate lazy ghost: class "%s" extends "%s" which is internal.', $class->name, $parent->name));
}
}
$propertyScopes = self::exportPropertyScopes($class->name);
$hooks = '';
$propertyScopes = Hydrator::$propertyScopes[$class->name] ??= Hydrator::getPropertyScopes($class->name);
foreach ($propertyScopes as $key => [$scope, $name, , $access]) {
$propertyScopes[$k = "\0$scope\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = $name;
$flags = $access >> 2;
if ($k !== $key || !($access & Hydrator::PROPERTY_HAS_HOOKS) || $flags & \ReflectionProperty::IS_VIRTUAL) {
continue;
}
if ($flags & (\ReflectionProperty::IS_FINAL | \ReflectionProperty::IS_PRIVATE)) {
throw new LogicException(sprintf('Cannot generate lazy ghost: property "%s::$%s" is final or private(set).', $class->name, $name));
}
$p = $propertyScopes[$k][4] ?? Hydrator::$propertyScopes[$class->name][$k][4] = new \ReflectionProperty($scope, $name);
$type = self::exportType($p);
$hooks .= "\n "
.($p->isProtected() ? 'protected' : 'public')
.($p->isProtectedSet() ? ' protected(set)' : '')
." {$type} \${$name}"
.($p->hasDefaultValue() ? ' = '.VarExporter::export($p->getDefaultValue()) : '')
." {\n";
foreach ($p->getHooks() as $hook => $method) {
if ('get' === $hook) {
$ref = ($method->returnsReference() ? '&' : '');
$hooks .= " {$ref}get { \$this->initializeLazyObject(); return parent::\${$name}::get(); }\n";
} elseif ('set' === $hook) {
$parameters = self::exportParameters($method, true);
$arg = '$'.$method->getParameters()[0]->name;
$hooks .= " set({$parameters}) { \$this->initializeLazyObject(); parent::\${$name}::set({$arg}); }\n";
} else {
throw new LogicException(sprintf('Cannot generate lazy ghost: hook "%s::%s()" is not supported.', $class->name, $method->name));
}
}
$hooks .= " }\n";
}
$propertyScopes = self::exportPropertyScopes($class->name, $propertyScopes);
return <<<EOPHP
extends \\{$class->name} implements \Symfony\Component\VarExporter\LazyObjectInterface
@@ -66,7 +107,7 @@ final class ProxyHelper
use \Symfony\Component\VarExporter\LazyGhostTrait;
private const LAZY_OBJECT_PROPERTY_SCOPES = {$propertyScopes};
}
{$hooks}}
// Help opcache.preload discover always-needed symbols
class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class);
@@ -92,7 +133,38 @@ final class ProxyHelper
throw new LogicException(sprintf('Cannot generate lazy proxy: class "%s" is final.', $class->name));
}
if (\PHP_VERSION_ID >= 80200 && \PHP_VERSION_ID < 80300 && $class?->isReadOnly()) {
throw new LogicException(sprintf('Cannot generate lazy proxy: class "%s" is readonly.', $class->name));
throw new LogicException(sprintf('Cannot generate lazy proxy with PHP < 8.3: class "%s" is readonly.', $class->name));
}
$propertyScopes = $class ? Hydrator::$propertyScopes[$class->name] ??= Hydrator::getPropertyScopes($class->name) : [];
$abstractProperties = [];
$hookedProperties = [];
if (\PHP_VERSION_ID >= 80400 && $class) {
foreach ($propertyScopes as $key => [$scope, $name, , $access]) {
$propertyScopes[$k = "\0$scope\0$name"] ?? $propertyScopes[$k = "\0*\0$name"] ?? $k = $name;
$flags = $access >> 2;
if ($k !== $key) {
continue;
}
if ($flags & \ReflectionProperty::IS_ABSTRACT) {
$abstractProperties[$name] = $propertyScopes[$k][4] ?? Hydrator::$propertyScopes[$class->name][$k][4] = new \ReflectionProperty($scope, $name);
continue;
}
$abstractProperties[$name] = false;
if (!($access & Hydrator::PROPERTY_HAS_HOOKS) || $flags & \ReflectionProperty::IS_VIRTUAL) {
continue;
}
if ($flags & (\ReflectionProperty::IS_FINAL | \ReflectionProperty::IS_PRIVATE)) {
throw new LogicException(sprintf('Cannot generate lazy proxy: property "%s::$%s" is final or private(set).', $class->name, $name));
}
$p = $propertyScopes[$k][4] ?? Hydrator::$propertyScopes[$class->name][$k][4] = new \ReflectionProperty($scope, $name);
$hookedProperties[$name] = [$p, $p->getHooks()];
}
}
$methodReflectors = [$class?->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) ?? []];
@@ -101,8 +173,67 @@ final class ProxyHelper
throw new LogicException(sprintf('Cannot generate lazy proxy: "%s" is not an interface.', $interface->name));
}
$methodReflectors[] = $interface->getMethods();
if (\PHP_VERSION_ID >= 80400) {
foreach ($interface->getProperties() as $p) {
$abstractProperties[$p->name] ??= $p;
$hookedProperties[$p->name] ??= [$p, []];
$hookedProperties[$p->name][1] += $p->getHooks();
}
}
}
$hooks = '';
foreach (array_filter($abstractProperties) as $name => $p) {
$type = self::exportType($p);
$hooks .= "\n "
.($p->isProtected() ? 'protected' : 'public')
.($p->isProtectedSet() ? ' protected(set)' : '')
." {$type} \${$name};\n";
}
foreach ($hookedProperties as $name => [$p, $methods]) {
$type = self::exportType($p);
$hooks .= "\n "
.($p->isProtected() ? 'protected' : 'public')
.($p->isProtectedSet() ? ' protected(set)' : '')
." {$type} \${$name} {\n";
foreach ($methods as $hook => $method) {
if ('get' === $hook) {
$ref = ($method->returnsReference() ? '&' : '');
$hooks .= <<<EOPHP
{$ref}get {
if (isset(\$this->lazyObjectState)) {
return (\$this->lazyObjectState->realInstance ??= (\$this->lazyObjectState->initializer)())->{$p->name};
}
return parent::\${$p->name}::get();
}
EOPHP;
} elseif ('set' === $hook) {
$parameters = self::exportParameters($method, true);
$arg = '$'.$method->getParameters()[0]->name;
$hooks .= <<<EOPHP
set({$parameters}) {
if (isset(\$this->lazyObjectState)) {
\$this->lazyObjectState->realInstance ??= (\$this->lazyObjectState->initializer)();
\$this->lazyObjectState->realInstance->{$p->name} = {$arg};
}
parent::\${$p->name}::set({$arg});
}
EOPHP;
} else {
throw new LogicException(sprintf('Cannot generate lazy proxy: hook "%s::%s()" is not supported.', $class->name, $method->name));
}
}
$hooks .= " }\n";
}
$methodReflectors = array_merge(...$methodReflectors);
$extendsInternalClass = false;
if ($parent = $class) {
@@ -112,6 +243,7 @@ final class ProxyHelper
}
$methodsHaveToBeProxied = $extendsInternalClass;
$methods = [];
$methodReflectors = array_merge(...$methodReflectors);
foreach ($methodReflectors as $method) {
if ('__get' !== strtolower($method->name) || 'mixed' === ($type = self::exportType($method) ?? 'mixed')) {
@@ -195,15 +327,40 @@ final class ProxyHelper
$methods = ['initializeLazyObject' => implode('', $body).' }'] + $methods;
}
$body = $methods ? "\n".implode("\n\n", $methods)."\n" : '';
$propertyScopes = $class ? self::exportPropertyScopes($class->name) : '[]';
$propertyScopes = $class ? self::exportPropertyScopes($class->name, $propertyScopes) : '[]';
if (
$class?->hasMethod('__unserialize')
&& !$class->getMethod('__unserialize')->getParameters()[0]->getType()
) {
// fix contravariance type problem when $class declares a `__unserialize()` method without typehint.
$lazyProxyTraitStatement = <<<EOPHP
use \Symfony\Component\VarExporter\LazyProxyTrait {
__unserialize as private __doUnserialize;
}
EOPHP;
$body .= <<<EOPHP
public function __unserialize(\$data): void
{
\$this->__doUnserialize(\$data);
}
EOPHP;
} else {
$lazyProxyTraitStatement = <<<EOPHP
use \Symfony\Component\VarExporter\LazyProxyTrait;
EOPHP;
}
return <<<EOPHP
{$parent} implements \\{$interfaces}
{
use \Symfony\Component\VarExporter\LazyProxyTrait;
{$lazyProxyTraitStatement}
private const LAZY_OBJECT_PROPERTY_SCOPES = {$propertyScopes};
{$body}}
{$hooks}{$body}}
// Help opcache.preload discover always-needed symbols
class_exists(\Symfony\Component\VarExporter\Internal\Hydrator::class);
@@ -213,18 +370,20 @@ final class ProxyHelper
EOPHP;
}
public static function exportSignature(\ReflectionFunctionAbstract $function, bool $withParameterTypes = true, string &$args = null): string
public static function exportParameters(\ReflectionFunctionAbstract $function, bool $withParameterTypes = true, ?string &$args = null): string
{
$byRefIndex = 0;
$args = '';
$param = null;
$parameters = [];
$namespace = $function instanceof \ReflectionMethod ? $function->class : $function->getNamespaceName().'\\';
$namespace = substr($namespace, 0, strrpos($namespace, '\\') ?: 0);
foreach ($function->getParameters() as $param) {
$parameters[] = ($param->getAttributes(\SensitiveParameter::class) ? '#[\SensitiveParameter] ' : '')
.($withParameterTypes && $param->hasType() ? self::exportType($param).' ' : '')
.($param->isPassedByReference() ? '&' : '')
.($param->isVariadic() ? '...' : '').'$'.$param->name
.($param->isOptional() && !$param->isVariadic() ? ' = '.self::exportDefault($param) : '');
.($param->isOptional() && !$param->isVariadic() ? ' = '.self::exportDefault($param, $namespace) : '');
if ($param->isPassedByReference()) {
$byRefIndex = 1 + $param->getPosition();
}
@@ -241,8 +400,15 @@ final class ProxyHelper
$args = implode(', ', $args);
}
return implode(', ', $parameters);
}
public static function exportSignature(\ReflectionFunctionAbstract $function, bool $withParameterTypes = true, ?string &$args = null): string
{
$parameters = self::exportParameters($function, $withParameterTypes, $args);
$signature = 'function '.($function->returnsReference() ? '&' : '')
.($function->isClosure() ? '' : $function->name).'('.implode(', ', $parameters).')';
.($function->isClosure() ? '' : $function->name).'('.$parameters.')';
if ($function instanceof \ReflectionMethod) {
$signature = ($function->isPublic() ? 'public ' : ($function->isProtected() ? 'protected ' : 'private '))
@@ -270,7 +436,7 @@ final class ProxyHelper
return $signature;
}
public static function exportType(\ReflectionFunctionAbstract|\ReflectionProperty|\ReflectionParameter $owner, bool $noBuiltin = false, \ReflectionType $type = null): ?string
public static function exportType(\ReflectionFunctionAbstract|\ReflectionProperty|\ReflectionParameter $owner, bool $noBuiltin = false, ?\ReflectionType $type = null): ?string
{
if (!$type ??= $owner instanceof \ReflectionFunctionAbstract ? $owner->getReturnType() : $owner->getType()) {
return null;
@@ -311,19 +477,18 @@ final class ProxyHelper
return '';
}
if (null === $glue) {
return (!$noBuiltin && $type->allowsNull() && 'mixed' !== $name ? '?' : '').$types[0];
return (!$noBuiltin && $type->allowsNull() && !\in_array($name, ['mixed', 'null'], true) ? '?' : '').$types[0];
}
sort($types);
return implode($glue, $types);
}
private static function exportPropertyScopes(string $parent): string
private static function exportPropertyScopes(string $parent, array $propertyScopes): string
{
$propertyScopes = Hydrator::$propertyScopes[$parent] ??= Hydrator::getPropertyScopes($parent);
uksort($propertyScopes, 'strnatcmp');
foreach ($propertyScopes as $k => $v) {
unset($propertyScopes[$k][3]);
unset($propertyScopes[$k][4]);
}
$propertyScopes = VarExporter::export($propertyScopes);
$propertyScopes = str_replace(VarExporter::export($parent), 'parent::class', $propertyScopes);
@@ -333,7 +498,7 @@ final class ProxyHelper
return $propertyScopes;
}
private static function exportDefault(\ReflectionParameter $param): string
private static function exportDefault(\ReflectionParameter $param, $namespace): string
{
$default = rtrim(substr(explode('$'.$param->name.' = ', (string) $param, 2)[1] ?? '', 0, -2));
@@ -347,7 +512,7 @@ final class ProxyHelper
$regexp = "/(\"(?:[^\"\\\\]*+(?:\\\\.)*+)*+\"|'(?:[^'\\\\]*+(?:\\\\.)*+)*+')/";
$parts = preg_split($regexp, $default, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY);
$regexp = '/([\[\( ]|^)([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z0-9_\x7f-\xff]++)*+)(?!: )/';
$regexp = '/([\[\( ]|^)([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+(?:\\\\[a-zA-Z0-9_\x7f-\xff]++)*+)(\(?)(?!: )/';
$callback = (false !== strpbrk($default, "\\:('") && $class = $param->getDeclaringClass())
? fn ($m) => $m[1].match ($m[2]) {
'new', 'false', 'true', 'null' => $m[2],
@@ -355,13 +520,13 @@ final class ProxyHelper
'self' => '\\'.$class->name,
'namespace\\parent',
'parent' => ($parent = $class->getParentClass()) ? '\\'.$parent->name : 'parent',
default => '\\'.$m[2],
}
default => self::exportSymbol($m[2], '(' !== $m[3], $namespace),
}.$m[3]
: fn ($m) => $m[1].match ($m[2]) {
'new', 'false', 'true', 'null', 'self', 'parent' => $m[2],
'NULL' => 'null',
default => '\\'.$m[2],
};
default => self::exportSymbol($m[2], '(' !== $m[3], $namespace),
}.$m[3];
return implode('', array_map(fn ($part) => match ($part[0]) {
'"' => $part, // for internal classes only
@@ -369,4 +534,18 @@ final class ProxyHelper
default => preg_replace_callback($regexp, $callback, $part),
}, $parts));
}
private static function exportSymbol(string $symbol, bool $mightBeRootConst, string $namespace): string
{
if (!$mightBeRootConst
|| false === ($ns = strrpos($symbol, '\\'))
|| substr($symbol, 0, $ns) !== $namespace
|| \defined($symbol)
|| !\defined(substr($symbol, $ns + 1))
) {
return '\\'.$symbol;
}
return '\\'.substr($symbol, $ns + 1);
}
}