CVE-2018-14774: Improper Input Validation (CWE-20)
value: An issue was discovered in HttpKernel in Symfony 2.7.0 through 2.7.48, 2.8.0 through 2.8.43, 3.3.0 through 3.3.17, 3.4.0 through 3.4.13, 4.0.0 through 4.0.13, and 4.1.0 through 4.1.2. When using HttpCache, the values of the X-Forwarded-Host headers are implicitly set as trusted while this should be forbidden, leading to potential host header injection.
CWE description: The product receives input or data, but it does
        not validate or incorrectly validates that the input has the
        properties that are required to process the data safely and
        correctly.
CVSS v3 base score: 7.2
Published: 2018-08-03T17:29Z
Repository: https://github.com/symfony/symfony
Language: PHP
Files changed: src/Symfony/Component/HttpFoundation/Request.php, src/Symfony/Component/HttpKernel/Fragment/InlineFragmentRenderer.php, src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php, src/Symfony/Component/HttpKernel/HttpCache/SubRequestHandler.php, src/Symfony/Component/HttpKernel/Tests/Fragment/InlineFragmentRendererTest.php, src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php, src/Symfony/Component/HttpKernel/Tests/HttpCache/SubRequestHandlerTest.php, src/Symfony/Component/HttpKernel/Tests/HttpCache/TestHttpKernel.php
Commit date: 2018-08-01 15:47:49 +0200
Commit message: [HttpKernel] fix trusted headers management in HttpCache and InlineFragmentRenderer

Diff: diff --git a/src/Symfony/Component/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php
index 7f34260494..9cc6a9773b 100644
--- a/src/Symfony/Component/HttpFoundation/Request.php
+++ b/src/Symfony/Component/HttpFoundation/Request.php
@@ -1942,10 +1942,15 @@ class Request
         }
 
         if (self::$trustedHeaders[self::HEADER_FORWARDED] && $this->headers->has(self::$trustedHeaders[self::HEADER_FORWARDED])) {
             $forwardedValues = $this->headers->get(self::$trustedHeaders[self::HEADER_FORWARDED]);
             $forwardedValues = preg_match_all(sprintf('{(?:%s)=(?:"?\[?)([a-zA-Z0-9\.:_\-/]*+)}', self::$forwardedParams[$type]), $forwardedValues, $matches) ? $matches[1] : array();
+            if (self::HEADER_CLIENT_PORT === $type) {
+                foreach ($forwardedValues as $k => $v) {
+                    $forwardedValues[$k] = substr_replace($v, '0.0.0.0', 0, strrpos($v, ':'));
+                }
+            }
         }
 
         if (null !== $ip) {
             $clientValues = $this->normalizeAndFilterClientIps($clientValues, $ip);
             $forwardedValues = $this->normalizeAndFilterClientIps($forwardedValues, $ip);
diff --git a/src/Symfony/Component/HttpKernel/Fragment/InlineFragmentRenderer.php b/src/Symfony/Component/HttpKernel/Fragment/InlineFragmentRenderer.php
index 31cfb0de09..712248578d 100644
--- a/src/Symfony/Component/HttpKernel/Fragment/InlineFragmentRenderer.php
+++ b/src/Symfony/Component/HttpKernel/Fragment/InlineFragmentRenderer.php
@@ -14,10 +14,11 @@ namespace Symfony\Component\HttpKernel\Fragment;
 use Symfony\Component\EventDispatcher\EventDispatcherInterface;
 use Symfony\Component\HttpFoundation\Request;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\HttpKernel\Controller\ControllerReference;
 use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
+use Symfony\Component\HttpKernel\HttpCache\SubRequestHandler;
 use Symfony\Component\HttpKernel\HttpKernelInterface;
 use Symfony\Component\HttpKernel\KernelEvents;
 
 /**
  * Implements the inline rendering strategy where the Request is rendered by the current HTTP kernel.
@@ -74,11 +75,11 @@ class InlineFragmentRenderer extends RoutableFragmentRenderer
             $subRequest->attributes->add($reference->attributes);
         }
 
         $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
             if (isset($options['ignore_errors']) && $options['ignore_errors'] && $this->dispatcher) {
                 $event = new GetResponseForExceptionEvent($this->kernel, $request, HttpKernelInterface::SUB_REQUEST, $e);
@@ -107,25 +108,10 @@ class InlineFragmentRenderer extends RoutableFragmentRenderer
     protected function createSubRequest($uri, Request $request)
     {
         $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'] = $this->resolveTrustedProxy();
-
         unset($server['HTTP_IF_MODIFIED_SINCE']);
         unset($server['HTTP_IF_NONE_MATCH']);
 
         $subRequest = Request::create($uri, 'get', array(), $cookies, array(), $server);
         if ($request->headers->has('Surrogate-Capability')) {
@@ -137,21 +123,10 @@ class InlineFragmentRenderer extends RoutableFragmentRenderer
         }
 
         return $subRequest;
     }
 
-    private function resolveTrustedProxy()
-    {
-        if (!$trustedProxies = Request::getTrustedProxies()) {
-            return '127.0.0.1';
-        }
-
-        $firstTrustedProxy = reset($trustedProxies);
-
-        return false !== ($i = strpos($firstTrustedProxy, '/')) ? substr($firstTrustedProxy, 0, $i) : $firstTrustedProxy;
-    }
-
     /**
      * {@inheritdoc}
      */
     public function getName()
     {
diff --git a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php
index de68eed942..f54c87aa4a 100644
--- a/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php
+++ b/src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php
@@ -466,31 +466,12 @@ class HttpCache implements HttpKernelInterface, TerminableInterface
     {
         if ($this->surrogate) {
             $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))) {
             if (null === $age = $entry->headers->getCacheControlDirective('stale-if-error')) {
                 $age = $this->options['stale_if_error'];
diff --git a/src/Symfony/Component/HttpKernel/HttpCache/SubRequestHandler.php b/src/Symfony/Component/HttpKernel/HttpCache/SubRequestHandler.php
new file mode 100644
index 0000000000..c050256025
--- /dev/null
+++ b/src/Symfony/Component/HttpKernel/HttpCache/SubRequestHandler.php
@@ -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;
+    }
+}
diff --git a/src/Symfony/Component/HttpKernel/Tests/Fragment/InlineFragmentRendererTest.php b/src/Symfony/Component/HttpKernel/Tests/Fragment/InlineFragmentRendererTest.php
index 481dcc7f4b..a955da20aa 100644
--- a/src/Symfony/Component/HttpKernel/Tests/Fragment/InlineFragmentRendererTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/Fragment/InlineFragmentRendererTest.php
@@ -24,16 +24,20 @@ class InlineFragmentRendererTest extends TestCase
 {
     private $originalTrustedHeaderName;
 
     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()
     {
         $strategy = new InlineFragmentRenderer($this->getKernel($this->returnValue(new Response('foo'))));
@@ -53,11 +57,11 @@ class InlineFragmentRendererTest extends TestCase
         $object = new \stdClass();
 
         $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));
 
         $this->assertSame('foo', $strategy->render(new ControllerReference('main_controller', array('object' => $object), array()), Request::create('/'))->getContent());
     }
