From 4381c883c9b64c548cc9950d49203bb7659b5c11 Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sat, 3 Oct 2026 13:19:36 +0300 Subject: [PATCH 1/4] Document sort input validation and recommend allowlisted fields --- CHANGELOG.md | 1 + README.md | 15 +++++++++++++-- docs/guide/pt-BR/README.md | 16 ++++++++++++++-- src/Reader/Sort.php | 10 ++++++++++ 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e14577..d31a01b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 2.0.1 under development +- Enh: Document sort input validation and recommend `Sort::only()` for user input (@samdark) - Enh #246: Explicitly import functions and constants in "use" section (@mspirkov) ## 2.0.0 December 13, 2025 diff --git a/README.md b/README.md index f835dae..8ba389c 100644 --- a/README.md +++ b/README.md @@ -243,8 +243,19 @@ To get a `Sort` instance, you can use either `Sort::only()` or `Sort::any()`. `S for logical fields that have no configuration. `Sort::any()` uses user-specified logical field name and order directly for fields that have no configuration. -Either way, you pass a config array that specifies which logical fields should be order-able and, optionally, details on -how these should map to real fields order. +Use `Sort::only()` by default, especially when the order comes from user input. Define the allowed logical fields in +application code: + +```php +$sort = Sort::only(['id', 'name'])->withOrderString($userInput); +``` + +`Sort::any()` does not restrict sorting to the configured fields or validate unknown field names. Before passing +untrusted input to `withOrder()` or `withOrderString()` on a `Sort::any()` instance, validate the field names against +the fields your application allows. Passing unchecked field names to a database-backed reader can lead to SQL injection. + +In both modes, the config array defines logical fields and, optionally, how they map to real fields and their order. +Only `Sort::only()` restricts sorting to those configured logical fields. The current order to apply is specified via `withOrder()` where you supply an array with keys corresponding to logical field names and values correspond to order (`asc` or `desc`). Alternatively `withOrderString()` can be used. In this case, diff --git a/docs/guide/pt-BR/README.md b/docs/guide/pt-BR/README.md index 2367f8d..d0b7e86 100644 --- a/docs/guide/pt-BR/README.md +++ b/docs/guide/pt-BR/README.md @@ -207,8 +207,20 @@ Para obter uma instância `Sort`, você pode usar `Sort::only()` ou `Sort::any() para campos lógicos que não possuem configuração. `Sort::any()` usa o nome do campo lógico especificado pelo usuário e ordena diretamente para campos que não possuem configuração. -De qualquer forma, você passa um array de configuração que especifica quais campos lógicos devem ser ordenados e, opcionalmente, detalhes sobre -como eles devem ser mapeados para a ordem real dos campos. +Use `Sort::only()` por padrão, especialmente quando a ordenação vier de dados fornecidos pelo usuário. Defina os campos +lógicos permitidos no código da aplicação: + +```php +$sort = Sort::only(['id', 'name'])->withOrderString($userInput); +``` + +`Sort::any()` não restringe a ordenação aos campos configurados nem valida nomes de campos desconhecidos. Antes de passar +dados não confiáveis para `withOrder()` ou `withOrderString()` em uma instância de `Sort::any()`, valide os nomes dos campos +de acordo com os campos permitidos pela aplicação. Passar nomes de campos sem validação para um leitor que consulta um +banco de dados pode levar à injeção de SQL. + +Nos dois modos, o array de configuração define os campos lógicos e, opcionalmente, como eles são mapeados para os campos +reais e sua ordenação. Apenas `Sort::only()` restringe a ordenação aos campos lógicos configurados. A ordem atual a ser aplicada é especificada via `withOrder()` onde você fornece um array com chaves lógicas correspondentes aos nomes e valores dos campos correspondem à ordem (`asc` ou `desc`). Alternativamente `withOrderString()` pode ser usado. Nesse caso diff --git a/src/Reader/Sort.php b/src/Reader/Sort.php index 33a4e55..7a76a96 100644 --- a/src/Reader/Sort.php +++ b/src/Reader/Sort.php @@ -36,6 +36,10 @@ * - {@see Sort::only()} ignores user-specified order for logical fields that have no configuration. * - {@see Sort::any()} uses user-specified logical field name and order directly for fields that have no configuration. * + * Use {@see Sort::only()} by default, especially for user input. With {@see Sort::any()}, validate untrusted field names + * against the fields your application allows before setting the order. Unchecked field names can lead to SQL injection + * when used with a database-backed reader. + * * @psalm-type TOrder = array * @psalm-type TSortFieldItem = array * @psalm-type TConfigItem = array{asc: TSortFieldItem, desc: TSortFieldItem, default: "asc"|"desc"} @@ -131,6 +135,8 @@ private function __construct(private bool $ignoreExtraFields, array $config) /** * Create a sort instance that ignores the current order for extra logical fields that have no configuration. * + * Recommended by default, especially for user input. Define the allowed logical fields in application code. + * * @param array $config Logical fields config. * @psalm-param TUserConfig $config * @@ -173,6 +179,10 @@ public static function only(array $config): self /** * Create a sort instance that uses logical field itself and direction provided when there is no configuration. * + * The configuration is not an allowlist. Before passing untrusted input to {@see withOrder()} or + * {@see withOrderString()}, validate field names against the fields your application allows. Unchecked field names + * can lead to SQL injection when used with a database-backed reader. Prefer {@see only()} for user input. + * * @param array $config Logical fields config. * @psalm-param TUserConfig $config * From 08a0edb83ddb8a62d7678c52da2e82ba2343b0df Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Sat, 3 Oct 2026 13:20:06 +0300 Subject: [PATCH 2/4] Reference pull request in changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d31a01b..cb9b6a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2.0.1 under development -- Enh: Document sort input validation and recommend `Sort::only()` for user input (@samdark) +- Enh #257: Document sort input validation and recommend `Sort::only()` for user input (@samdark) - Enh #246: Explicitly import functions and constants in "use" section (@mspirkov) ## 2.0.0 December 13, 2025 From 56fd42d3cb3dbea95601d6d34ebca14bf47aa720 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 3 Oct 2026 10:20:26 +0000 Subject: [PATCH 3/4] Apply PHP CS Fixer and Rector changes (CI) --- src/Paginator/KeysetPaginator.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Paginator/KeysetPaginator.php b/src/Paginator/KeysetPaginator.php index b68e13e..6e6dd1b 100644 --- a/src/Paginator/KeysetPaginator.php +++ b/src/Paginator/KeysetPaginator.php @@ -350,9 +350,7 @@ private function readData(ReadableDataInterface $dataReader, Sort $sort): array [$field] = $this->getFieldAndSortingFromSort($sort); foreach ($dataReader->read() as $key => $item) { - if ($this->currentFirstValue === null) { - $this->currentFirstValue = (string) ArrayHelper::getValue($item, $field); - } + $this->currentFirstValue ??= (string) ArrayHelper::getValue($item, $field); if (count($data) === $this->pageSize) { $this->hasNextPage = true; From 1f0df312ab3cdf9a6001f066903c6ac44e626b3d Mon Sep 17 00:00:00 2001 From: Alexander Makarov Date: Wed, 28 Oct 2026 01:48:51 +0300 Subject: [PATCH 4/4] Update CHANGELOG.md Co-authored-by: Sergei Predvoditelev --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb9b6a7..0e14577 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,6 @@ ## 2.0.1 under development -- Enh #257: Document sort input validation and recommend `Sort::only()` for user input (@samdark) - Enh #246: Explicitly import functions and constants in "use" section (@mspirkov) ## 2.0.0 December 13, 2025