Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ CHANGELOG
* Add support for configuring the `CachingHttpClient`
* Add support for weighted transitions in workflows
* Add support for union types with `Symfony\Component\EventDispatcher\Attribute\AsEventListener`
* Add `framework.allowed_http_method_override` option

7.3
---
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,14 @@ public function getConfigTreeBuilder(): TreeBuilder
->children()
->scalarNode('secret')->end()
->booleanNode('http_method_override')
->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.")
->info("Set true to enable support for the '_method' request parameter to determine the intended HTTP method on POST requests.")
->defaultFalse()
->end()
->arrayNode('allowed_http_method_override')
->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.')
->stringPrototype()->end()
->defaultNull()
->end()
->scalarNode('trust_x_sendfile_type_header')
->info('Set true to enable support for xsendfile in binary file responses.')
->defaultValue('%env(bool:default::SYMFONY_TRUST_X_SENDFILE_TYPE_HEADER)%')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,7 @@ public function load(array $configs, ContainerBuilder $container): void
$container->parameterCannotBeEmpty('kernel.secret', 'A non-empty value for the parameter "kernel.secret" is required. Did you forget to configure the '.$emptySecretHint.'?');

$container->setParameter('kernel.http_method_override', $config['http_method_override']);
Copy link
Member Author

@nicolas-grekas nicolas-grekas Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that this naming is quite unfortunate as it's confusing: the setting is only about allowing the use of the _method parameter. Overriding using the header is always available.
A better name would have been enable_http_method_override_parameter, like the name of the corresponding static method. In case anyone wants to follow up in another PR (deprecating the option, the parameter, BC/FC layer, etc.).

$container->setParameter('kernel.allowed_http_method_override', $config['allowed_http_method_override']);
$container->setParameter('kernel.trust_x_sendfile_type_header', $config['trust_x_sendfile_type_header']);
$container->setParameter('kernel.trusted_hosts', [0] === array_keys($config['trusted_hosts']) ? $config['trusted_hosts'][0] : $config['trusted_hosts']);
$container->setParameter('kernel.default_locale', $config['default_locale']);
Expand Down Expand Up @@ -442,7 +443,7 @@ public function load(array $configs, ContainerBuilder $container): void
}

$propertyInfoEnabled = $this->readConfigEnabled('property_info', $container, $config['property_info']);
$this->registerHttpCacheConfiguration($config['http_cache'], $container, $config['http_method_override']);
$this->registerHttpCacheConfiguration($config['http_cache'], $container, $config['http_method_override'], $config['allowed_http_method_override']);
$this->registerEsiConfiguration($config['esi'], $container, $loader);
$this->registerSsiConfiguration($config['ssi'], $container, $loader);
$this->registerFragmentsConfiguration($config['fragments'], $container, $loader);
Expand Down Expand Up @@ -945,7 +946,7 @@ private function registerFormConfiguration(array $config, ContainerBuilder $cont
}
}

private function registerHttpCacheConfiguration(array $config, ContainerBuilder $container, bool $httpMethodOverride): void
private function registerHttpCacheConfiguration(array $config, ContainerBuilder $container, bool $httpMethodOverride, ?array $allowedHttpMethodOverride): void
{
$options = $config;
unset($options['enabled']);
Expand All @@ -968,6 +969,14 @@ private function registerHttpCacheConfiguration(array $config, ContainerBuilder
->setFactory([Request::class, 'enableHttpMethodParameterOverride'])
);
}

if (null !== $allowedHttpMethodOverride) {
$container->getDefinition('http_cache')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For what do we need this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Git blame tells me its for configuring this before httpcache is created, which can happen super early before bundles are initialized

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah indeed, this is needed for the wrong behaviour reported in #40618 (fixed in #40619).

->addArgument((new Definition('void'))
->setFactory([Request::class, 'setAllowedHttpMethodOverride'])
->addArgument($allowedHttpMethodOverride)
);
}
}

private function registerEsiConfiguration(array $config, ContainerBuilder $container, PhpFileLoader $loader): void
Expand Down
4 changes: 4 additions & 0 deletions src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ public function boot(): void
Request::enableHttpMethodParameterOverride();
}

if ($this->container->hasParameter('kernel.allowed_http_method_override')) {
Request::setAllowedHttpMethodOverride($this->container->getParameter('kernel.allowed_http_method_override'));
}

