Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 1db2d2e

Browse files
security #cve-2018-14774 [HttpKernel] fix trusted headers management in HttpCache and InlineFragmentRenderer (nicolas-grekas)
* commit '7f912bbb78': [HttpKernel] fix trusted headers management in HttpCache and InlineFragmentRenderer
2 parents 6ad5fe6 + 7f912bb commit 1db2d2e

File tree

8 files changed

+341
-74
lines changed

8 files changed

+341
-74
lines changed

src/Symfony/Component/HttpFoundation/Request.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2060,6 +2060,11 @@ private function getTrustedValues($type, $ip = null)
20602060
if (self::$trustedHeaders[self::HEADER_FORWARDED] && $this->headers->has(self::$trustedHeaders[self::HEADER_FORWARDED])) {
20612061
$forwardedValues = $this->headers->get(self::$trustedHeaders[self::HEADER_FORWARDED]);
20622062
$forwardedValues = preg_match_all(sprintf('{(?:%s)=(?:"?\[?)([a-zA-Z0-9\.:_\-/]*+)}', self::$forwardedParams[$type]), $forwardedValues, $matches) ? $matches[1] : array();
2063+
if (self::HEADER_CLIENT_PORT === $type) {
2064+
foreach ($forwardedValues as $k => $v) {
2065+
$forwardedValues[$k] = substr_replace($v, '0.0.0.0', 0, strrpos($v, ':'));
2066+
}
2067+
}
20632068
}
20642069

