From 9d58111e016468c58b4959cb5041627016caf48a Mon Sep 17 00:00:00 2001 From: Sascha Greuel Date: Thu, 3 Sep 2026 03:07:26 +0200 Subject: [PATCH] Validate list view constructor parameters Map query parameters against list view constructor signatures before instantiating the selected view. This consistently validates parameters across the filter action and both list view endpoints, preventing malformed tag IDs and other constructor arguments from reaching downstream code. --- .../lib/action/ListViewFilterAction.class.php | 7 ++- .../install/files/lib/http/Helper.class.php | 53 +++++++++++++++++-- .../core/listViews/GetItem.class.php | 8 ++- .../core/listViews/GetItems.class.php | 8 ++- .../user/TaggedArticleListView.class.php | 2 +- 5 files changed, 66 insertions(+), 12 deletions(-) diff --git a/wcfsetup/install/files/lib/action/ListViewFilterAction.class.php b/wcfsetup/install/files/lib/action/ListViewFilterAction.class.php index 75d9069303b..409ef7ffec2 100644 --- a/wcfsetup/install/files/lib/action/ListViewFilterAction.class.php +++ b/wcfsetup/install/files/lib/action/ListViewFilterAction.class.php @@ -47,7 +47,10 @@ public function handle(ServerRequestInterface $request): ResponseInterface try { /** @var AbstractListView> $view */ - $view = new $parameters['listView'](...$parameters['listViewParameters']); + $view = Helper::mapQueryParametersToClass( + $parameters['listViewParameters'], + $parameters['listView'] + ); // @phpstan-ignore catch.neverThrown } catch (\ArgumentCountError | \TypeError $e) { if (\ENABLE_DEBUG_MODE) { @@ -93,7 +96,7 @@ public function handle(ServerRequestInterface $request): ResponseInterface } return new JsonResponse([ - 'result' => $data + 'result' => $data, ]); } else { throw new \LogicException('Unreachable'); diff --git a/wcfsetup/install/files/lib/http/Helper.class.php b/wcfsetup/install/files/lib/http/Helper.class.php index 732c4bead37..af4551548a0 100644 --- a/wcfsetup/install/files/lib/http/Helper.class.php +++ b/wcfsetup/install/files/lib/http/Helper.class.php @@ -113,11 +113,7 @@ public static function getPreferredContentType(RequestInterface $request, array */ public static function mapQueryParameters(array $queryParameters, string $schema): mixed { - $mapper = (new MapperBuilder()) - ->allowSuperfluousKeys() - ->allowScalarValueCasting() - ->allowUndefinedValues() - ->mapper(); + $mapper = self::getQueryParameterMapperBuilder()->mapper(); return $mapper->map( $schema, @@ -125,6 +121,53 @@ public static function mapQueryParameters(array $queryParameters, string $schema ); } + /** + * Validates query parameters against the constructor signature of the + * provided class and invokes the constructor with the mapped arguments. + * No other properties of the class are mapped. + * + * @template T of object + * @param mixed[] $queryParameters + * @param class-string $className + * @return T + * @throws MappingError + */ + public static function mapQueryParametersToClass(array $queryParameters, string $className): object + { + $reflectionClass = new \ReflectionClass($className); + if (!$reflectionClass->isInstantiable()) { + throw new \InvalidArgumentException("Class '{$className}' is not instantiable."); + } + + $constructor = $reflectionClass->getConstructor(); + if ($constructor === null) { + return $reflectionClass->newInstance(); + } + + $object = $reflectionClass->newInstanceWithoutConstructor(); + $constructorClosure = $constructor->getClosure($object); + \assert($constructorClosure !== null); + + $arguments = self::getQueryParameterMapperBuilder() + ->argumentsMapper() + ->mapArguments( + $constructorClosure, + Source::array($queryParameters) + ); + + $constructorClosure(...$arguments); + + return $object; + } + + private static function getQueryParameterMapperBuilder(): MapperBuilder + { + return (new MapperBuilder()) + ->allowSuperfluousKeys() + ->allowScalarValueCasting() + ->allowUndefinedValues(); + } + /** * Validates body parameters against the provided schema. Expects * the data source to be JSON and thus values to be of the correct diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/listViews/GetItem.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/listViews/GetItem.class.php index 098af503864..ef3ade3d796 100644 --- a/wcfsetup/install/files/lib/system/endpoint/controller/core/listViews/GetItem.class.php +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/listViews/GetItem.class.php @@ -32,7 +32,10 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res throw new UserInputException('listView', 'invalid'); } - $view = new $parameters->listView(...$parameters->listViewParameters); + $view = Helper::mapQueryParametersToClass( + $parameters->listViewParameters, + $parameters->listView + ); // @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue \assert($view instanceof AbstractListView); @@ -68,5 +71,6 @@ public function __construct( public readonly array $listViewParameters, public readonly bool $allowInteractions = true, public readonly bool $allowBulkInteractions = true, - ) {} + ) { + } } diff --git a/wcfsetup/install/files/lib/system/endpoint/controller/core/listViews/GetItems.class.php b/wcfsetup/install/files/lib/system/endpoint/controller/core/listViews/GetItems.class.php index 05ab4ab8c2c..c0f5b9bd6b9 100644 --- a/wcfsetup/install/files/lib/system/endpoint/controller/core/listViews/GetItems.class.php +++ b/wcfsetup/install/files/lib/system/endpoint/controller/core/listViews/GetItems.class.php @@ -32,7 +32,10 @@ public function __invoke(ServerRequestInterface $request, array $variables): Res throw new UserInputException('listView', 'invalid'); } - $view = new $parameters->listView(...$parameters->listViewParameters); + $view = Helper::mapQueryParametersToClass( + $parameters->listViewParameters, + $parameters->listView + ); // @phpstan-ignore function.alreadyNarrowedType, instanceof.alwaysTrue \assert($view instanceof AbstractListView); @@ -98,5 +101,6 @@ public function __construct( public readonly bool $allowSorting = true, public readonly bool $allowInteractions = true, public readonly bool $allowBulkInteractions = true, - ) {} + ) { + } } diff --git a/wcfsetup/install/files/lib/system/listView/user/TaggedArticleListView.class.php b/wcfsetup/install/files/lib/system/listView/user/TaggedArticleListView.class.php index df506413d9f..603dc1a24b9 100644 --- a/wcfsetup/install/files/lib/system/listView/user/TaggedArticleListView.class.php +++ b/wcfsetup/install/files/lib/system/listView/user/TaggedArticleListView.class.php @@ -16,7 +16,7 @@ class TaggedArticleListView extends ArticleListView { public function __construct( - /** @var list */ + /** @var non-empty-list */ public readonly array $tagIDs, ) { parent::__construct();