Skip to content

Commit 96bd1b0

Browse files
authored
fix(http): prevent mapping request data to reserved properties on request objects (#1374)
1 parent 7956c0b commit 96bd1b0

File tree

6 files changed

+71
-6
lines changed

6 files changed

+71
-6
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,10 @@
4545
"carthage-software/mago": "0.26.1",
4646
"guzzlehttp/psr7": "^2.6.1",
4747
"league/flysystem-aws-s3-v3": "^3.0",
48-
"league/flysystem-local": "^3.25.1",
4948
"league/flysystem-azure-blob-storage": "^3.0",
5049
"league/flysystem-ftp": "^3.0",
5150
"league/flysystem-google-cloud-storage": "^3.0",
51+
"league/flysystem-local": "^3.25.1",
5252
"league/flysystem-memory": "^3.0",
5353
"league/flysystem-read-only": "^3.0",
5454
"league/flysystem-sftp-v3": "^3.0",

packages/http/src/Mappers/RequestToObjectMapper.php

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@
44

55
namespace Tempest\Http\Mappers;
66

7+
use ReflectionClass;
78
use Tempest\Http\Request;
9+
use Tempest\Http\RequestParametersIncludedReservedNames;
810
use Tempest\Mapper\Mapper;
911
use Tempest\Mapper\Mappers\ArrayToObjectMapper;
12+
use Tempest\Reflection\ClassReflector;
13+
use Tempest\Reflection\PropertyReflector;
1014
use Tempest\Validation\Exceptions\ValidationFailed;
1115
use Tempest\Validation\Validator;
1216

1317
use function Tempest\map;
18+
use function Tempest\Support\arr;
1419

1520
final readonly class RequestToObjectMapper implements Mapper
1621
{
@@ -22,9 +27,17 @@ public function canMap(mixed $from, mixed $to): bool
2227
public function map(mixed $from, mixed $to): array|object
2328
{
2429
/** @var Request $from */
25-
$data = $from->body;
30+
$data = [...$from->files, ...$from->body, ...$from->query];
2631

2732
if (is_a($to, Request::class, true)) {
33+
$invalidReservedProperties = arr(new ClassReflector(Request::class)->getProperties())
34+
->map(fn (PropertyReflector $property) => $property->getName())
35+
->filter(fn (string $property) => array_key_exists($property, $data));
36+
37+
if ($invalidReservedProperties->isNotEmpty()) {
38+
throw new RequestParametersIncludedReservedNames($to, $invalidReservedProperties);
39+
}
40+
2841
$data = [
2942
...[
3043
'method' => $from->method,
@@ -36,8 +49,6 @@ public function map(mixed $from, mixed $to): array|object
3649
'files' => $from->files,
3750
'cookies' => $from->cookies,
3851
],
39-
...$from->files,
40-
...$from->query,
4152
...$data,
4253
];
4354
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace Tempest\Http;
4+
5+
use Exception;
6+
use Tempest\Support\Arr\ImmutableArray;
7+
8+
final class RequestParametersIncludedReservedNames extends Exception
9+
{
10+
public function __construct(string $requestClass, ImmutableArray $reservedProperties)
11+
{
12+
$message = sprintf(
13+
'The request payload included data with reserved property names: %s. It could not be mapped to `%s`',
14+
$reservedProperties->join('`, `')->wrap('`'),
15+
$requestClass,
16+
);
17+
18+
parent::__construct($message);
19+
}
20+
}

tests/Integration/Database/Builder/InsertQueryBuilderTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ public function test_insert_on_model_table_with_existing_relation(): void
162162

163163
public function test_inserting_has_many_via_parent_model_throws_exception(): void
164164
{
165-
$this->assertException(HasManyRelationCouldNotBeInsterted::class, function () {
165+
$this->assertException(HasManyRelationCouldNotBeInsterted::class, function (): void {
166166
query(Book::class)
167167
->insert(
168168
title: 'Timeline Taxi',
@@ -174,7 +174,7 @@ public function test_inserting_has_many_via_parent_model_throws_exception(): voi
174174

175175
public function test_inserting_has_one_via_parent_model_throws_exception(): void
176176
{
177-
$this->assertException(HasOneRelationCouldNotBeInserted::class, function () {
177+
$this->assertException(HasOneRelationCouldNotBeInserted::class, function (): void {
178178
query(Book::class)
179179
->insert(
180180
title: 'Timeline Taxi',
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
namespace Tests\Tempest\Integration\Route;
4+
5+
use Tempest\Http\IsRequest;
6+
use Tempest\Http\Request;
7+
8+
final class RequestForInvalidMap implements Request
9+
{
10+
use IsRequest;
11+
}

tests/Integration/Route/RequestToObjectMapperTest.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use Tempest\Http\Mappers\PsrRequestToGenericRequestMapper;
88
use Tempest\Http\Mappers\RequestToObjectMapper;
99
use Tempest\Http\Method;
10+
use Tempest\Http\RequestParametersIncludedReservedNames;
1011
use Tempest\Http\Upload;
1112
use Tempest\Validation\Exceptions\ValidationFailed;
1213
use Tempest\Validation\Rules\NotNull;
@@ -109,4 +110,26 @@ public function test_validation_fails_for_enum(): void
109110
$this->assertArrayHasKey('enumParam', $validationFailed->failingRules);
110111
}
111112
}
113+
114+
public function test_reserved_properties_cannot_be_mapped(): void
115+
{
116+
$this->assertException(
117+
expectedExceptionClass: RequestParametersIncludedReservedNames::class,
118+
handler: function (): void {
119+
map(new GenericRequest(
120+
method: Method::GET,
121+
uri: '/books?uri=invalid',
122+
body: ['query' => 'invalid'],
123+
files: ['body' => 'invalid'],
124+
))->with(
125+
RequestToObjectMapper::class,
126+
)->to(RequestForInvalidMap::class);
127+
},
128+
assertException: function (RequestParametersIncludedReservedNames $exception): void {
129+
$this->assertStringContainsString('uri', $exception->getMessage());
130+
$this->assertStringContainsString('query', $exception->getMessage());
131+
$this->assertStringContainsString('body', $exception->getMessage());
132+
},
133+
);
134+
}
112135
}

0 commit comments

Comments
 (0)