@@ -81,12 +85,16 @@ 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());
     }
 
     /**
      * @expectedException \RuntimeException
@@ -166,15 +174,14 @@ class InlineFragmentRendererTest extends TestCase
 
     public function testESIHeaderIsKeptInSubrequest()
     {
         $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));
 
         $request = Request::create('/');
         $request->headers->set('Surrogate-Capability', 'abc="ESI/1.0"');
@@ -192,30 +199,25 @@ 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', '1.1.1.1');
-
-        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->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));
 
@@ -228,13 +230,13 @@ class InlineFragmentRendererTest extends TestCase
 
     public function testIpAddressOfRangedTrustedProxyIsSetAsRemote()
     {
         $expectedSubRequest = Request::create('/');
         $expectedSubRequest->headers->set('Surrogate-Capability', 'abc="ESI/1.0"');
-        $expectedSubRequest->server->set('REMOTE_ADDR', '1.1.1.1');
+        $expectedSubRequest->server->set('REMOTE_ADDR', '127.0.0.1');
         $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'));
 
         Request::setTrustedProxies(array('1.1.1.1/24'));
 
         $strategy = new InlineFragmentRenderer($this->getKernelExpectingRequest($expectedSubRequest));
 
diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php
index 7231fc1c8e..102d7644ed 100644
--- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php
@@ -1301,70 +1301,76 @@ class HttpCacheTest extends HttpCacheTestCase
     public function testClientIpIsAlwaysLocalhostForForwardedRequests()
     {
         $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());
 
         $responses = array(
diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/SubRequestHandlerTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/SubRequestHandlerTest.php
new file mode 100644
index 0000000000..395bc06445
--- /dev/null
+++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/SubRequestHandlerTest.php
@@ -0,0 +1,168 @@
+<?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";proto=https');
+        $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());
+            $that->assertSame(443, $request->getPort());
+        });
+
+        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();
+    }
+}
diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/TestHttpKernel.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/TestHttpKernel.php
index 198eafd2bf..429708416d 100644
--- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/TestHttpKernel.php
+++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/TestHttpKernel.php
@@ -32,23 +32,44 @@ class TestHttpKernel extends HttpKernel implements ControllerResolverInterface
     {
         $this->body = $body;
         $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);
     }
 
     public function isCatchingExceptions()

Vulnerable code:             return $this->kernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST, false);
        // 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'] = $this->resolveTrustedProxy();

    private function resolveTrustedProxy()
    {
        if (!$trustedProxies = Request::getTrustedProxies()) {
            return '127.0.0.1';
        }

        $firstTrustedProxy = reset($trustedProxies);

        return false !== ($i = strpos($firstTrustedProxy, '/')) ? substr($firstTrustedProxy, 0, $i) : $firstTrustedProxy;
    }

        // 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);
        }

        $response = $this->kernel->handle($request, HttpKernelInterface::MASTER_REQUEST, $catch);
        // FIXME: we probably need to also catch exceptions if raw === true
        $this->originalTrustedHeaderName = Request::getTrustedHeaderName(Request::HEADER_CLIENT_IP);
        Request::setTrustedHeaderName(Request::HEADER_CLIENT_IP, $this->originalTrustedHeaderName);
        $subRequest->server->set('HTTP_X_FORWARDED_FOR', '127.0.0.1');
        $strategy = new InlineFragmentRenderer($this->getKernelExpectingRequest(Request::create('/')));

            $expectedSubRequest->server->set('HTTP_X_FORWARDED_FOR', '127.0.0.1');
        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->server->set('REMOTE_ADDR', '1.1.1.1');

        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->server->set('REMOTE_ADDR', '1.1.1.1');
        $expectedSubRequest->server->set('HTTP_X_FORWARDED_FOR', '127.0.0.1');
        $this->assertEquals('127.0.0.1', $this->kernel->getBackendRequest()->server->get('REMOTE_ADDR'));
    public function testHttpCacheIsSetAsATrustedProxy(array $existing, array $expected)
        $this->assertEquals($expected, Request::getTrustedProxies());
            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')),
     * @dataProvider getXForwardedForData
    public function testXForwarderForHeaderForForwardedRequests($xForwardedFor, $expected)
        if (false !== $xForwardedFor) {
            $server['HTTP_X_FORWARDED_FOR'] = $xForwardedFor;
        $this->assertEquals($expected, $this->kernel->getBackendRequest()->headers->get('X-Forwarded-For'));
    public function getXForwardedForData()
            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'),
    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 getBackendRequest()
        return $this->backendRequest;
        $this->backendRequest = $request;
Fixed code:             if (self::HEADER_CLIENT_PORT === $type) {
                foreach ($forwardedValues as $k => $v) {
                    $forwardedValues[$k] = substr_replace($v, '0.0.0.0', 0, strrpos($v, ':'));
                }
            }
use Symfony\Component\HttpKernel\HttpCache\SubRequestHandler;
            return SubRequestHandler::handle($this->kernel, $subRequest, HttpKernelInterface::SUB_REQUEST, false);
        $response = SubRequestHandler::handle($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST, $catch);
<?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;
    }
}
        $this->originalTrustedHeaderNames = array(
            Request::getTrustedHeaderName(Request::HEADER_CLIENT_IP),
            Request::getTrustedHeaderName(Request::HEADER_FORWARDED),
        );
        Request::setTrustedHeaderName(Request::HEADER_CLIENT_IP, $this->originalTrustedHeaderNames[0]);
        Request::setTrustedHeaderName(Request::HEADER_FORWARDED, $this->originalTrustedHeaderNames[1]);
        $subRequest->headers->set('forwarded', array('for="127.0.0.1";host="localhost";proto=http'));
        Request::setTrustedHeaderName(Request::HEADER_FORWARDED, '');
        $expectedSubRequest = Request::create('/');
        $expectedSubRequest->headers->set('x-forwarded-for', array('127.0.0.1'));

        $strategy = new InlineFragmentRenderer($this->getKernelExpectingRequest($expectedSubRequest));
        $expectedSubRequest->headers->set('forwarded', array('for="127.0.0.1";host="localhost";proto=http'));
        $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'));
        $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'));
        $expectedSubRequest->server->set('REMOTE_ADDR', '127.0.0.1');
        $expectedSubRequest->headers->set('forwarded', array('for="127.0.0.1";host="localhost";proto=http'));
        $that = $this;
        $this->kernel->assert(function ($backendRequest) use ($that) {
            $that->assertSame('127.0.0.1', $backendRequest->server->get('REMOTE_ADDR'));
        });
    public function testHttpCacheIsSetAsATrustedProxy(array $existing)
        $this->assertSame($existing, 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());
        });
            array(array()),
            array(array('10.0.0.2')),
            array(array('10.0.0.2', '127.0.0.1')),
     * @dataProvider getForwardedData
    public function testForwarderHeaderForForwardedRequests($forwarded, $expected)
        if (null !== $forwarded) {
            Request::setTrustedProxies($server);
            $server['HTTP_FORWARDED'] = $forwarded;
        $that = $this;
        $this->kernel->assert(function ($backendRequest) use ($expected, $that) {
            $that->assertSame($expected, $backendRequest->headers->get('Forwarded'));
        });

        Request::setTrustedProxies(array());
    public function getForwardedData()
            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"'),
<?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";proto=https');
        $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());
            $that->assertSame(443, $request->getPort());
        });

        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();
    }
}
        $this->trustedHeadersReflector = new \ReflectionProperty('Symfony\Component\HttpFoundation\Request', 'trustedHeaders');
        $this->trustedHeadersReflector->setAccessible(true);
    public function assert(\Closure $callback)
        $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;
        }
        $this->backendRequest = array($request::getTrustedProxies(), $this->trustedHeadersReflector->getValue(), $request);