20652070
if (null !== $ip) {

src/Symfony/Component/HttpKernel/Fragment/InlineFragmentRenderer.php

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
use Symfony\Component\HttpFoundation\Request;
1515
use Symfony\Component\HttpFoundation\Response;
16+
use Symfony\Component\HttpKernel\HttpCache\SubRequestHandler;
1617
use Symfony\Component\HttpKernel\HttpKernelInterface;
1718
use Symfony\Component\HttpKernel\Controller\ControllerReference;
1819
use Symfony\Component\HttpKernel\KernelEvents;
@@ -76,7 +77,7 @@ public function render($uri, Request $request, array $options = array())
7677

7778
$level = ob_get_level();
7879
try {
79-
return $this->kernel->handle($subRequest, HttpKernelInterface::SUB_REQUEST, false);
80+
return SubRequestHandler::handle($this->kernel, $subRequest, HttpKernelInterface::SUB_REQUEST, false);
8081
} catch (\Exception $e) {
8182
// we dispatch the exception event to trigger the logging
8283
// the response that comes back is simply ignored
@@ -109,24 +110,6 @@ protected function createSubRequest($uri, Request $request)
109110
$cookies = $request->cookies->all();
110111
$server = $request->server->all();
111112

112-
// Override the arguments to emulate a sub-request.
113-
// Sub-request object will point to localhost as client ip and real client ip
114-
// will be included into trusted header for client ip
115-
try {
116-
if (Request::HEADER_X_FORWARDED_FOR & Request::getTrustedHeaderSet()) {
117-
$currentXForwardedFor = $request->headers->get('X_FORWARDED_FOR', '');
118-
119-
$server['HTTP_X_FORWARDED_FOR'] = ($currentXForwardedFor ? $currentXForwardedFor.', ' : '').$request->getClientIp();
120-
} elseif (method_exists(Request::class, 'getTrustedHeaderName') && $trustedHeaderName = Request::getTrustedHeaderName(Request::HEADER_CLIENT_IP, false)) {
121-
$currentXForwardedFor = $request->headers->get($trustedHeaderName, '');
122-
123-
$server['HTTP_'.$trustedHeaderName] = ($currentXForwardedFor ? $currentXForwardedFor.', ' : '').$request->getClientIp();
124-
}
125-
} catch (\InvalidArgumentException $e) {
126-
// Do nothing
127-
}
128-
129-
$server['REMOTE_ADDR'] = '127.0.0.1';
130113
unset($server['HTTP_IF_MODIFIED_SINCE']);
131114
unset($server['HTTP_IF_NONE_MATCH']);
132115

src/Symfony/Component/HttpKernel/HttpCache/HttpCache.php

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -440,27 +440,8 @@ protected function forward(Request $request, $catch = false, Response $entry = n
440440
$this->surrogate->addSurrogateCapability($request);
441441
}
442442

443-
// modify the X-Forwarded-For header if needed
444-
$forwardedFor = $request->headers->get('X-Forwarded-For');
445-
if ($forwardedFor) {
446-
$request->headers->set('X-Forwarded-For', $forwardedFor.', '.$request->server->get('REMOTE_ADDR'));
447-
} else {
448-
$request->headers->set('X-Forwarded-For', $request->server->get('REMOTE_ADDR'));
449-
}
450-
451-
// fix the client IP address by setting it to 127.0.0.1 as HttpCache
452-
// is always called from the same process as the backend.
453-
$request->server->set('REMOTE_ADDR', '127.0.0.1');
454-
455-
// make sure HttpCache is a trusted proxy
456-
if (!in_array('127.0.0.1', $trustedProxies = Request::getTrustedProxies())) {
457-
$trustedProxies[] = '127.0.0.1';
458-
Request::setTrustedProxies($trustedProxies, Request::HEADER_X_FORWARDED_ALL);
459-
}
460-
461443
// always a "master" request (as the real master request can be in cache)
462-
$response = $this->kernel->handle($request, HttpKernelInterface::MASTER_REQUEST, $catch);
463-
// FIXME: we probably need to also catch exceptions if raw === true
444+
$response = SubRequestHandler::handle($this->kernel, $request, HttpKernelInterface::MASTER_REQUEST, $catch);
464445

465446
// we don't implement the stale-if-error on Requests, which is nonetheless part of the RFC
466447
if (null !== $entry && in_array($response->getStatusCode(), array(500, 502, 503, 504))) {
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\HttpCache;
13+
14+
use Symfony\Component\HttpFoundation\IpUtils;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpFoundation\Response;
17+
use Symfony\Component\HttpKernel\HttpKernelInterface;
18+
19+
/**
20+
* @author Nicolas Grekas <[email protected]>
21+
*
22+
* @internal
23+
*/
24+
class SubRequestHandler
25+
{
26+
/**
27+
* @return Response
28+
*/
29+
public static function handle(HttpKernelInterface $kernel, Request $request, $type, $catch)
30+
{
31+
// save global state related to trusted headers and proxies
32+
$trustedProxies = Request::getTrustedProxies();
33+
$trustedHeaderSet = Request::getTrustedHeaderSet();
34+
$trustedHeaders = array(
35+
Request::HEADER_FORWARDED => Request::getTrustedHeaderName(Request::HEADER_FORWARDED, false),
36+
Request::HEADER_X_FORWARDED_FOR => Request::getTrustedHeaderName(Request::HEADER_X_FORWARDED_FOR, false),
37+
Request::HEADER_X_FORWARDED_HOST => Request::getTrustedHeaderName(Request::HEADER_X_FORWARDED_HOST, false),
38+
Request::HEADER_X_FORWARDED_PROTO => Request::getTrustedHeaderName(Request::HEADER_X_FORWARDED_PROTO, false),
39+
Request::HEADER_X_FORWARDED_PORT => Request::getTrustedHeaderName(Request::HEADER_X_FORWARDED_PORT, false),
40+
);
41+
42+
// remove untrusted values
43+
$remoteAddr = $request->server->get('REMOTE_ADDR');
44+
if (!IpUtils::checkIp($remoteAddr, $trustedProxies)) {
45+
foreach (array_filter($trustedHeaders) as $name) {
46+
$request->headers->remove($name);
47+
}
48+
}
49+
50+
// compute trusted values, taking any trusted proxies into account
51+
$trustedIps = array();
52+
$trustedValues = array();
53+
foreach (array_reverse($request->getClientIps()) as $ip) {
54+
$trustedIps[] = $ip;
55+
$trustedValues[] = sprintf('for="%s"', $ip);
56+
}
57+
if ($ip !== $remoteAddr) {
58+
$trustedIps[] = $remoteAddr;
59+
$trustedValues[] = sprintf('for="%s"', $remoteAddr);
60+
}
61+
62+
// set trusted values, reusing as much as possible the global trusted settings
63+
if ($name = $trustedHeaders[Request::HEADER_FORWARDED]) {
64+
$trustedValues[0] .= sprintf(';host="%s";proto=%s', $request->getHttpHost(), $request->getScheme());
65+
$request->headers->set($name, implode(', ', $trustedValues));
66+
}
67+
if ($name = $trustedHeaders[Request::HEADER_X_FORWARDED_FOR]) {
68+
$request->headers->set($name, implode(', ', $trustedIps));
69+
}
70+
if (!$name && !$trustedHeaders[Request::HEADER_FORWARDED]) {
71+
Request::setTrustedProxies($trustedProxies, $trustedHeaderSet | Request::HEADER_X_FORWARDED_FOR);
72+
$request->headers->set(Request::getTrustedHeaderName(Request::HEADER_X_FORWARDED_FOR, false), implode(', ', $trustedIps));
73+
}
74+
75+
// fix the client IP address by setting it to 127.0.0.1,
76+
// which is the core responsibility of this method
77+
$request->server->set('REMOTE_ADDR', '127.0.0.1');
78+
79+
// ensure 127.0.0.1 is set as trusted proxy
80+
if (!IpUtils::checkIp('127.0.0.1', $trustedProxies)) {
81+
Request::setTrustedProxies(array_merge($trustedProxies, array('127.0.0.1')), Request::getTrustedHeaderSet());
82+
}
83+
84+
try {
85+
return $kernel->handle($request, $type, $catch);
86+
} finally {
87+
// restore global state
88+
Request::setTrustedProxies($trustedProxies, $trustedHeaderSet);
89+
}
90+
}
91+
}

src/Symfony/Component/HttpKernel/Tests/Fragment/InlineFragmentRendererTest.php

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public function testRenderWithObjectsAsAttributes()
4646
$subRequest = Request::create('/_fragment?_path=_format%3Dhtml%26_locale%3Den%26_controller%3Dmain_controller');
4747
$subRequest->attributes->replace(array('object' => $object, '_format' => 'html', '_controller' => 'main_controller', '_locale' => 'en'));
4848
$subRequest->headers->set('x-forwarded-for', array('127.0.0.1'));
49-
$subRequest->server->set('HTTP_X_FORWARDED_FOR', '127.0.0.1');
49+
$subRequest->headers->set('forwarded', array('for="127.0.0.1";host="localhost";proto=http'));
5050

5151
$strategy = new InlineFragmentRenderer($this->getKernelExpectingRequest($subRequest));
5252

@@ -99,7 +99,10 @@ public function testRenderWithTrustedHeaderDisabled()
9999
{
100100
Request::setTrustedProxies(array(), 0);
101101

102-
$strategy = new InlineFragmentRenderer($this->getKernelExpectingRequest(Request::create('/')));
102+
$expectedSubRequest = Request::create('/');
103+
$expectedSubRequest->headers->set('x-forwarded-for', array('127.0.0.1'));
104+
105+
$strategy = new InlineFragmentRenderer($this->getKernelExpectingRequest($expectedSubRequest));
103106
$this->assertSame('foo', $strategy->render('/', Request::create('/'))->getContent());
104107

105108
Request::setTrustedProxies(array(), -1);
@@ -190,8 +193,8 @@ public function testESIHeaderIsKeptInSubrequest()
190193

191194
if (Request::HEADER_X_FORWARDED_FOR & Request::getTrustedHeaderSet()) {
192195
$expectedSubRequest->headers->set('x-forwarded-for', array('127.0.0.1'));
193-
$expectedSubRequest->server->set('HTTP_X_FORWARDED_FOR', '127.0.0.1');
194196
}
197+
$expectedSubRequest->headers->set('forwarded', array('for="127.0.0.1";host="localhost";proto=http'));
195198

196199
$strategy = new InlineFragmentRenderer($this->getKernelExpectingRequest($expectedSubRequest));
197200

@@ -202,7 +205,7 @@ public function testESIHeaderIsKeptInSubrequest()
202205

203206
public function testESIHeaderIsKeptInSubrequestWithTrustedHeaderDisabled()
204207
{
205-
Request::setTrustedProxies(array(), 0);
208+
Request::setTrustedProxies(array(), Request::HEADER_FORWARDED);
206209

207210
$this->testESIHeaderIsKeptInSubrequest();
208211

@@ -212,16 +215,52 @@ public function testESIHeaderIsKeptInSubrequestWithTrustedHeaderDisabled()
212215
public function testHeadersPossiblyResultingIn304AreNotAssignedToSubrequest()
213216
{
214217
$expectedSubRequest = Request::create('/');
215-
if (Request::HEADER_X_FORWARDED_FOR & Request::getTrustedHeaderSet()) {
216-
$expectedSubRequest->headers->set('x-forwarded-for', array('127.0.0.1'));
217-
$expectedSubRequest->server->set('HTTP_X_FORWARDED_FOR', '127.0.0.1');
218-
}
218+
$expectedSubRequest->headers->set('x-forwarded-for', array('127.0.0.1'));
219+
$expectedSubRequest->headers->set('forwarded', array('for="127.0.0.1";host="localhost";proto=http'));
219220

220221
$strategy = new InlineFragmentRenderer($this->getKernelExpectingRequest($expectedSubRequest));
221222
$request = Request::create('/', 'GET', array(), array(), array(), array('HTTP_IF_MODIFIED_SINCE' => 'Fri, 01 Jan 2016 00:00:00 GMT', 'HTTP_IF_NONE_MATCH' => '*'));
222223
$strategy->render('/', $request);
223224
}
224225

226+
public function testFirstTrustedProxyIsSetAsRemote()
227+
{
228+
Request::setTrustedProxies(array('1.1.1.1'), -1);
229+
230+
$expectedSubRequest = Request::create('/');
231+
$expectedSubRequest->headers->set('Surrogate-Capability', 'abc="ESI/1.0"');
232+
$expectedSubRequest->server->set('REMOTE_ADDR', '127.0.0.1');
233+
$expectedSubRequest->headers->set('x-forwarded-for', array('127.0.0.1'));
234+
$expectedSubRequest->headers->set('forwarded', array('for="127.0.0.1";host="localhost";proto=http'));
235+
236+
$strategy = new InlineFragmentRenderer($this->getKernelExpectingRequest($expectedSubRequest));
237+
238+
$request = Request::create('/');
239+
$request->headers->set('Surrogate-Capability', 'abc="ESI/1.0"');
240+
$strategy->render('/', $request);
241+
242+
Request::setTrustedProxies(array(), -1);
243+
}
244+
245+
public function testIpAddressOfRangedTrustedProxyIsSetAsRemote()
246+
{
247+
$expectedSubRequest = Request::create('/');
248+
$expectedSubRequest->headers->set('Surrogate-Capability', 'abc="ESI/1.0"');
249+
$expectedSubRequest->server->set('REMOTE_ADDR', '127.0.0.1');
250+
$expectedSubRequest->headers->set('x-forwarded-for', array('127.0.0.1'));
251+
$expectedSubRequest->headers->set('forwarded', array('for="127.0.0.1";host="localhost";proto=http'));
252+
253+
Request::setTrustedProxies(array('1.1.1.1/24'), -1);
254+
255+
$strategy = new InlineFragmentRenderer($this->getKernelExpectingRequest($expectedSubRequest));
256+
257+
$request = Request::create('/');
258+
$request->headers->set('Surrogate-Capability', 'abc="ESI/1.0"');
259+
$strategy->render('/', $request);
260+
261+
Request::setTrustedProxies(array(), -1);
262+
}
263+
225264
/**
226265
* Creates a Kernel expecting a request equals to $request
227266
* Allows delta in comparison in case REQUEST_TIME changed by 1 second.

src/Symfony/Component/HttpKernel/Tests/HttpCache/HttpCacheTest.php

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1336,64 +1336,69 @@ public function testClientIpIsAlwaysLocalhostForForwardedRequests()
13361336
$this->setNextResponse();
13371337
$this->request('GET', '/', array('REMOTE_ADDR' => '10.0.0.1'));
13381338

1339-
$this->assertEquals('127.0.0.1', $this->kernel->getBackendRequest()->server->get('REMOTE_ADDR'));
1339+
$this->kernel->assert(function ($backendRequest) {
1340+
$this->assertSame('127.0.0.1', $backendRequest->server->get('REMOTE_ADDR'));
1341+
});
13401342
}
13411343

13421344
/**
13431345
* @dataProvider getTrustedProxyData
13441346
*/
1345-
public function testHttpCacheIsSetAsATrustedProxy(array $existing, array $expected)
1347+
public function testHttpCacheIsSetAsATrustedProxy(array $existing)
13461348
{
13471349
Request::setTrustedProxies($existing, Request::HEADER_X_FORWARDED_ALL);
13481350

13491351
$this->setNextResponse();
13501352
$this->request('GET', '/', array('REMOTE_ADDR' => '10.0.0.1'));
1353+
$this->assertSame($existing, Request::getTrustedProxies());
1354+
1355+
$existing = array_unique(array_merge($existing, array('127.0.0.1')));
1356+
$this->kernel->assert(function ($backendRequest) use ($existing) {
1357+
$this->assertSame($existing, Request::getTrustedProxies());
1358+
$this->assertsame('10.0.0.1', $backendRequest->getClientIp());
1359+
});
13511360

1352-
$this->assertEquals($expected, Request::getTrustedProxies());
1361+
Request::setTrustedProxies(array(), -1);
13531362
}
13541363

13551364
public function getTrustedProxyData()
13561365
{
13571366
return array(
1358-
array(array(), array('127.0.0.1')),
1359-
array(array('10.0.0.2'), array('10.0.0.2', '127.0.0.1')),
1360-
array(array('10.0.0.2', '127.0.0.1'), array('10.0.0.2', '127.0.0.1')),
1367+
array(array()),
1368+
array(array('10.0.0.2')),
1369+
array(array('10.0.0.2', '127.0.0.1')),
13611370
);
13621371
}
13631372

13641373
/**
1365-
* @dataProvider getXForwardedForData
1374+
* @dataProvider getForwardedData
13661375
*/
1367-
public function testXForwarderForHeaderForForwardedRequests($xForwardedFor, $expected)
1376+
public function testForwarderHeaderForForwardedRequests($forwarded, $expected)
13681377
{
13691378
$this->setNextResponse();
13701379
$server = array('REMOTE_ADDR' => '10.0.0.1');
1371-
if (false !== $xForwardedFor) {
1372-
$server['HTTP_X_FORWARDED_FOR'] = $xForwardedFor;
1380+
if (null !== $forwarded) {
1381+
Request::setTrustedProxies($server, -1);
1382+
$server['HTTP_FORWARDED'] = $forwarded;
13731383
}
13741384
$this->request('GET', '/', $server);
13751385

1376-
$this->assertEquals($expected, $this->kernel->getBackendRequest()->headers->get('X-Forwarded-For'));
1386+
$this->kernel->assert(function ($backendRequest) use ($expected) {
1387+
$this->assertSame($expected, $backendRequest->headers->get('Forwarded'));
1388+
});
1389+
1390+
Request::setTrustedProxies(array(), -1);
13771391
}
13781392

1379-
public function getXForwardedForData()
1393+
public function getForwardedData()
13801394
{
13811395
return array(
1382-
array(false, '10.0.0.1'),
1383-
array('10.0.0.2', '10.0.0.2, 10.0.0.1'),
1384-
array('10.0.0.2, 10.0.0.3', '10.0.0.2, 10.0.0.3, 10.0.0.1'),
1396+
array(null, 'for="10.0.0.1";host="localhost";proto=http'),
1397+
array('for=10.0.0.2', 'for="10.0.0.2";host="localhost";proto=http, for="10.0.0.1"'),
1398+
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"'),
13851399
);
13861400
}
13871401

1388-
public function testXForwarderForHeaderForPassRequests()
1389-
{
1390-
$this->setNextResponse();
1391-
$server = array('REMOTE_ADDR' => '10.0.0.1');
1392-
$this->request('POST', '/', $server);
1393-
1394-
$this->assertEquals('10.0.0.1', $this->kernel->getBackendRequest()->headers->get('X-Forwarded-For'));
1395-
}
1396-
13971402
public function testEsiCacheRemoveValidationHeadersIfEmbeddedResponses()
13981403
{
13991404
$time = \DateTime::createFromFormat('U', time());

0 commit comments

Comments
 (0)