From c35cc5b900835827b7ee6b186775eca52f129172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Fran=C3=A7ois=20Simon?= Date: Tue, 6 Aug 2013 09:14:49 +0200 Subject: [PATCH] added trusted hosts check --- .../DependencyInjection/Configuration.php | 7 +++ .../FrameworkExtension.php | 1 + .../FrameworkBundle/FrameworkBundle.php | 4 ++ .../DependencyInjection/ConfigurationTest.php | 14 ++--- .../Component/HttpFoundation/Request.php | 54 +++++++++++++++++++ .../Component/HttpFoundation/RequestTest.php | 31 +++++++++++ 6 files changed, 104 insertions(+), 7 deletions(-) diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 2d1d145080..db65bb1080 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -63,6 +63,13 @@ class Configuration implements ConfigurationInterface ->scalarNode('secret')->isRequired()->end() ->scalarNode('ide')->defaultNull()->end() ->booleanNode('test')->end() + ->arrayNode('trusted_hosts') + ->beforeNormalization() + ->ifTrue(function($v) { return is_string($v); }) + ->then(function($v) { return array($v); }) + ->end() + ->prototype('scalar')->end() + ->end() ->end() ; diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 0a74063123..0cfb0eda7f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -63,6 +63,7 @@ class FrameworkExtension extends Extension $container->setParameter('kernel.secret', $config['secret']); $container->setParameter('kernel.trusted_proxies', $config['trusted_proxies']); + $container->setParameter('kernel.trusted_hosts', $config['trusted_hosts']); // @deprecated, to be removed in 2.3 $container->setParameter('kernel.trust_proxy_headers', $config['trust_proxy_headers']); diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index 06284b96a5..b9ee493e12 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -42,6 +42,10 @@ class FrameworkBundle extends Bundle } elseif ($this->container->getParameter('kernel.trust_proxy_headers')) { Request::trustProxyData(); // @deprecated, to be removed in 2.3 } + + if ($trustedHosts = $this->container->getParameter('kernel.trusted_hosts')) { + Request::setTrustedHosts($trustedHosts); + } } public function build(ContainerBuilder $container) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 949dc18d61..45fc89fc7d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -31,7 +31,7 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase public function getTestConfigTreeData() { return array( - array(array('secret' => 's3cr3t'), array('secret' => 's3cr3t', 'trusted_proxies' => array(), 'trust_proxy_headers' => false, 'ide' => NULL, 'annotations' => array('cache' => 'file', 'file_cache_dir' => '%kernel.cache_dir%/annotations', 'debug' => false))), + array(array('secret' => 's3cr3t'), array('secret' => 's3cr3t', 'trusted_proxies' => array(), 'trusted_hosts' => array(), 'trust_proxy_headers' => false, 'ide' => NULL, 'annotations' => array('cache' => 'file', 'file_cache_dir' => '%kernel.cache_dir%/annotations', 'debug' => false))), ); } @@ -50,12 +50,12 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase public function getTestValidTrustedProxiesData() { return array( - array(array('secret' => 's3cr3t', 'trusted_proxies' => array('127.0.0.1')), array('secret' => 's3cr3t', 'trusted_proxies' => array('127.0.0.1'), 'trust_proxy_headers' => false, 'ide' => NULL, 'annotations' => array('cache' => 'file', 'file_cache_dir' => '%kernel.cache_dir%/annotations', 'debug' => false))), - array(array('secret' => 's3cr3t', 'trusted_proxies' => array('::1')), array('secret' => 's3cr3t', 'trusted_proxies' => array('::1'), 'trust_proxy_headers' => false, 'ide' => NULL, 'annotations' => array('cache' => 'file', 'file_cache_dir' => '%kernel.cache_dir%/annotations', 'debug' => false))), - array(array('secret' => 's3cr3t', 'trusted_proxies' => array('127.0.0.1', '::1')), array('secret' => 's3cr3t', 'trusted_proxies' => array('127.0.0.1', '::1'), 'trust_proxy_headers' => false, 'ide' => NULL, 'annotations' => array('cache' => 'file', 'file_cache_dir' => '%kernel.cache_dir%/annotations', 'debug' => false))), - array(array('secret' => 's3cr3t', 'trusted_proxies' => null), array('secret' => 's3cr3t', 'trusted_proxies' => array(), 'trust_proxy_headers' => false, 'ide' => NULL, 'annotations' => array('cache' => 'file', 'file_cache_dir' => '%kernel.cache_dir%/annotations', 'debug' => false))), - array(array('secret' => 's3cr3t', 'trusted_proxies' => false), array('secret' => 's3cr3t', 'trusted_proxies' => array(), 'trust_proxy_headers' => false, 'ide' => NULL, 'annotations' => array('cache' => 'file', 'file_cache_dir' => '%kernel.cache_dir%/annotations', 'debug' => false))), - array(array('secret' => 's3cr3t', 'trusted_proxies' => array()), array('secret' => 's3cr3t', 'trusted_proxies' => array(), 'trust_proxy_headers' => false, 'ide' => NULL, 'annotations' => array('cache' => 'file', 'file_cache_dir' => '%kernel.cache_dir%/annotations', 'debug' => false))), + array(array('secret' => 's3cr3t', 'trusted_proxies' => array('127.0.0.1')), array('secret' => 's3cr3t', 'trusted_hosts' => array(), 'trusted_proxies' => array('127.0.0.1'), 'trust_proxy_headers' => false, 'ide' => NULL, 'annotations' => array('cache' => 'file', 'file_cache_dir' => '%kernel.cache_dir%/annotations', 'debug' => false))), + array(array('secret' => 's3cr3t', 'trusted_proxies' => array('::1')), array('secret' => 's3cr3t', 'trusted_hosts' => array(), 'trusted_proxies' => array('::1'), 'trust_proxy_headers' => false, 'ide' => NULL, 'annotations' => array('cache' => 'file', 'file_cache_dir' => '%kernel.cache_dir%/annotations', 'debug' => false))), + array(array('secret' => 's3cr3t', 'trusted_proxies' => array('127.0.0.1', '::1')), array('secret' => 's3cr3t', 'trusted_hosts' => array(), 'trusted_proxies' => array('127.0.0.1', '::1'), 'trust_proxy_headers' => false, 'ide' => NULL, 'annotations' => array('cache' => 'file', 'file_cache_dir' => '%kernel.cache_dir%/annotations', 'debug' => false))), + array(array('secret' => 's3cr3t', 'trusted_proxies' => null), array('secret' => 's3cr3t', 'trusted_hosts' => array(), 'trusted_proxies' => array(), 'trust_proxy_headers' => false, 'ide' => NULL, 'annotations' => array('cache' => 'file', 'file_cache_dir' => '%kernel.cache_dir%/annotations', 'debug' => false))), + array(array('secret' => 's3cr3t', 'trusted_proxies' => false), array('secret' => 's3cr3t', 'trusted_hosts' => array(), 'trusted_proxies' => array(), 'trust_proxy_headers' => false, 'ide' => NULL, 'annotations' => array('cache' => 'file', 'file_cache_dir' => '%kernel.cache_dir%/annotations', 'debug' => false))), + array(array('secret' => 's3cr3t', 'trusted_proxies' => array()), array('secret' => 's3cr3t', 'trusted_hosts' => array(), 'trusted_proxies' => array(), 'trust_proxy_headers' => false, 'ide' => NULL, 'annotations' => array('cache' => 'file', 'file_cache_dir' => '%kernel.cache_dir%/annotations', 'debug' => false))), ); } diff --git a/src/Symfony/Component/HttpFoundation/Request.php b/src/Symfony/Component/HttpFoundation/Request.php index b033a4f2fa..4852b183cd 100644 --- a/src/Symfony/Component/HttpFoundation/Request.php +++ b/src/Symfony/Component/HttpFoundation/Request.php @@ -29,6 +29,16 @@ class Request protected static $trustedProxies = array(); + /** + * @var string[] + */ + protected static $trustedHostPatterns = array(); + + /** + * @var string[] + */ + protected static $trustedHosts = array(); + /** * Names for headers that can be trusted when * using trusted proxies. @@ -400,6 +410,32 @@ class Request self::$trustProxy = $proxies ? true : false; } + /** + * Sets a list of trusted host patterns. + * + * You should only list the hosts you manage using regexs. + * + * @param array $hostPatterns A list of trusted host patterns + */ + public static function setTrustedHosts(array $hostPatterns) + { + self::$trustedHostPatterns = array_map(function ($hostPattern) { + return sprintf('{%s}i', str_replace('}', '\\}', $hostPattern)); + }, $hostPatterns); + // we need to reset trusted hosts on trusted host patterns change + self::$trustedHosts = array(); + } + + /** + * Gets the list of trusted host patterns. + * + * @return array An array of trusted host patterns. + */ + public static function getTrustedHosts() + { + return self::$trustedHostPatterns; + } + /** * Sets the name for trusted headers. * @@ -821,6 +857,24 @@ class Request throw new \UnexpectedValueException('Invalid Host'); } + if (count(self::$trustedHostPatterns) > 0) { + // to avoid host header injection attacks, you should provide a list of trusted host patterns + + if (in_array($host, self::$trustedHosts)) { + return $host; + } + + foreach (self::$trustedHostPatterns as $pattern) { + if (preg_match($pattern, $host)) { + self::$trustedHosts[] = $host; + + return $host; + } + } + + throw new \UnexpectedValueException('Untrusted Host'); + } + return $host; } diff --git a/tests/Symfony/Tests/Component/HttpFoundation/RequestTest.php b/tests/Symfony/Tests/Component/HttpFoundation/RequestTest.php index a1e119010e..528b52420c 100644 --- a/tests/Symfony/Tests/Component/HttpFoundation/RequestTest.php +++ b/tests/Symfony/Tests/Component/HttpFoundation/RequestTest.php @@ -917,6 +917,37 @@ class RequestTest extends \PHPUnit_Framework_TestCase // reset Request::setTrustedProxies(array()); } + + public function testTrustedHosts() + { + // create a request + $request = Request::create('/'); + + // no trusted host set -> no host check + $request->headers->set('host', 'evil.com'); + $this->assertEquals('evil.com', $request->getHost()); + + // add a trusted domain and all its subdomains + Request::setTrustedHosts(array('.*\.?trusted.com$')); + + // untrusted host + $request->headers->set('host', 'evil.com'); + try { + $request->getHost(); + $this->fail('Request::getHost() should throw an exception when host is not trusted.'); + } catch (\UnexpectedValueException $e) { + $this->assertEquals('Untrusted Host', $e->getMessage()); + } + + // trusted hosts + $request->headers->set('host', 'trusted.com'); + $this->assertEquals('trusted.com', $request->getHost()); + $request->headers->set('host', 'subdomain.trusted.com'); + $this->assertEquals('subdomain.trusted.com', $request->getHost()); + + // reset request for following tests + Request::setTrustedHosts(array()); + } } class RequestContentProxy extends Request