Skip to content

Commit 1a5b5e5

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 ------- a4f51c9 [HttpFoundation] Add `Request::$allowedHttpMethodOverride` to list which HTTP methods can be overridden
2 parents 34dae00 + a4f51c9 commit 1a5b5e5

File tree

10 files changed

+129
-8
lines changed

10 files changed

+129
-8
lines changed

src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ CHANGELOG
1515
* Add support for configuring the `CachingHttpClient`
1616
* Add support for weighted transitions in workflows
1717
* Add support for union types with `Symfony\Component\EventDispatcher\Attribute\AsEventListener`
18+
* Add `framework.allowed_http_method_override` option
1819

1920
7.3
2021
---

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,14 @@ public function getConfigTreeBuilder(): TreeBuilder
9191
->children()
9292
->scalarNode('secret')->end()
9393
->booleanNode('http_method_override')
94-
->info("Set true to enable support for the '_method' request parameter to determine the intended HTTP method on POST requests. Note: When using the HttpCache, you need to call the method in your front controller instead.")
94+
->info("Set true to enable support for the '_method' request parameter to determine the intended HTTP method on POST requests.")
9595
->defaultFalse()
9696
->end()
97+
->arrayNode('allowed_http_method_override')
98+
->info('Sets the list of HTTP methods that can be overridden. Set to null to allow all methods to be overridden (default). Set to an empty array to disallow overrides entirely. Otherwise, provide the list of uppercased method names that are allowed.')
99+
->stringPrototype()->end()
100+
->defaultNull()
101+
->end()
97102
->scalarNode('trust_x_sendfile_type_header')
98103
->info('Set true to enable support for xsendfile in binary file responses.')
99104
->defaultValue('%env(bool:default::SYMFONY_TRUST_X_SENDFILE_TYPE_HEADER)%')

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,7 @@ public function load(array $configs, ContainerBuilder $container): void
383383
$container->parameterCannotBeEmpty('kernel.secret', 'A non-empty value for the parameter "kernel.secret" is required. Did you forget to configure the '.$emptySecretHint.'?');
384384

385385
$container->setParameter('kernel.http_method_override', $config['http_method_override']);
386+
$container->setParameter('kernel.allowed_http_method_override', $config['allowed_http_method_override']);
386387
$container->setParameter('kernel.trust_x_sendfile_type_header', $config['trust_x_sendfile_type_header']);
387388
$container->setParameter('kernel.trusted_hosts', [0] === array_keys($config['trusted_hosts']) ? $config['trusted_hosts'][0] : $config['trusted_hosts']);
388389
$container->setParameter('kernel.default_locale', $config['default_locale']);
@@ -442,7 +443,7 @@ public function load(array $configs, ContainerBuilder $container): void
442443
}
443444

444445
$propertyInfoEnabled = $this->readConfigEnabled('property_info', $container, $config['property_info']);
445-
$this->registerHttpCacheConfiguration($config['http_cache'], $container, $config['http_method_override']);
446+
$this->registerHttpCacheConfiguration($config['http_cache'], $container, $config['http_method_override'], $config['allowed_http_method_override']);
446447
$this->registerEsiConfiguration($config['esi'], $container, $loader);
447448
$this->registerSsiConfiguration($config['ssi'], $container, $loader);
448449
$this->registerFragmentsConfiguration($config['fragments'], $container, $loader);
@@ -945,7 +946,7 @@ private function registerFormConfiguration(array $config, ContainerBuilder $cont
945946
}
946947
}
947948

948-
private function registerHttpCacheConfiguration(array $config, ContainerBuilder $container, bool $httpMethodOverride): void
949+
private function registerHttpCacheConfiguration(array $config, ContainerBuilder $container, bool $httpMethodOverride, ?array $allowedHttpMethodOverride): void
949950
{
950951
$options = $config;
951952
unset($options['enabled']);
@@ -968,6 +969,14 @@ private function registerHttpCacheConfiguration(array $config, ContainerBuilder
968969
->setFactory([Request::class, 'enableHttpMethodParameterOverride'])
969970
);
970971
}
972+
973+
if (null !== $allowedHttpMethodOverride) {
974+
$container->getDefinition('http_cache')
975+
->addArgument((new Definition('void'))
976+
->setFactory([Request::class, 'setAllowedHttpMethodOverride'])
977+
->addArgument($allowedHttpMethodOverride)
978+
);
979+
}
971980
}
972981

973982
private function registerEsiConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void

src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ public function boot(): void
119119
Request::enableHttpMethodParameterOverride();
120120
}
121121

122+
if ($this->container->hasParameter('kernel.allowed_http_method_override')) {
123+
Request::setAllowedHttpMethodOverride($this->container->getParameter('kernel.allowed_http_method_override'));
124+
}
125+
122126
if ($this->container->hasParameter('kernel.trust_x_sendfile_type_header') && $this->container->getParameter('kernel.trust_x_sendfile_type_header')) {
123127
BinaryFileResponse::trustXSendfileTypeHeader();
124128
}

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,7 @@ protected static function getBundleDefaultConfig()
715715
{
716716
return [
717717
'http_method_override' => false,
718+
'allowed_http_method_override' => null,
718719
'handle_all_throwables' => true,
719720
'trust_x_sendfile_type_header' => '%env(bool:default::SYMFONY_TRUST_X_SENDFILE_TYPE_HEADER)%',
720721
'ide' => '%env(default::SYMFONY_IDE)%',

src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTestCase.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,30 @@ public function testHttpMethodOverride()
214214
$this->assertFalse($container->getParameter('kernel.http_method_override'));
215215
}
216216

217+
public function testAllowedHttpMethodOverride()
218+
{
219+
$container = $this->createContainerFromFile('full');
220+
221+
$this->assertNull($container->getParameter('kernel.allowed_http_method_override'));
222+
}
223+
224+
public function testAllowedHttpMethodOverrideWithSpecificMethods()
225+
{
226+
$container = $this->createContainerFromClosure(function ($container) {
227+
$container->loadFromExtension('framework', [
228+
'annotations' => false,
229+
'http_method_override' => true,
230+
'allowed_http_method_override' => ['PUT', 'DELETE'],
231+
'handle_all_throwables' => true,
232+
'php_errors' => ['log' => true],
233+
'secret' => 's3cr3t',
234+
]);
235+
});
236+
237+
$this->assertTrue($container->getParameter('kernel.http_method_override'));
238+
$this->assertEquals(['PUT', 'DELETE'], $container->getParameter('kernel.allowed_http_method_override'));
239+
}
240+
217241
public function testTrustXSendfileTypeHeader()
218242
{
219243
$container = $this->createContainerFromFile('full');

src/Symfony/Bundle/FrameworkBundle/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
"symfony/deprecation-contracts": "^2.5|^3",
2626
"symfony/error-handler": "^7.3|^8.0",
2727
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
28-
"symfony/http-foundation": "^7.3|^8.0",
28+
"symfony/http-foundation": "^7.4|^8.0",
2929
"symfony/http-kernel": "^7.2|^8.0",
3030
"symfony/polyfill-mbstring": "~1.0",
3131
"symfony/polyfill-php85": "^1.32",

src/Symfony/Component/HttpFoundation/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

src/Symfony/Component/HttpFoundation/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

src/Symfony/Component/HttpFoundation/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)