Skip to content

Commit 3ea392a

Browse files
committed
Merge branch '3.1'
* 3.1: [HttpFoundation] Warning when request has both Forwarded and X-Forwarded-For fixed test [Console] Decouple SymfonyStyle from TableCell
2 parents 4a0be68 + 5128cd3 commit 3ea392a

File tree

9 files changed

+279
-40
lines changed

9 files changed

+279
-40
lines changed

src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,5 +61,9 @@
6161
<argument type="service" id="request_stack" />
6262
<tag name="kernel.event_subscriber" />
6363
</service>
64+
65+
<service id="validate_request_listener" class="Symfony\Component\HttpKernel\EventListener\ValidateRequestListener">
66+
<tag name="kernel.event_subscriber" />
67+
</service>
6468
</services>
6569
</container>

src/Symfony/Component/Console/Style/SymfonyStyle.php

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
use Symfony\Component\Console\Helper\ProgressBar;
1818
use Symfony\Component\Console\Helper\SymfonyQuestionHelper;
1919
use Symfony\Component\Console\Helper\Table;
20-
use Symfony\Component\Console\Helper\TableCell;
2120
use Symfony\Component\Console\Input\InputInterface;
2221
use Symfony\Component\Console\Output\BufferedOutput;
2322
use Symfony\Component\Console\Output\OutputInterface;
@@ -186,21 +185,13 @@ public function caution($message)
186185
*/
187186
public function table(array $headers, array $rows)
188187
{
189-
array_walk_recursive($headers, function (&$value) {
190-
if ($value instanceof TableCell) {
191-
$value = new TableCell(sprintf('<info>%s</>', $value), array(
192-
'colspan' => $value->getColspan(),
193-
'rowspan' => $value->getRowspan(),
194-
));
195-
} else {
196-
$value = sprintf('<info>%s</>', $value);
197-
}
198-
});
188+
$style = clone Table::getStyleDefinition('symfony-style-guide');
189+
$style->setCellHeaderFormat('<info>%s</info>');
199190

200191
$table = new Table($this);
201192
$table->setHeaders($headers);
202193
$table->setRows($rows);
203-
$table->setStyle('symfony-style-guide');
194+
$table->setStyle($style);
204195

205196
$table->render();
206197
$this->newLine();
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\HttpFoundation\Exception;
13+
14+
/**
15+
* The HTTP request contains headers with conflicting information.
16+
*
17+
* This exception should trigger an HTTP 400 response in your application code.
18+
*
19+
* @author Magnus Nordlander <magnus@fervo.se>
20+
*/
21+
class ConflictingHeadersException extends \RuntimeException
22+
{
23+
}

src/Symfony/Component/HttpFoundation/Request.php

Lines changed: 51 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\HttpFoundation;
1313

14+
use Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException;
1415
use Symfony\Component\HttpFoundation\Session\SessionInterface;
1516

1617
/**
@@ -798,41 +799,34 @@ public function getClientIps()
798799
return array($ip);
799800
}
800801

801-
if (self::$trustedHeaders[self::HEADER_FORWARDED] && $this->headers->has(self::$trustedHeaders[self::HEADER_FORWARDED])) {
802+
$hasTrustedForwardedHeader = self::$trustedHeaders[self::HEADER_FORWARDED] && $this->headers->has(self::$trustedHeaders[self::HEADER_FORWARDED]);
803+
$hasTrustedClientIpHeader = self::$trustedHeaders[self::HEADER_CLIENT_IP] && $this->headers->has(self::$trustedHeaders[self::HEADER_CLIENT_IP]);
804+
805+
if ($hasTrustedForwardedHeader) {
802806
$forwardedHeader = $this->headers->get(self::$trustedHeaders[self::HEADER_FORWARDED]);
803807
preg_match_all('{(for)=("?\[?)([a-z0-9\.:_\-/]*)}', $forwardedHeader, $matches);
804-
$clientIps = $matches[3];
805-
} elseif (self::$trustedHeaders[self::HEADER_CLIENT_IP] && $this->headers->has(self::$trustedHeaders[self::HEADER_CLIENT_IP])) {
806-
$clientIps = array_map('trim', explode(',', $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_IP])));
807-
}
808+
$forwardedClientIps = $matches[3];
808809

809-
$clientIps[] = $ip; // Complete the IP chain with the IP the request actually came from
810-
$firstTrustedIp = null;
811-
812-
foreach ($clientIps as $key => $clientIp) {
813-
// Remove port (unfortunately, it does happen)
814-
if (preg_match('{((?:\d+\.){3}\d+)\:\d+}', $clientIp, $match)) {
815-
$clientIps[$key] = $clientIp = $match[1];
816-
}
810+
$forwardedClientIps = $this->normalizeAndFilterClientIps($forwardedClientIps, $ip);
811+
$clientIps = $forwardedClientIps;
812+
}
817813

818-
if (!filter_var($clientIp, FILTER_VALIDATE_IP)) {
819-
unset($clientIps[$key]);
814+
if ($hasTrustedClientIpHeader) {
815+
$xForwardedForClientIps = array_map('trim', explode(',', $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_IP])));
820816

821-
continue;
822-
}
817+
$xForwardedForClientIps = $this->normalizeAndFilterClientIps($xForwardedForClientIps, $ip);
818+
$clientIps = $xForwardedForClientIps;
819+
}
823820

824-
if (IpUtils::checkIp($clientIp, self::$trustedProxies)) {
825-
unset($clientIps[$key]);
821+
if ($hasTrustedForwardedHeader && $hasTrustedClientIpHeader && $forwardedClientIps !== $xForwardedForClientIps) {
822+
throw new ConflictingHeadersException('The request has both a trusted Forwarded header and a trusted Client IP header, conflicting with each other with regards to the originating IP addresses of the request. This is the result of a misconfiguration. You should either configure your proxy only to send one of these headers, or configure Symfony to distrust one of them.');
823+
}
826824

827-
// Fallback to this when the client IP falls into the range of trusted proxies
828-
if (null === $firstTrustedIp) {
829-
$firstTrustedIp = $clientIp;
830-
}
831-
}
825+
if (!$hasTrustedForwardedHeader && !$hasTrustedClientIpHeader) {
826+
return $this->normalizeAndFilterClientIps(array(), $ip);
832827
}
833828

834-
// Now the IP chain contains only untrusted proxies and the client IP
835-
return $clientIps ? array_reverse($clientIps) : array($firstTrustedIp);
829+
return $clientIps;
836830
}
837831

838832
/**
@@ -1947,4 +1941,35 @@ public function isFromTrustedProxy()
19471941
{
19481942
return self::$trustedProxies && IpUtils::checkIp($this->server->get('REMOTE_ADDR'), self::$trustedProxies);
19491943
}
1944+
1945+
private function normalizeAndFilterClientIps(array $clientIps, $ip)
1946+
{
1947+
$clientIps[] = $ip; // Complete the IP chain with the IP the request actually came from
1948+
$firstTrustedIp = null;
1949+
1950+
foreach ($clientIps as $key => $clientIp) {
1951+
// Remove port (unfortunately, it does happen)
1952+
if (preg_match('{((?:\d+\.){3}\d+)\:\d+}', $clientIp, $match)) {
1953+
$clientIps[$key] = $clientIp = $match[1];
1954+
}
1955+
1956+
if (!filter_var($clientIp, FILTER_VALIDATE_IP)) {
1957+
unset($clientIps[$key]);
1958+
1959+
continue;
1960+
}
1961+
1962+
if (IpUtils::checkIp($clientIp, self::$trustedProxies)) {
1963+
unset($clientIps[$key]);
1964+
1965+
// Fallback to this when the client IP falls into the range of trusted proxies
1966+
if (null === $firstTrustedIp) {
1967+
$firstTrustedIp = $clientIp;
1968+
}
1969+
}
1970+
}
1971+
1972+
// Now the IP chain contains only untrusted proxies and the client IP
1973+
return $clientIps ? array_reverse($clientIps) : array($firstTrustedIp);
1974+
}
19501975
}

src/Symfony/Component/HttpFoundation/Tests/RequestTest.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -940,6 +940,74 @@ public function testGetClientIpsProvider()
940940
);
941941
}
942942

943+
/**
944+
* @expectedException \Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException
945+
* @dataProvider testGetClientIpsWithConflictingHeadersProvider
946+
*/
947+
public function testGetClientIpsWithConflictingHeaders($httpForwarded, $httpXForwardedFor)
948+
{
949+
$request = new Request();
950+
951+
$server = array(
952+
'REMOTE_ADDR' => '88.88.88.88',
953+
'HTTP_FORWARDED' => $httpForwarded,
954+
'HTTP_X_FORWARDED_FOR' => $httpXForwardedFor,
955+
);
956+
957+
Request::setTrustedProxies(array('88.88.88.88'));
958+
959+
$request->initialize(array(), array(), array(), array(), array(), $server);
960+
961+
$request->getClientIps();
962+
}
963+
964+
public function testGetClientIpsWithConflictingHeadersProvider()
965+
{
966+
// $httpForwarded $httpXForwardedFor
967+
return array(
968+
array('for=87.65.43.21', '192.0.2.60'),
969+
array('for=87.65.43.21, for=192.0.2.60', '192.0.2.60'),
970+
array('for=192.0.2.60', '192.0.2.60,87.65.43.21'),
971+
array('for="::face", for=192.0.2.60', '192.0.2.60,192.0.2.43'),
972+
array('for=87.65.43.21, for=192.0.2.60', '192.0.2.60,87.65.43.21'),
973+
);
974+
}
975+
976+
/**
977+
* @dataProvider testGetClientIpsWithAgreeingHeadersProvider
978+
*/
979+
public function testGetClientIpsWithAgreeingHeaders($httpForwarded, $httpXForwardedFor)
980+
{
981+
$request = new Request();
982+
983+
$server = array(
984+
'REMOTE_ADDR' => '88.88.88.88',
985+
'HTTP_FORWARDED' => $httpForwarded,
986+
'HTTP_X_FORWARDED_FOR' => $httpXForwardedFor,
987+
);
988+
989+
Request::setTrustedProxies(array('88.88.88.88'));
990+
991+
$request->initialize(array(), array(), array(), array(), array(), $server);
992+
993+
$request->getClientIps();
994+
995+
Request::setTrustedProxies(array());
996+
}
997+
998+
public function testGetClientIpsWithAgreeingHeadersProvider()
999+
{
1000+
// $httpForwarded $httpXForwardedFor
1001+
return array(
1002+
array('for="192.0.2.60"', '192.0.2.60'),
1003+
array('for=192.0.2.60, for=87.65.43.21', '192.0.2.60,87.65.43.21'),
1004+
array('for="[::face]", for=192.0.2.60', '::face,192.0.2.60'),
1005+
array('for="192.0.2.60:80"', '192.0.2.60'),
1006+
array('for=192.0.2.60;proto=http;by=203.0.113.43', '192.0.2.60'),
1007+
array('for="[2001:db8:cafe::17]:4711"', '2001:db8:cafe::17'),
1008+
);
1009+
}
1010+
9431011
public function testGetContentWorksTwiceInDefaultMode()
9441012
{
9451013
$req = new Request();
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\EventListener;
13+
14+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
15+
use Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException;
16+
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
17+
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
18+
use Symfony\Component\HttpKernel\KernelEvents;
19+
20+
/**
21+
* Validates that the headers and other information indicating the
22+
* client IP address of a request are consistent.
23+
*
24+
* @author Magnus Nordlander <magnus@fervo.se>
25+
*/
26+
class ValidateRequestListener implements EventSubscriberInterface
27+
{
28+
/**
29+
* Performs the validation.
30+
*
31+
* @param GetResponseEvent $event
32+
*/
33+
public function onKernelRequest(GetResponseEvent $event)
34+
{
35+
if ($event->isMasterRequest()) {
36+
try {
37+
// This will throw an exception if the headers are inconsistent.
38+
$event->getRequest()->getClientIps();
39+
} catch (ConflictingHeadersException $e) {
40+
throw new BadRequestHttpException('The request headers contain conflicting information regarding the origin of this request.', $e);
41+
}
42+
}
43+
}
44+
45+
/**
46+
* {@inheritdoc}
47+
*/
48+
public static function getSubscribedEvents()
49+
{
50+
return array(
51+
KernelEvents::REQUEST => array(
52+
array('onKernelRequest', 256),
53+
),
54+
);
55+
}
56+
}

src/Symfony/Component/HttpKernel/Profiler/Profiler.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
namespace Symfony\Component\HttpKernel\Profiler;
1313

14+
use Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException;
1415
use Symfony\Component\HttpFoundation\Request;
1516
use Symfony\Component\HttpFoundation\Response;
1617
use Symfony\Component\HttpKernel\DataCollector\DataCollectorInterface;
@@ -169,9 +170,13 @@ public function collect(Request $request, Response $response, \Exception $except
169170
$profile = new Profile(substr(hash('sha256', uniqid(mt_rand(), true)), 0, 6));
170171
$profile->setTime(time());
171172
$profile->setUrl($request->getUri());
172-
$profile->setIp($request->getClientIp());
173173
$profile->setMethod($request->getMethod());
174174
$profile->setStatusCode($response->getStatusCode());
175+
try {
176+
$profile->setIp($request->getClientIp());
177+
} catch (ConflictingHeadersException $e) {
178+
$profile->setIp('Unknown');
179+
}
175180

176181
$response->headers->set('X-Debug-Token', $profile->getToken());
177182

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
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\Tests\EventListener;
13+
14+
use Symfony\Component\EventDispatcher\EventDispatcher;
15+
use Symfony\Component\HttpFoundation\Exception\ConflictingHeadersException;
16+
use Symfony\Component\HttpFoundation\Request;
17+
use Symfony\Component\HttpKernel\EventListener\ValidateRequestListener;
18+
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
19+
use Symfony\Component\HttpKernel\HttpKernelInterface;
20+
use Symfony\Component\HttpKernel\KernelEvents;
21+
22+
class ValidateRequestListenerTest extends \PHPUnit_Framework_TestCase
23+
{
24+
public function testListenerThrowsWhenMasterRequestHasInconsistentClientIps()
25+
{
26+
$dispatcher = new EventDispatcher();
27+
$kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
28+
$listener = new ValidateRequestListener();
29+
$request = $this->getMock('Symfony\Component\HttpFoundation\Request');
30+
$request->method('getClientIps')
31+
->will($this->throwException(new ConflictingHeadersException()));
32+
33+
$dispatcher->addListener(KernelEvents::REQUEST, array($listener, 'onKernelRequest'));
34+
$event = new GetResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST);
35+
36+
$this->setExpectedException('Symfony\Component\HttpKernel\Exception\BadRequestHttpException');
37+
$dispatcher->dispatch(KernelEvents::REQUEST, $event);
38+
}
39+
40+
public function testListenerDoesNothingOnValidRequests()
41+
{
42+
$dispatcher = new EventDispatcher();
43+
$kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
44+
$listener = new ValidateRequestListener();
45+
$request = $this->getMock('Symfony\Component\HttpFoundation\Request');
46+
$request->method('getClientIps')
47+
->willReturn(array('127.0.0.1'));
48+
49+
$dispatcher->addListener(KernelEvents::REQUEST, array($listener, 'onKernelRequest'));
50+
$event = new GetResponseEvent($kernel, $request, HttpKernelInterface::MASTER_REQUEST);
51+
$dispatcher->dispatch(KernelEvents::REQUEST, $event);
52+
}
53+
54+
public function testListenerDoesNothingOnSubrequests()
55+
{
56+
$dispatcher = new EventDispatcher();
57+
$kernel = $this->getMock('Symfony\Component\HttpKernel\HttpKernelInterface');
58+
$listener = new ValidateRequestListener();
59+
$request = $this->getMock('Symfony\Component\HttpFoundation\Request');
60+
$request->method('getClientIps')
61+
->will($this->throwException(new ConflictingHeadersException()));
62+
63+
$dispatcher->addListener(KernelEvents::REQUEST, array($listener, 'onKernelRequest'));
64+
$event = new GetResponseEvent($kernel, $request, HttpKernelInterface::SUB_REQUEST);
65+
$dispatcher->dispatch(KernelEvents::REQUEST, $event);
66+
}
67+
}

0 commit comments

Comments
 (0)