CVE-2018-14774 (PHP, published 2018-08-03T17:29Z)
Description: 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.
Severity: MEDIUM (CVSS v3 base score: 7.2)
Weakness: CWE-20 \u2014 Improper Input Validation
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.
Repository: https://github.com/symfony/symfony
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 message: [HttpKernel] fix trusted headers management in HttpCache and InlineFragmentRenderer


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