if ($this->container->hasParameter('kernel.trust_x_sendfile_type_header') && $this->container->getParameter('kernel.trust_x_sendfile_type_header')) {
BinaryFileResponse::trustXSendfileTypeHeader();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,7 @@ protected static function getBundleDefaultConfig()
{
return [
'http_method_override' => false,
'allowed_http_method_override' => null,
'handle_all_throwables' => true,
'trust_x_sendfile_type_header' => '%env(bool:default::SYMFONY_TRUST_X_SENDFILE_TYPE_HEADER)%',
'ide' => '%env(default::SYMFONY_IDE)%',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,30 @@ public function testHttpMethodOverride()
$this->assertFalse($container->getParameter('kernel.http_method_override'));
}

public function testAllowedHttpMethodOverride()
{
$container = $this->createContainerFromFile('full');

$this->assertNull($container->getParameter('kernel.allowed_http_method_override'));
}

public function testAllowedHttpMethodOverrideWithSpecificMethods()
{
$container = $this->createContainerFromClosure(function ($container) {
$container->loadFromExtension('framework', [
'annotations' => false,
'http_method_override' => true,
'allowed_http_method_override' => ['PUT', 'DELETE'],
'handle_all_throwables' => true,
'php_errors' => ['log' => true],
'secret' => 's3cr3t',
]);
});

$this->assertTrue($container->getParameter('kernel.http_method_override'));
$this->assertEquals(['PUT', 'DELETE'], $container->getParameter('kernel.allowed_http_method_override'));
}

public function testTrustXSendfileTypeHeader()
{
$container = $this->createContainerFromFile('full');
Expand Down
2 changes: 1 addition & 1 deletion src/Symfony/Bundle/FrameworkBundle/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"symfony/deprecation-contracts": "^2.5|^3",
"symfony/error-handler": "^7.3|^8.0",
"symfony/event-dispatcher": "^6.4|^7.0|^8.0",
"symfony/http-foundation": "^7.3|^8.0",
"symfony/http-foundation": "^7.4|^8.0",
"symfony/http-kernel": "^7.2|^8.0",
"symfony/polyfill-mbstring": "~1.0",
"symfony/polyfill-php85": "^1.32",
Expand Down
1 change: 1 addition & 0 deletions src/Symfony/Component/HttpFoundation/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ CHANGELOG
* Add `#[WithHttpStatus]` to define status codes: 404 for `SignedUriException` and 403 for `ExpiredSignedUriException`
* Add support for the `QUERY` HTTP method
* Add support for structured MIME suffix
* Add `Request::set/getAllowedHttpMethodOverride()` to list which HTTP methods can be overridden
* Deprecate using `Request::sendHeaders()` after headers have already been sent; use a `StreamedResponse` instead
* Deprecate method `Request::get()`, use properties `->attributes`, `query` or `request` directly instead

Expand Down
39 changes: 35 additions & 4 deletions src/Symfony/Component/HttpFoundation/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ class Request

protected static bool $httpMethodParameterOverride = false;

/**
* The HTTP methods that can be overridden.
*
* @var uppercase-string[]|null
*/
protected static ?array $allowedHttpMethodOverride = null;

/**
* Custom parameters.
*/
Expand Down Expand Up @@ -670,6 +677,30 @@ public static function getHttpMethodParameterOverride(): bool
return self::$httpMethodParameterOverride;
}

/**
* 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.
*
* @param uppercase-string[]|null $methods
*/
public static function setAllowedHttpMethodOverride(?array $methods): void
{
self::$allowedHttpMethodOverride = $methods;
}

/**
* Gets the list of HTTP methods that can be overridden.
*
* @return uppercase-string[]|null
*/
public static function getAllowedHttpMethodOverride(): ?array
{
return self::$allowedHttpMethodOverride;
}

/**
* Gets a "parameter" value from any bag.
*
Expand Down Expand Up @@ -1187,7 +1218,7 @@ public function getMethod(): string

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

if ('POST' !== $this->method) {
if ('POST' !== $this->method || !(self::$allowedHttpMethodOverride ?? true)) {
return $this->method;
}

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

$method = strtoupper($method);

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

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

Expand Down
45 changes: 45 additions & 0 deletions src/Symfony/Component/HttpFoundation/Tests/RequestTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ protected function tearDown(): void
{
Request::setTrustedProxies([], -1);
Request::setTrustedHosts([]);
Request::setAllowedHttpMethodOverride(null);
}

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

public function testHttpMethodOverrideRespectsAllowedListWithHeader()
{
$request = Request::create('http://example.com/', 'POST');
$request->headers->set('X-HTTP-METHOD-OVERRIDE', 'PATCH');

Request::setAllowedHttpMethodOverride(['PUT', 'PATCH']);

$this->assertSame('PATCH', $request->getMethod());
}

public function testHttpMethodOverrideDisallowedSkipsOverrideWithHeader()
{
$request = Request::create('http://example.com/', 'POST');
$request->headers->set('X-HTTP-METHOD-OVERRIDE', 'DELETE');

Request::setAllowedHttpMethodOverride(['PUT', 'PATCH']);

$this->assertSame('POST', $request->getMethod());
}

public function testHttpMethodOverrideDisabledWithEmptyAllowedList()
{
$request = Request::create('http://example.com/', 'POST');
$request->headers->set('X-HTTP-METHOD-OVERRIDE', 'PUT');

Request::setAllowedHttpMethodOverride([]);

$this->assertSame('POST', $request->getMethod());
}

public function testHttpMethodOverrideRespectsAllowedListWithParameter()
{
Request::enableHttpMethodParameterOverride();
Request::setAllowedHttpMethodOverride(['PUT']);

try {
$request = Request::create('http://example.com/', 'POST', ['_method' => 'PUT']);

$this->assertSame('PUT', $request->getMethod());
} finally {
(new \ReflectionProperty(Request::class, 'httpMethodParameterOverride'))->setValue(null, false);
}
}

public function testCreateWithRequestUri()
{
$request = Request::create('http://test.com:80/foo');
Expand Down
Loading