Skip to content

Request exceptions #20962

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Dec 17, 2016
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
3 changes: 3 additions & 0 deletions src/Symfony/Component/Debug/Exception/FlattenException.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\Debug\Exception;

use Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface;
use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface;

/**
Expand Down Expand Up @@ -41,6 +42,8 @@ public static function create(\Exception $exception, $statusCode = null, array $
if ($exception instanceof HttpExceptionInterface) {
$statusCode = $exception->getStatusCode();
$headers = array_merge($headers, $exception->getHeaders());
} elseif ($exception instanceof RequestExceptionInterface) {
$statusCode = 400;
}

if (null === $statusCode) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\Debug\Tests\Exception;

use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
Expand Down Expand Up @@ -78,6 +79,11 @@ public function testStatusCode()

$flattened = FlattenException::create(new UnsupportedMediaTypeHttpException());
$this->assertEquals('415', $flattened->getStatusCode());

if (class_exists(SuspiciousOperationException::class)) {
$flattened = FlattenException::create(new SuspiciousOperationException());
$this->assertEquals('400', $flattened->getStatusCode());
}
}

public function testHeadersForHttpException()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@
/**
* The HTTP request contains headers with conflicting information.
*
* This exception should trigger an HTTP 400 response in your application code.
*
* @author Magnus Nordlander <magnus@fervo.se>
*/
class ConflictingHeadersException extends \RuntimeException
class ConflictingHeadersException extends \UnexpectedValueException implements RequestExceptionInterface
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\HttpFoundation\Exception;

/**
* Interface for Request exceptions.
*
* Exceptions implementing this interface should trigger an HTTP 400 response in the application code.
*/
interface RequestExceptionInterface
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\HttpFoundation\Exception;

/**
* Raised when a user has performed an operation that should be considered
* suspicious from a security perspective.
*/
class SuspiciousOperationException extends \UnexpectedValueException implements RequestExceptionInterface
{
}
27 changes: 23 additions & 4 deletions src/Symfony/Component/HttpFoundation/Request.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
namespace Symfony\Component\HttpFoundation;

use Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException;
use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException;
use Symfony\Component\HttpFoundation\Session\SessionInterface;

/**
Expand Down Expand Up @@ -206,6 +207,9 @@ class Request

protected static $requestFactory;

private $isHostValid = true;
private $isClientIpsValid = true;

/**
* Constructor.
*
Expand Down Expand Up @@ -819,7 +823,12 @@ public function getClientIps()
}

if ($hasTrustedForwardedHeader && $hasTrustedClientIpHeader && $forwardedClientIps !== $xForwardedForClientIps) {
throw new ConflictingHeadersException('The request has both a trusted Forwarded header and a trusted Client IP header, conflicting with each other with regards to the originating IP addresses of the request. This is the result of a misconfiguration. You should either configure your proxy only to send one of these headers, or configure Symfony to distrust one of them.');
if (!$this->isClientIpsValid) {
return array('0.0.0.0');
}
$this->isClientIpsValid = false;

throw new ConflictingHeadersException('The request has both a trusted Forwarded header and a trusted Client IP header, conflicting with each other with regards to the originating IP addresses of the request. This is the result of a misconfiguration. You should either configure your proxy only to send one of these headers, or configure your project to distrust one of them.');
}

if (!$hasTrustedForwardedHeader && !$hasTrustedClientIpHeader) {
Expand Down Expand Up @@ -1198,7 +1207,7 @@ public function isSecure()
*
* @return string
*
* @throws \UnexpectedValueException when the host name is invalid
* @throws SuspiciousOperationException when the host name is invalid or not trusted
*/
public function getHost()
{
Expand All @@ -1220,7 +1229,12 @@ public function getHost()
// check that it does not contain forbidden characters (see RFC 952 and RFC 2181)
// use preg_replace() instead of preg_match() to prevent DoS attacks with long host names
if ($host && '' !== preg_replace('/(?:^\[)?[a-zA-Z0-9-:\]_]+\.?/', '', $host)) {
throw new \UnexpectedValueException(sprintf('Invalid Host "%s"', $host));
if (!$this->isHostValid) {
Copy link
Member

Choose a reason for hiding this comment

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

Why method produce a different outcome depending on whether it was called before?

Copy link
Member Author

Choose a reason for hiding this comment

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

That's what I tried to explained in the description.

The first time you try to get the host (it happens in the request validation or router listener for instance), you get the exception, which means that you should then not access the request again. But, when handling the exception in the kernel, you might handle another request in development or production to convert it to a proper HTTP response (think API, ...). The problem comes from the fact that the request used to handle this exception is a duplicate of the original request. If we throw an exception again here, which will happen without this ugly code, then exception handling fails and the original exception bubbles. This is what we are trying to avoid here.

return '';
}
$this->isHostValid = false;

throw new SuspiciousOperationException(sprintf('Invalid Host "%s".', $host));
}

if (count(self::$trustedHostPatterns) > 0) {
Expand All @@ -1238,7 +1252,12 @@ public function getHost()
}
}

