security #cve-2018-14774 [HttpKernel] fix trusted headers management in HttpCache and InlineFragmentRenderer (nicolas-grekas)

* commit '08a32d44b6':
  [HttpKernel] fix trusted headers management in HttpCache and InlineFragmentRenderer
This commit is contained in:
Nicolas Grekas 2018-08-01 14:45:04 +02:00
commit 9cfcaba0bf
7 changed files with 375 additions and 72 deletions

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\HttpKernel\Fragment;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpCache\SubRequestHandler;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Controller\ControllerReference;
use Symfony\Component\HttpKernel\KernelEvents;
@ -76,7 +77,7 @@ class InlineFragmentRenderer extends RoutableFragmentRenderer
$level = ob_get_level();
try {
return $this->kernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST, false);
return SubRequestHandler::handle($this->kernel, $subRequest, HttpKernelInterface::SUB_REQUEST, false);
} catch (\Exception $e) {
// we dispatch the exception event to trigger the logging
// the response that comes back is simply ignored
@ -109,20 +110,6 @@ class InlineFragmentRenderer extends RoutableFragmentRenderer
$cookies = $request->cookies->all();
$server = $request->server->all();
// Override the arguments to emulate a sub-request.
// Sub-request object will point to localhost as client ip and real client ip
// will be included into trusted header for client ip
try {
if ($trustedHeaderName = Request::getTrustedHeaderName(Request::HEADER_CLIENT_IP)) {
$currentXForwardedFor = $request->headers->get($trustedHeaderName, '');
$server['HTTP_'.$trustedHeaderName] = ($currentXForwardedFor ? $currentXForwardedFor.', ' : '').$request->getClientIp();
}
} catch (\InvalidArgumentException $e) {
// Do nothing
}
$server['REMOTE_ADDR'] = '127.0.0.1';
unset($server['HTTP_IF_MODIFIED_SINCE']);
unset($server['HTTP_IF_NONE_MATCH']);

View File

@ -464,27 +464,8 @@ class HttpCache implements HttpKernelInterface, TerminableInterface
$this->surrogate->addSurrogateCapability($request);
}
// modify the X-Forwarded-For header if needed
$forwardedFor = $request->headers->get('X-Forwarded-For');
if ($forwardedFor) {
$request->headers->set('X-Forwarded-For', $forwardedFor.', '.$request->server->get('REMOTE_ADDR'));
} else {
$request->headers->set('X-Forwarded-For', $request->server->get('REMOTE_ADDR'));
}
// fix the client IP address by setting it to 127.0.0.1 as HttpCache
// is always called from the same process as the backend.
$request->server->set('REMOTE_ADDR', '127.0.0.1');
// make sure HttpCache is a trusted proxy
if (!in_array('127.0.0.1', $trustedProxies = Request::getTrustedProxies())) {
$trustedProxies[] = '127.0.0.1';
Request::setTrustedProxies($trustedProxies);
}
// always a "master" request (as the real master request can be in cache)
$response = $this->kernel->handle($request, HttpKernelInterface::MASTER_REQUEST, $catch);
// FIXME: we probably need to also catch exceptions if raw === true
$response = SubRequestHandler::handle($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST, $catch);
// we don't implement the stale-if-error on Requests, which is nonetheless part of the RFC
if (null !== $entry && in_array($response->getStatusCode(), array(500, 502, 503, 504))) {

View File

@ -0,0 +1,100 @@
<?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\HttpKernel\HttpCache;
use Symfony\Component\HttpFoundation\IpUtils;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
/**
* @author Nicolas Grekas <p@tchwork.com>
*
* @internal
*/
class SubRequestHandler
{
/**
* @return Response
*/
public static function handle(HttpKernelInterface $kernel, Request $request, $type, $catch)
{
// save global state related to trusted headers and proxies
$trustedProxies = Request::getTrustedProxies();
$trustedHeaders = array(
Request::HEADER_FORWARDED => Request::getTrustedHeaderName(Request::HEADER_FORWARDED),
Request::HEADER_CLIENT_IP => Request::getTrustedHeaderName(Request::HEADER_CLIENT_IP),
Request::HEADER_CLIENT_HOST => Request::getTrustedHeaderName(Request::HEADER_CLIENT_HOST),
Request::HEADER_CLIENT_PROTO => Request::getTrustedHeaderName(Request::HEADER_CLIENT_PROTO),
Request::HEADER_CLIENT_PORT => Request::getTrustedHeaderName(Request::HEADER_CLIENT_PORT),
);
// remove untrusted values
$remoteAddr = $request->server->get('REMOTE_ADDR');
if (!IpUtils::checkIp($remoteAddr, $trustedProxies)) {
foreach (array_filter($trustedHeaders) as $name) {
$request->headers->remove($name);
}
}
// compute trusted values, taking any trusted proxies into account
$trustedIps = array();
$trustedValues = array();
foreach (array_reverse($request->getClientIps()) as $ip) {
$trustedIps[] = $ip;
$trustedValues[] = sprintf('for="%s"', $ip);
}
if ($ip !== $remoteAddr) {
$trustedIps[] = $remoteAddr;
$trustedValues[] = sprintf('for="%s"', $remoteAddr);
}
// set trusted values, reusing as much as possible the global trusted settings
if ($name = $trustedHeaders[Request::HEADER_FORWARDED]) {
$trustedValues[0] .= sprintf(';host="%s";proto=%s', $request->getHttpHost(), $request->getScheme());
$request->headers->set($name, implode(', ', $trustedValues));
}
if ($name = $trustedHeaders[Request::HEADER_CLIENT_IP]) {
$request->headers->set($name, implode(', ', $trustedIps));
}
if (!$name && !$trustedHeaders[Request::HEADER_FORWARDED]) {
$request->headers->set('X-Forwarded-For', implode(', ', $trustedIps));
Request::setTrustedHeaderName(Request::HEADER_CLIENT_IP, 'X_FORWARDED_FOR');
}
// fix the client IP address by setting it to 127.0.0.1,
// which is the core responsibility of this method
$request->server->set('REMOTE_ADDR', '127.0.0.1');
// ensure 127.0.0.1 is set as trusted proxy
if (!IpUtils::checkIp('127.0.0.1', $trustedProxies)) {
Request::setTrustedProxies(array_merge($trustedProxies, array('127.0.0.1')));
}
try {
$e = null;
$response = $kernel->handle($request, $type, $catch);
} catch (\Throwable $e) {
} catch (\Exception $e) {
}
// restore global state
Request::setTrustedHeaderName(Request::HEADER_CLIENT_IP, $trustedHeaders[Request::HEADER_CLIENT_IP]);
Request::setTrustedProxies($trustedProxies);
if (null !== $e) {
throw $e;
}
return $response;
}
}

View File

@ -26,12 +26,16 @@ class InlineFragmentRendererTest extends TestCase
protected function setUp()
{
$this->originalTrustedHeaderName = Request::getTrustedHeaderName(Request::HEADER_CLIENT_IP);
$this->originalTrustedHeaderNames = array(
Request::getTrustedHeaderName(Request::HEADER_CLIENT_IP),
Request::getTrustedHeaderName(Request::HEADER_FORWARDED),
);
}
protected function tearDown()
{
Request::setTrustedHeaderName(Request::HEADER_CLIENT_IP, $this->originalTrustedHeaderName);
Request::setTrustedHeaderName(Request::HEADER_CLIENT_IP, $this->originalTrustedHeaderNames[0]);
Request::setTrustedHeaderName(Request::HEADER_FORWARDED, $this->originalTrustedHeaderNames[1]);
}
public function testRender()
@ -55,7 +59,7 @@ class InlineFragmentRendererTest extends TestCase
$subRequest = Request::create('/_fragment?_path=_format%3Dhtml%26_locale%3Den%26_controller%3Dmain_controller');
$subRequest->attributes->replace(array('object' => $object, '_format' => 'html', '_controller' => 'main_controller', '_locale' => 'en'));
$subRequest->headers->set('x-forwarded-for', array('127.0.0.1'));
$subRequest->server->set('HTTP_X_FORWARDED_FOR', '127.0.0.1');
$subRequest->headers->set('forwarded', array('for="127.0.0.1";host="localhost";proto=http'));
$strategy = new InlineFragmentRenderer($this->getKernelExpectingRequest($subRequest));
@ -83,8 +87,12 @@ class InlineFragmentRendererTest extends TestCase
public function testRenderWithTrustedHeaderDisabled()
{
Request::setTrustedHeaderName(Request::HEADER_CLIENT_IP, '');
Request::setTrustedHeaderName(Request::HEADER_FORWARDED, '');
$strategy = new InlineFragmentRenderer($this->getKernelExpectingRequest(Request::create('/')));
$expectedSubRequest = Request::create('/');
$expectedSubRequest->headers->set('x-forwarded-for', array('127.0.0.1'));
$strategy = new InlineFragmentRenderer($this->getKernelExpectingRequest($expectedSubRequest));
$this->assertSame('foo', $strategy->render('/', Request::create('/'))->getContent());
}
@ -168,11 +176,10 @@ class InlineFragmentRendererTest extends TestCase
{
$expectedSubRequest = Request::create('/');
$expectedSubRequest->headers->set('Surrogate-Capability', 'abc="ESI/1.0"');
if (Request::getTrustedHeaderName(Request::HEADER_CLIENT_IP)) {
$expectedSubRequest->headers->set('x-forwarded-for', array('127.0.0.1'));
$expectedSubRequest->server->set('HTTP_X_FORWARDED_FOR', '127.0.0.1');
}
$expectedSubRequest->headers->set('forwarded', array('for="127.0.0.1";host="localhost";proto=http'));
$strategy = new InlineFragmentRenderer($this->getKernelExpectingRequest($expectedSubRequest));
@ -194,16 +201,52 @@ class InlineFragmentRendererTest extends TestCase
public function testHeadersPossiblyResultingIn304AreNotAssignedToSubrequest()
{
$expectedSubRequest = Request::create('/');
if (Request::getTrustedHeaderName(Request::HEADER_CLIENT_IP)) {
$expectedSubRequest->headers->set('x-forwarded-for', array('127.0.0.1'));
$expectedSubRequest->server->set('HTTP_X_FORWARDED_FOR', '127.0.0.1');
}
$expectedSubRequest->headers->set('x-forwarded-for', array('127.0.0.1'));
$expectedSubRequest->headers->set('forwarded', array('for="127.0.0.1";host="localhost";proto=http'));
$strategy = new InlineFragmentRenderer($this->getKernelExpectingRequest($expectedSubRequest));
$request = Request::create('/', 'GET', array(), array(), array(), array('HTTP_IF_MODIFIED_SINCE' => 'Fri, 01 Jan 2016 00:00:00 GMT', 'HTTP_IF_NONE_MATCH' => '*'));
$strategy->render('/', $request);
}
public function testFirstTrustedProxyIsSetAsRemote()
{
$expectedSubRequest = Request::create('/');
$expectedSubRequest->headers->set('Surrogate-Capability', 'abc="ESI/1.0"');
$expectedSubRequest->server->set('REMOTE_ADDR', '127.0.0.1');
$expectedSubRequest->headers->set('x-forwarded-for', array('127.0.0.1'));
$expectedSubRequest->headers->set('forwarded', array('for="127.0.0.1";host="localhost";proto=http'));
Request::setTrustedProxies(array('1.1.1.1'));
$strategy = new InlineFragmentRenderer($this->getKernelExpectingRequest($expectedSubRequest));
$request = Request::create('/');
$request->headers->set('Surrogate-Capability', 'abc="ESI/1.0"');
$strategy->render('/', $request);
Request::setTrustedProxies(array());
}
public function testIpAddressOfRangedTrustedProxyIsSetAsRemote()
{
$expectedSubRequest = Request::create('/');
$expectedSubRequest->headers->set('Surrogate-Capability', 'abc="ESI/1.0"');
$expectedSubRequest->server->set('REMOTE_ADDR', '127.0.0.1');
$expectedSubRequest->headers->set('x-forwarded-for', array('127.0.0.1'));
$expectedSubRequest->headers->set('forwarded', array('for="127.0.0.1";host="localhost";proto=http'));
Request::setTrustedProxies(array('1.1.1.1/24'));
$strategy = new InlineFragmentRenderer($this->getKernelExpectingRequest($expectedSubRequest));
$request = Request::create('/');
$request->headers->set('Surrogate-Capability', 'abc="ESI/1.0"');
$strategy->render('/', $request);
Request::setTrustedProxies(array());
}
/**
* Creates a Kernel expecting a request equals to $request
* Allows delta in comparison in case REQUEST_TIME changed by 1 second.

View File

@ -1301,64 +1301,72 @@ class HttpCacheTest extends HttpCacheTestCase
$this->setNextResponse();
$this->request('GET', '/', array('REMOTE_ADDR' => '10.0.0.1'));
$this->assertEquals('127.0.0.1', $this->kernel->getBackendRequest()->server->get('REMOTE_ADDR'));
$that = $this;
$this->kernel->assert(function ($backendRequest) use ($that) {
$that->assertSame('127.0.0.1', $backendRequest->server->get('REMOTE_ADDR'));
});
}
/**
* @dataProvider getTrustedProxyData
*/
public function testHttpCacheIsSetAsATrustedProxy(array $existing, array $expected)
public function testHttpCacheIsSetAsATrustedProxy(array $existing)
{
Request::setTrustedProxies($existing);
$this->setNextResponse();
$this->request('GET', '/', array('REMOTE_ADDR' => '10.0.0.1'));
$this->assertSame($existing, Request::getTrustedProxies());
$this->assertEquals($expected, Request::getTrustedProxies());
$that = $this;
$existing = array_unique(array_merge($existing, array('127.0.0.1')));
$this->kernel->assert(function ($backendRequest) use ($existing, $that) {
$that->assertSame($existing, Request::getTrustedProxies());
$that->assertsame('10.0.0.1', $backendRequest->getClientIp());
});
Request::setTrustedProxies(array());
}
public function getTrustedProxyData()
{
return array(
array(array(), array('127.0.0.1')),
array(array('10.0.0.2'), array('10.0.0.2', '127.0.0.1')),
array(array('10.0.0.2', '127.0.0.1'), array('10.0.0.2', '127.0.0.1')),
array(array()),
array(array('10.0.0.2')),
array(array('10.0.0.2', '127.0.0.1')),
);
}
/**
* @dataProvider getXForwardedForData
* @dataProvider getForwardedData
*/
public function testXForwarderForHeaderForForwardedRequests($xForwardedFor, $expected)
public function testForwarderHeaderForForwardedRequests($forwarded, $expected)
{
$this->setNextResponse();
$server = array('REMOTE_ADDR' => '10.0.0.1');
if (false !== $xForwardedFor) {
$server['HTTP_X_FORWARDED_FOR'] = $xForwardedFor;
if (null !== $forwarded) {
Request::setTrustedProxies($server);
$server['HTTP_FORWARDED'] = $forwarded;
}
$this->request('GET', '/', $server);
$this->assertEquals($expected, $this->kernel->getBackendRequest()->headers->get('X-Forwarded-For'));
$that = $this;
$this->kernel->assert(function ($backendRequest) use ($expected, $that) {
$that->assertSame($expected, $backendRequest->headers->get('Forwarded'));
});
Request::setTrustedProxies(array());
}
public function getXForwardedForData()
public function getForwardedData()
{
return array(
array(false, '10.0.0.1'),
array('10.0.0.2', '10.0.0.2, 10.0.0.1'),
array('10.0.0.2, 10.0.0.3', '10.0.0.2, 10.0.0.3, 10.0.0.1'),
array(null, 'for="10.0.0.1";host="localhost";proto=http'),
array('for=10.0.0.2', 'for="10.0.0.2";host="localhost";proto=http, for="10.0.0.1"'),
array('for=10.0.0.2, for=10.0.0.3', 'for="10.0.0.2";host="localhost";proto=http, for="10.0.0.3", for="10.0.0.1"'),
);
}
public function testXForwarderForHeaderForPassRequests()
{
$this->setNextResponse();
$server = array('REMOTE_ADDR' => '10.0.0.1');
$this->request('POST', '/', $server);
$this->assertEquals('10.0.0.1', $this->kernel->getBackendRequest()->headers->get('X-Forwarded-For'));
}
public function testEsiCacheRemoveValidationHeadersIfEmbeddedResponses()
{
$time = \DateTime::createFromFormat('U', time());

View File

@ -0,0 +1,163 @@
<?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\HttpKernel\Tests\HttpCache;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpCache\SubRequestHandler;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class SubRequestHandlerTest extends TestCase
{
private static $globalState;
protected function setUp()
{
self::$globalState = $this->getGlobalState();
}
protected function tearDown()
{
foreach (self::$globalState[1] as $key => $name) {
Request::setTrustedHeaderName($key, $name);
}
Request::setTrustedProxies(self::$globalState[0]);
}
public function testTrustedHeadersAreKept()
{
Request::setTrustedProxies(array('10.0.0.1'));
$globalState = $this->getGlobalState();
$request = Request::create('/');
$request->server->set('REMOTE_ADDR', '10.0.0.1');
$request->headers->set('X-Forwarded-For', '10.0.0.2');
$request->headers->set('X-Forwarded-Host', 'Good');
$request->headers->set('X-Forwarded-Port', '1234');
$request->headers->set('X-Forwarded-Proto', 'https');
$that = $this;
$kernel = new TestSubRequestHandlerKernel(function ($request, $type, $catch) use ($that) {
$that->assertSame('127.0.0.1', $request->server->get('REMOTE_ADDR'));
$that->assertSame('10.0.0.2', $request->getClientIp());
$that->assertSame('Good', $request->headers->get('X-Forwarded-Host'));
$that->assertSame('1234', $request->headers->get('X-Forwarded-Port'));
$that->assertSame('https', $request->headers->get('X-Forwarded-Proto'));
});
SubRequestHandler::handle($kernel, $request, HttpKernelInterface::MASTER_REQUEST, true);
$this->assertSame($globalState, $this->getGlobalState());
}
public function testUntrustedHeadersAreRemoved()
{
$request = Request::create('/');
$request->server->set('REMOTE_ADDR', '10.0.0.1');
$request->headers->set('X-Forwarded-For', '10.0.0.2');
$request->headers->set('X-Forwarded-Host', 'Evil');
$request->headers->set('X-Forwarded-Port', '1234');
$request->headers->set('X-Forwarded-Proto', 'http');
$request->headers->set('Forwarded', 'Evil2');
$that = $this;
$kernel = new TestSubRequestHandlerKernel(function ($request, $type, $catch) use ($that) {
$that->assertSame('127.0.0.1', $request->server->get('REMOTE_ADDR'));
$that->assertSame('10.0.0.1', $request->getClientIp());
$that->assertFalse($request->headers->has('X-Forwarded-Host'));
$that->assertFalse($request->headers->has('X-Forwarded-Port'));
$that->assertFalse($request->headers->has('X-Forwarded-Proto'));
$that->assertSame('for="10.0.0.1";host="localhost";proto=http', $request->headers->get('Forwarded'));
});
SubRequestHandler::handle($kernel, $request, HttpKernelInterface::MASTER_REQUEST, true);
$this->assertSame(self::$globalState, $this->getGlobalState());
}
public function testTrustedForwardedHeader()
{
Request::setTrustedProxies(array('10.0.0.1'));
$globalState = $this->getGlobalState();
$request = Request::create('/');
$request->server->set('REMOTE_ADDR', '10.0.0.1');
$request->headers->set('Forwarded', 'for="10.0.0.2";host="foo.bar:1234";proto=https');
$that = $this;
$kernel = new TestSubRequestHandlerKernel(function ($request, $type, $catch) use ($that) {
$that->assertSame('127.0.0.1', $request->server->get('REMOTE_ADDR'));
$that->assertSame('10.0.0.2', $request->getClientIp());
});
SubRequestHandler::handle($kernel, $request, HttpKernelInterface::MASTER_REQUEST, true);
$this->assertSame($globalState, $this->getGlobalState());
}
public function testTrustedXForwardedForHeader()
{
Request::setTrustedProxies(array('10.0.0.1'));
$globalState = $this->getGlobalState();
$request = Request::create('/');
$request->server->set('REMOTE_ADDR', '10.0.0.1');
$request->headers->set('X-Forwarded-For', '10.0.0.2');
$request->headers->set('X-Forwarded-Host', 'foo.bar');
$request->headers->set('X-Forwarded-Proto', 'https');
$that = $this;
$kernel = new TestSubRequestHandlerKernel(function ($request, $type, $catch) use ($that) {
$that->assertSame('127.0.0.1', $request->server->get('REMOTE_ADDR'));
$that->assertSame('10.0.0.2', $request->getClientIp());
$that->assertSame('foo.bar', $request->getHttpHost());
$that->assertSame('https', $request->getScheme());
});
SubRequestHandler::handle($kernel, $request, HttpKernelInterface::MASTER_REQUEST, true);
$this->assertSame($globalState, $this->getGlobalState());
}
private function getGlobalState()
{
return array(
Request::getTrustedProxies(),
array(
Request::HEADER_FORWARDED => Request::getTrustedHeaderName(Request::HEADER_FORWARDED),
Request::HEADER_CLIENT_IP => Request::getTrustedHeaderName(Request::HEADER_CLIENT_IP),
Request::HEADER_CLIENT_HOST => Request::getTrustedHeaderName(Request::HEADER_CLIENT_HOST),
Request::HEADER_CLIENT_PROTO => Request::getTrustedHeaderName(Request::HEADER_CLIENT_PROTO),
Request::HEADER_CLIENT_PORT => Request::getTrustedHeaderName(Request::HEADER_CLIENT_PORT),
),
);
}
}
class TestSubRequestHandlerKernel implements HttpKernelInterface
{
private $assertCallback;
public function __construct(\Closure $assertCallback)
{
$this->assertCallback = $assertCallback;
}
public function handle(Request $request, $type = self::MASTER_REQUEST, $catch = true)
{
$assertCallback = $this->assertCallback;
$assertCallback($request, $type, $catch);
return new Response();
}
}

View File

@ -34,19 +34,40 @@ class TestHttpKernel extends HttpKernel implements ControllerResolverInterface
$this->status = $status;
$this->headers = $headers;
$this->customizer = $customizer;
$this->trustedHeadersReflector = new \ReflectionProperty('Symfony\Component\HttpFoundation\Request', 'trustedHeaders');
$this->trustedHeadersReflector->setAccessible(true);
parent::__construct(new EventDispatcher(), $this);
}
public function getBackendRequest()
public function assert(\Closure $callback)
{
return $this->backendRequest;
$trustedConfig = array(Request::getTrustedProxies(), $this->trustedHeadersReflector->getValue());
list($trustedProxies, $trustedHeaders, $backendRequest) = $this->backendRequest;
Request::setTrustedProxies($trustedProxies);
$this->trustedHeadersReflector->setValue(null, $trustedHeaders);
try {
$e = null;
$callback($backendRequest);
} catch (\Throwable $e) {
} catch (\Exception $e) {
}
list($trustedProxies, $trustedHeaders) = $trustedConfig;
Request::setTrustedProxies($trustedProxies);
$this->trustedHeadersReflector->setValue(null, $trustedHeaders);
if (null !== $e) {
throw $e;
}
}
public function handle(Request $request, $type = HttpKernelInterface::MASTER_REQUEST, $catch = false)
{
$this->catch = $catch;
$this->backendRequest = $request;
$this->backendRequest = array($request::getTrustedProxies(), $this->trustedHeadersReflector->getValue(), $request);
return parent::handle($request, $type, $catch);
}