Skip to content

Commit b49cc7d

Browse files
committed
feature #61979 [HttpFoundation] Add Request::set/getAllowedHttpMethodOverride() to list which HTTP methods can be overridden (nicolas-grekas)
This PR was merged into the 7.4 branch. Discussion ---------- [HttpFoundation] Add `Request::set/getAllowedHttpMethodOverride()` to list which HTTP methods can be overridden | Q | A | ------------- | --- | Branch? | 7.4 | Bug fix? | no | New feature? | yes | Deprecations? | no | Issues | - | License | MIT This feature addresses https://github.com/symfony/symfony/pull/61949/files#r2402280654 and hardens HttpFoundation by giving control over which HTTP methods can be overridden: ```php Request::setAllowedHttpMethodOverride(['PUT', 'PATCH', 'DELETE']); ``` Providing no method disables verb tunneling altogether: ```php Request::setAllowedHttpMethodOverride([]); ``` This setting can be set using standard Symfony configuration: ```yaml framework: allowed_http_method_override: ['PUT', 'DELETE', 'PATCH'] ``` 2 implementations note: - This doesn't update the XSD file on purpose: that format is deprecated and handling it would mean adding more complexity that nobody will benefit from in practice. - This isn't compatible with defining the list of allowed methods using env vars. This could be added later if one has a use case for that. Until it happens, I prefer keeping the code simpler. Commits ------- a4f51c9548c [HttpFoundation] Add `Request::$allowedHttpMethodOverride` to list which HTTP methods can be overridden
2 parents f9b8417 + e9b1d43 commit b49cc7d

File tree

3 files changed

+81
-4
lines changed

3 files changed

+81
-4
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ CHANGELOG
77
* Add `#[WithHttpStatus]` to define status codes: 404 for `SignedUriException` and 403 for `ExpiredSignedUriException`
88
* Add support for the `QUERY` HTTP method
99
* Add support for structured MIME suffix
10+
* Add `Request::set/getAllowedHttpMethodOverride()` to list which HTTP methods can be overridden
1011
* Deprecate using `Request::sendHeaders()` after headers have already been sent; use a `StreamedResponse` instead
1112
* Deprecate method `Request::get()`, use properties `->attributes`, `query` or `request` directly instead
1213
* Make `Request::createFromGlobals()` parse the body of PUT, DELETE, PATCH and QUERY requests

Request.php

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,13 @@ class Request
8181

8282
protected static bool $httpMethodParameterOverride = false;
8383

84+
/**
85+
* The HTTP methods that can be overridden.
86+
*
87+
* @var uppercase-string[]|null
88+
*/
89+
protected static ?array $allowedHttpMethodOverride = null;
90+
8491
/**
8592
* Custom parameters.
8693
*/
@@ -680,6 +687,30 @@ public static function getHttpMethodParameterOverride(): bool
680687
return self::$httpMethodParameterOverride;
681688
}
682689

690+
/**
691+
* Sets the list of HTTP methods that can be overridden.
692+
*
693+
* Set to null to allow all methods to be overridden (default). Set to an
694+
* empty array to disallow overrides entirely. Otherwise, provide the list
695+
* of uppercased method names that are allowed.
696+
*
697+
* @param uppercase-string[]|null $methods
698+
*/
699+
public static function setAllowedHttpMethodOverride(?array $methods): void
700+
{
701+
self::$allowedHttpMethodOverride = $methods;
702+
}
703+
704+
/**
705+
* Gets the list of HTTP methods that can be overridden.
706+
*
707+
* @return uppercase-string[]|null
708+
*/
709+
public static function getAllowedHttpMethodOverride(): ?array
710+
{
711+
return self::$allowedHttpMethodOverride;
712+
}
713+
683714
/**
684715
* Gets a "parameter" value from any bag.
685716
*
@@ -1197,7 +1228,7 @@ public function getMethod(): string
11971228

11981229
$this->method = strtoupper($this->server->get('REQUEST_METHOD', 'GET'));
11991230

1200-
if ('POST' !== $this->method) {
1231+
if ('POST' !== $this->method || !(self::$allowedHttpMethodOverride ?? true)) {
12011232
return $this->method;
12021233
}
12031234

@@ -1213,11 +1244,11 @@ public function getMethod(): string
12131244

12141245
$method = strtoupper($method);
12151246

1216-
if (\in_array($method, ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'PATCH', 'PURGE', 'TRACE', 'QUERY'], true)) {
1217-
return $this->method = $method;
1247+
if (self::$allowedHttpMethodOverride && !\in_array($method, self::$allowedHttpMethodOverride, true)) {
1248+
return $this->method;
12181249
}
12191250

1220-
if (!preg_match('/^[A-Z]++$/D', $method)) {
1251+
if (\strlen($method) !== strspn($method, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')) {
12211252
throw new SuspiciousOperationException('Invalid HTTP method override.');
12221253
}
12231254

Tests/RequestTest.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ protected function tearDown(): void
3333
{
3434
Request::setTrustedProxies([], -1);
3535
Request::setTrustedHosts([]);
36+
Request::setAllowedHttpMethodOverride(null);
3637
}
3738

3839
public function testInitialize()
@@ -255,6 +256,50 @@ public function testCreate()
255256
$this->assertEquals('http://test.com/foo', $request->getUri());
256257
}
257258

259+
public function testHttpMethodOverrideRespectsAllowedListWithHeader()
260+
{
261+
$request = Request::create('http://example.com/', 'POST');
262+
$request->headers->set('X-HTTP-METHOD-OVERRIDE', 'PATCH');
263+
264+
Request::setAllowedHttpMethodOverride(['PUT', 'PATCH']);
265+
266+
$this->assertSame('PATCH', $request->getMethod());
267+
}
268+
269+
public function testHttpMethodOverrideDisallowedSkipsOverrideWithHeader()
270+
{
271+
$request = Request::create('http://example.com/', 'POST');
272+
$request->headers->set('X-HTTP-METHOD-OVERRIDE', 'DELETE');
273+
274+
Request::setAllowedHttpMethodOverride(['PUT', 'PATCH']);
275+
276+
$this->assertSame('POST', $request->getMethod());
277+
}
278+
279+
public function testHttpMethodOverrideDisabledWithEmptyAllowedList()
280+
{
281+
$request = Request::create('http://example.com/', 'POST');
282+
$request->headers->set('X-HTTP-METHOD-OVERRIDE', 'PUT');
283+
284+
Request::setAllowedHttpMethodOverride([]);
285+
286+
$this->assertSame('POST', $request->getMethod());
287+
}
288+
289+
public function testHttpMethodOverrideRespectsAllowedListWithParameter()
290+
{
291+
Request::enableHttpMethodParameterOverride();
292+
Request::setAllowedHttpMethodOverride(['PUT']);
293+
294+
try {
295+
$request = Request::create('http://example.com/', 'POST', ['_method' => 'PUT']);
296+
297+
$this->assertSame('PUT', $request->getMethod());
298+
} finally {
299+
(new \ReflectionProperty(Request::class, 'httpMethodParameterOverride'))->setValue(null, false);
300+
}
301+
}
302+
258303
public function testCreateWithRequestUri()
259304
{
260305
$request = Request::create('http://test.com:80/foo');

0 commit comments

Comments
 (0)