throw new \UnexpectedValueException(sprintf('Untrusted Host "%s"', $host));
if (!$this->isHostValid) {
return '';
}
$this->isHostValid = false;

throw new SuspiciousOperationException(sprintf('Untrusted Host "%s".', $host));
}

return $host;
Expand Down
7 changes: 4 additions & 3 deletions src/Symfony/Component/HttpFoundation/Tests/RequestTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

namespace Symfony\Component\HttpFoundation\Tests;

use Symfony\Component\HttpFoundation\Exception\SuspiciousOperationException;
use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\HttpFoundation\Request;
Expand Down Expand Up @@ -1871,8 +1872,8 @@ public function testTrustedHosts()
try {
$request->getHost();
$this->fail('Request::getHost() should throw an exception when host is not trusted.');
} catch (\UnexpectedValueException $e) {
$this->assertEquals('Untrusted Host "evil.com"', $e->getMessage());
} catch (SuspiciousOperationException $e) {
$this->assertEquals('Untrusted Host "evil.com".', $e->getMessage());
}

// trusted hosts
Expand Down Expand Up @@ -1935,7 +1936,7 @@ public function testHostValidity($host, $isValid, $expectedHost = null, $expecte
$this->assertSame($expectedPort, $request->getPort());
}
} else {
$this->setExpectedException('UnexpectedValueException', 'Invalid Host');
$this->setExpectedException(SuspiciousOperationException::class, 'Invalid Host');
$request->getHost();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,7 @@
use Symfony\Component\HttpKernel\KernelEvents;

/**
* Validates that the headers and other information indicating the
* client IP address of a request are consistent.
* Validates Requests.
*
* @author Magnus Nordlander <magnus@fervo.se>
*/
Expand All @@ -36,9 +35,10 @@ public function onKernelRequest(GetResponseEvent $event)
$request = $event->getRequest();

if ($request::getTrustedProxies()) {
// This will throw an exception if the headers are inconsistent.
$request->getClientIps();
}

$request->getHost();
}

/**
Expand Down
6 changes: 3 additions & 3 deletions src/Symfony/Component/HttpKernel/HttpKernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
use Symfony\Component\HttpKernel\Event\GetResponseForControllerResultEvent;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\Event\PostResponseEvent;
use Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException;
use Symfony\Component\HttpFoundation\Exception\RequestExceptionInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
Expand Down Expand Up @@ -67,8 +67,8 @@ public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQ
try {
return $this->handleRaw($request, $type);
} catch (\Exception $e) {
if ($e instanceof ConflictingHeadersException) {
$e = new BadRequestHttpException('The request headers contain conflicting information regarding the origin of this request.', $e);
if ($e instanceof RequestExceptionInterface) {
$e = new BadRequestHttpException($e->getMessage(), $e);
}
if (false === $catch) {
$this->finishRequest($request, $type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,17 @@
* file that was distributed with this source code.
*/

namespace Symfony\Component\HttpKernel\Tests\EventListener;

use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
use Symfony\Component\HttpKernel\Controller\ControllerResolver;
use Symfony\Component\HttpKernel\EventListener\ExceptionListener;
use Symfony\Component\HttpKernel\EventListener\RouterListener;
use Symfony\Component\HttpKernel\EventListener\ValidateRequestListener;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Routing\RequestContext;

Expand Down Expand Up @@ -154,4 +160,26 @@ public function getLoggingParameterData()
array(array(), 'Matched route "{route}".', array('route' => 'n/a', 'route_parameters' => array(), 'request_uri' => 'http://localhost/', 'method' => 'GET')),
);
}

public function testWithBadRequest()
{
$requestStack = new RequestStack();

$requestMatcher = $this->getMock('Symfony\Component\Routing\Matcher\RequestMatcherInterface');
$requestMatcher->expects($this->never())->method('matchRequest');

$dispatcher = new EventDispatcher();
$dispatcher->addSubscriber(new ValidateRequestListener());
$dispatcher->addSubscriber(new RouterListener($requestMatcher, $requestStack, new RequestContext()));
$dispatcher->addSubscriber(new ExceptionListener(function () {
return new Response('Exception handled', 400);
}));

$kernel = new HttpKernel($dispatcher, new ControllerResolver(), $requestStack, new ArgumentResolver());

$request = Request::create('http://localhost/');
$request->headers->set('host', '###');
$response = $kernel->handle($request);
$this->assertSame(400, $response->getStatusCode());
}
}
2 changes: 1 addition & 1 deletion src/Symfony/Component/HttpKernel/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"require": {
"php": ">=5.5.9",
"symfony/event-dispatcher": "~2.8|~3.0",
"symfony/http-foundation": "~2.8.13|~3.1.6|~3.2",
"symfony/http-foundation": "~3.3",
"symfony/debug": "~2.8|~3.0",
"psr/log": "~1.0"
},
Expand Down