Merge branch '2.8' into 3.2

* 2.8:
  [HttpFoundation] Fix missing handling of for/host/proto info from "Forwarded" header
  [Validator] Add object handling of invalid constraints in Composite
  [WebProfilerBundle] Remove uneeded directive in the form collector styles
  Revert "bug #21841 [Console] Do not squash input changes made from console.command event (chalasr)"
  [HttpFoundation] Fix Request::getHost() when having several hosts in X_FORWARDED_HOST
This commit is contained in:
Fabien Potencier 2017-03-22 13:31:03 -07:00
commit 04fcac74b1
10 changed files with 143 additions and 107 deletions

View File

@ -60,7 +60,6 @@
}
#tree-menu .empty {
border: 0;
margin: 0;
padding: 0;
}
#tree-details-container {

View File

@ -839,10 +839,6 @@ class Application
// ignore invalid options/arguments for now, to allow the event listeners to customize the InputDefinition
}
// don't bind the input again as it would override any input argument/option set from the command event in
// addition to being useless
$command->setInputBound(true);
$event = new ConsoleCommandEvent($command, $input, $output);
$this->dispatcher->dispatch(ConsoleEvents::COMMAND, $event);

View File

@ -40,7 +40,6 @@ class Command
private $ignoreValidationErrors = false;
private $applicationDefinitionMerged = false;
private $applicationDefinitionMergedWithArgs = false;
private $inputBound = false;
private $code;
private $synopsis = array();
private $usages = array();
@ -217,13 +216,11 @@ class Command
$this->mergeApplicationDefinition();
// bind the input against the command specific arguments/options
if (!$this->inputBound) {
try {
$input->bind($this->definition);
} catch (ExceptionInterface $e) {
if (!$this->ignoreValidationErrors) {
throw $e;
}
try {
$input->bind($this->definition);
} catch (ExceptionInterface $e) {
if (!$this->ignoreValidationErrors) {
throw $e;
}
}
@ -652,14 +649,6 @@ class Command
return $this->helperSet->get($name);
}
/**
* @internal
*/
public function setInputBound($inputBound)
{
$this->inputBound = $inputBound;
}
/**
* Validates a command name.
*

View File

@ -1106,31 +1106,6 @@ class ApplicationTest extends TestCase
$this->assertEquals('some test value', $extraValue);
}
public function testUpdateInputFromConsoleCommandEvent()
{
$dispatcher = $this->getDispatcher();
$dispatcher->addListener('console.command', function (ConsoleCommandEvent $event) {
$event->getInput()->setOption('extra', 'overriden');
});
$application = new Application();
$application->setDispatcher($dispatcher);
$application->setAutoExit(false);
$application
->register('foo')
->addOption('extra', null, InputOption::VALUE_REQUIRED)
->setCode(function (InputInterface $input, OutputInterface $output) {
$output->write('foo.');
})
;
$tester = new ApplicationTester($application);
$tester->run(array('command' => 'foo', '--extra' => 'original'));
$this->assertEquals('overriden', $tester->getInput()->getOption('extra'));
}
/**
* @group legacy
*/

View File

@ -206,6 +206,15 @@ class Request
protected static $requestFactory;
private $isForwardedValid = true;
private static $forwardedParams = array(
self::HEADER_CLIENT_IP => 'for',
self::HEADER_CLIENT_HOST => 'host',
self::HEADER_CLIENT_PROTO => 'proto',
self::HEADER_CLIENT_PORT => 'host',
);
/**
* Constructor.
*
@ -793,41 +802,13 @@ class Request
*/
public function getClientIps()
{
$clientIps = array();
$ip = $this->server->get('REMOTE_ADDR');
if (!$this->isFromTrustedProxy()) {
return array($ip);
}
$hasTrustedForwardedHeader = self::$trustedHeaders[self::HEADER_FORWARDED] && $this->headers->has(self::$trustedHeaders[self::HEADER_FORWARDED]);
$hasTrustedClientIpHeader = self::$trustedHeaders[self::HEADER_CLIENT_IP] && $this->headers->has(self::$trustedHeaders[self::HEADER_CLIENT_IP]);
if ($hasTrustedForwardedHeader) {
$forwardedHeader = $this->headers->get(self::$trustedHeaders[self::HEADER_FORWARDED]);
preg_match_all('{(for)=("?\[?)([a-z0-9\.:_\-/]*)}', $forwardedHeader, $matches);
$forwardedClientIps = $matches[3];
$forwardedClientIps = $this->normalizeAndFilterClientIps($forwardedClientIps, $ip);
$clientIps = $forwardedClientIps;
}
if ($hasTrustedClientIpHeader) {
$xForwardedForClientIps = array_map('trim', explode(',', $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_IP])));
$xForwardedForClientIps = $this->normalizeAndFilterClientIps($xForwardedForClientIps, $ip);
$clientIps = $xForwardedForClientIps;
}
if ($hasTrustedForwardedHeader && $hasTrustedClientIpHeader && $forwardedClientIps !== $xForwardedForClientIps) {
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.');
}
if (!$hasTrustedForwardedHeader && !$hasTrustedClientIpHeader) {
return $this->normalizeAndFilterClientIps(array(), $ip);
}
return $clientIps;
return $this->getTrustedValues(self::HEADER_CLIENT_IP, $ip) ?: array($ip);
}
/**
@ -953,31 +934,25 @@ class Request
*/
public function getPort()
{
if ($this->isFromTrustedProxy()) {
if (self::$trustedHeaders[self::HEADER_CLIENT_PORT] && $port = $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_PORT])) {
return $port;
}
if (self::$trustedHeaders[self::HEADER_CLIENT_PROTO] && 'https' === $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_PROTO], 'http')) {
return 443;
}
if ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_CLIENT_PORT)) {
$host = $host[0];
} elseif ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_CLIENT_HOST)) {
$host = $host[0];
} elseif (!$host = $this->headers->get('HOST')) {
return $this->server->get('SERVER_PORT');
}
if ($host = $this->headers->get('HOST')) {
if ($host[0] === '[') {
$pos = strpos($host, ':', strrpos($host, ']'));
} else {
$pos = strrpos($host, ':');
}
if (false !== $pos) {
return (int) substr($host, $pos + 1);
}
return 'https' === $this->getScheme() ? 443 : 80;
if ($host[0] === '[') {
$pos = strpos($host, ':', strrpos($host, ']'));
} else {
$pos = strrpos($host, ':');
}
return $this->server->get('SERVER_PORT');
if (false !== $pos) {
return (int) substr($host, $pos + 1);
}
return 'https' === $this->getScheme() ? 443 : 80;
}
/**
@ -1177,8 +1152,8 @@ class Request
*/
public function isSecure()
{
if ($this->isFromTrustedProxy() && self::$trustedHeaders[self::HEADER_CLIENT_PROTO] && $proto = $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_PROTO])) {
return in_array(strtolower(current(explode(',', $proto))), array('https', 'on', 'ssl', '1'));
if ($this->isFromTrustedProxy() && $proto = $this->getTrustedValues(self::HEADER_CLIENT_PROTO)) {
return in_array(strtolower($proto[0]), array('https', 'on', 'ssl', '1'), true);
}
$https = $this->server->get('HTTPS');
@ -1203,10 +1178,8 @@ class Request
*/
public function getHost()
{
if ($this->isFromTrustedProxy() && self::$trustedHeaders[self::HEADER_CLIENT_HOST] && $host = $this->headers->get(self::$trustedHeaders[self::HEADER_CLIENT_HOST])) {
$elements = explode(',', $host);
$host = $elements[count($elements) - 1];
if ($this->isFromTrustedProxy() && $host = $this->getTrustedValues(self::HEADER_CLIENT_HOST)) {
$host = $host[0];
} elseif (!$host = $this->headers->get('HOST')) {
if (!$host = $this->server->get('SERVER_NAME')) {
$host = $this->server->get('SERVER_ADDR', '');
@ -1977,8 +1950,48 @@ class Request
return self::$trustedProxies && IpUtils::checkIp($this->server->get('REMOTE_ADDR'), self::$trustedProxies);
}
private function getTrustedValues($type, $ip = null)
{
$clientValues = array();
$forwardedValues = array();
if (self::$trustedHeaders[$type] && $this->headers->has(self::$trustedHeaders[$type])) {
foreach (explode(',', $this->headers->get(self::$trustedHeaders[$type])) as $v) {
$clientValues[] = (self::HEADER_CLIENT_PORT === $type ? '0.0.0.0:' : '').trim($v);
}
}
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 (null !== $ip) {
$clientValues = $this->normalizeAndFilterClientIps($clientValues, $ip);
$forwardedValues = $this->normalizeAndFilterClientIps($forwardedValues, $ip);
}
if ($forwardedValues === $clientValues || !$clientValues) {
return $forwardedValues;
}
if (!$forwardedValues) {
return $clientValues;
}
if (!$this->isForwardedValid) {
return null !== $ip ? array('0.0.0.0', $ip) : array();
}
$this->isForwardedValid = false;
throw new ConflictingHeadersException(sprintf('The request has both a trusted "%s" header and a trusted "%s" header, conflicting with each other. You should either configure your proxy to remove one of them, or configure your project to distrust the offending one.', self::$trustedHeaders[self::HEADER_FORWARDED], self::$trustedHeaders[$type]));
}
private function normalizeAndFilterClientIps(array $clientIps, $ip)
{
if (!$clientIps) {
return array();
}
$clientIps[] = $ip; // Complete the IP chain with the IP the request actually came from
$firstTrustedIp = null;

View File

@ -1670,12 +1670,12 @@ class RequestTest extends TestCase
return $request;
}
public function testTrustedProxies()
public function testTrustedProxiesXForwardedFor()
{
$request = Request::create('http://example.com/');
$request->server->set('REMOTE_ADDR', '3.3.3.3');
$request->headers->set('X_FORWARDED_FOR', '1.1.1.1, 2.2.2.2');
$request->headers->set('X_FORWARDED_HOST', 'foo.example.com, real.example.com:8080');
$request->headers->set('X_FORWARDED_HOST', 'foo.example.com:1234, real.example.com:8080');
$request->headers->set('X_FORWARDED_PROTO', 'https');
$request->headers->set('X_FORWARDED_PORT', 443);
$request->headers->set('X_MY_FOR', '3.3.3.3, 4.4.4.4');
@ -1706,7 +1706,7 @@ class RequestTest extends TestCase
// trusted proxy via setTrustedProxies()
Request::setTrustedProxies(array('3.3.3.3', '2.2.2.2'));
$this->assertEquals('1.1.1.1', $request->getClientIp());
$this->assertEquals('real.example.com', $request->getHost());
$this->assertEquals('foo.example.com', $request->getHost());
$this->assertEquals(443, $request->getPort());
$this->assertTrue($request->isSecure());
@ -1753,6 +1753,55 @@ class RequestTest extends TestCase
Request::setTrustedHeaderName(Request::HEADER_CLIENT_PROTO, 'X_FORWARDED_PROTO');
}
public function testTrustedProxiesForwarded()
{
$request = Request::create('http://example.com/');
$request->server->set('REMOTE_ADDR', '3.3.3.3');
$request->headers->set('FORWARDED', 'for=1.1.1.1, host=foo.example.com:8080, proto=https, for=2.2.2.2, host=real.example.com:8080');
// no trusted proxies
$this->assertEquals('3.3.3.3', $request->getClientIp());
$this->assertEquals('example.com', $request->getHost());
$this->assertEquals(80, $request->getPort());
$this->assertFalse($request->isSecure());
// disabling proxy trusting
Request::setTrustedProxies(array());
$this->assertEquals('3.3.3.3', $request->getClientIp());
$this->assertEquals('example.com', $request->getHost());
$this->assertEquals(80, $request->getPort());
$this->assertFalse($request->isSecure());
// request is forwarded by a non-trusted proxy
Request::setTrustedProxies(array('2.2.2.2'));
$this->assertEquals('3.3.3.3', $request->getClientIp());
$this->assertEquals('example.com', $request->getHost());
$this->assertEquals(80, $request->getPort());
$this->assertFalse($request->isSecure());
// trusted proxy via setTrustedProxies()
Request::setTrustedProxies(array('3.3.3.3', '2.2.2.2'));
$this->assertEquals('1.1.1.1', $request->getClientIp());
$this->assertEquals('foo.example.com', $request->getHost());
$this->assertEquals(8080, $request->getPort());
$this->assertTrue($request->isSecure());
// trusted proxy via setTrustedProxies()
Request::setTrustedProxies(array('3.3.3.4', '2.2.2.2'));
$this->assertEquals('3.3.3.3', $request->getClientIp());
$this->assertEquals('example.com', $request->getHost());
$this->assertEquals(80, $request->getPort());
$this->assertFalse($request->isSecure());
// check various X_FORWARDED_PROTO header values
Request::setTrustedProxies(array('3.3.3.3', '2.2.2.2'));
$request->headers->set('FORWARDED', 'proto=ssl');
$this->assertTrue($request->isSecure());
$request->headers->set('FORWARDED', 'proto=https, proto=http');
$this->assertTrue($request->isSecure());
}
/**
* @expectedException \InvalidArgumentException
*/

View File

@ -32,7 +32,7 @@ class ValidateRequestListenerTest extends TestCase
$request = new Request();
$request->setTrustedProxies(array('1.1.1.1'));
$request->server->set('REMOTE_ADDR', '1.1.1.1');
$request->headers->set('FORWARDED', '2.2.2.2');
$request->headers->set('FORWARDED', 'for=2.2.2.2');
$request->headers->set('X_FORWARDED_FOR', '3.3.3.3');
$dispatcher->addListener(KernelEvents::REQUEST, array(new ValidateRequestListener(), 'onKernelRequest'));

View File

@ -311,7 +311,7 @@ class HttpKernelTest extends TestCase
$request = new Request();
$request->setTrustedProxies(array('1.1.1.1'));
$request->server->set('REMOTE_ADDR', '1.1.1.1');
$request->headers->set('FORWARDED', '2.2.2.2');
$request->headers->set('FORWARDED', 'for=2.2.2.2');
$request->headers->set('X_FORWARDED_FOR', '3.3.3.3');
$dispatcher = new EventDispatcher();

View File

@ -67,6 +67,10 @@ abstract class Composite extends Constraint
foreach ($nestedConstraints as $constraint) {
if (!$constraint instanceof Constraint) {
if (is_object($constraint)) {
$constraint = get_class($constraint);
}
throw new ConstraintDefinitionException(sprintf('The value %s is not an instance of Constraint in constraint %s', $constraint, get_class($this)));
}

View File

@ -125,6 +125,17 @@ class CompositeTest extends TestCase
));
}
/**
* @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException
*/
public function testFailIfNoConstraintObject()
{
new ConcreteComposite(array(
new NotNull(array('groups' => 'Default')),
new \ArrayObject(),
));
}
/**
* @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException
*/