Skip to content

Commit eac4c7c

Browse files
[HttpFoundation] Add Request::$allowedHttpMethodOverride to list which HTTP methods can be overridden
1 parent 9b72d76 commit eac4c7c

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

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 string[]|null
88+
*/
89+
protected static ?array $allowedHttpMethodOverride = null;
90+
8491
/**
8592
* Custom parameters.
8693
*/
@@ -670,6 +677,30 @@ public static function getHttpMethodParameterOverride(): bool
670677
return self::$httpMethodParameterOverride;
671678
}
672679

680+
/**
681+
* Sets the list of HTTP methods that can be overridden.
682+
*
683+
* Set to null to allow all methods to be overridden (default). Set to an
684+
* empty array to disallow overrides entirely. Otherwise, provide the list
685+
* of uppercased method names that are allowed.
686+
*
687+
* @param uppercase-string[]|null $methods
688+
*/
689+
public static function setAllowedHttpMethodOverride(?array $methods): void
690+
{
691+
self::$allowedHttpMethodOverride = $methods;
692+
}
693+
694+
/**
695+
* Gets the list of HTTP methods that can be overridden.
696+
*
697+
* @return uppercase-string[]|null
698+
*/
699+
public static function getAllowedHttpMethodOverride(): ?array
700+
{
701+
return self::$allowedHttpMethodOverride;
702+
}
703+
673704
/**
674705
* Gets a "parameter" value from any bag.
675706
*
@@ -1187,7 +1218,7 @@ public function getMethod(): string
11871218

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

1190-
if ('POST' !== $this->method) {
1221+
if ('POST' !== $this->method || !(self::$allowedHttpMethodOverride ?? true)) {
11911222
return $this->method;
11921223
}
11931224

@@ -1203,11 +1234,11 @@ public function getMethod(): string
12031234

12041235
$method = strtoupper($method);
12051236

1206-
if (\in_array($method, ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'CONNECT', 'OPTIONS', 'PATCH', 'PURGE', 'TRACE', 'QUERY'], true)) {
1207-
return $this->method = $method;
1237+
if (self::$allowedHttpMethodOverride && !\in_array($method, self::$allowedHttpMethodOverride, true)) {
1238+
return $this->method;
12081239
}
12091240

1210-
if (!preg_match('/^[A-Z]++$/D', $method)) {
1241+
if (\strlen($method) !== strspn($method, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')) {
12111242
throw new SuspiciousOperationException('Invalid HTTP method override.');
12121243
}
12131244

src/Symfony/Component/HttpFoundation/Tests/RequestTest.php

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

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

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

0 commit comments

Comments
 (0)