diff --git a/config-templates/config.php b/config-templates/config.php index ed0b8f003601222cc70fb2fc25be8303ba37ef99..b11a67825ce5616c7d949c60248ad1602cc28a4f 100644 --- a/config-templates/config.php +++ b/config-templates/config.php @@ -104,6 +104,17 @@ $config = array( */ 'trusted.url.domains' => array(), + /* + * Enable regular expression matching of trusted.url.domains. + * + * Set to true to treat the values in trusted.url.domains as regular + * expressions. Set to false to do exact string matching. + * + * If enabled, the start and end delimiters ('^' and '$') will be added to + * all regular expressions in trusted.url.domains. + */ + 'trusted.url.regex' => false, + /* * Enable secure POST from HTTPS to HTTP. * diff --git a/docs/simplesamlphp-changelog.txt b/docs/simplesamlphp-changelog.txt index be38758d3b7164f6c173a7be886b8addcb02e9a0..979e9c878f90fcc89516ddbd131a1b4876197b89 100644 --- a/docs/simplesamlphp-changelog.txt +++ b/docs/simplesamlphp-changelog.txt @@ -32,6 +32,7 @@ Released TBD * Added the SAML NameID to the attributes status page, when available. * Added attribute definitions for schacGender (schac), sisSchoolGrade and sisLegalGuardianFor (skolfederation.se). * Attributes required in metadata are now taken into account when parsing. + * Allow regular expression matching of trusted.url.domains. Off by default, set trusted.url.regex to true to enable. ### Bug fixes diff --git a/lib/SimpleSAML/Utils/HTTP.php b/lib/SimpleSAML/Utils/HTTP.php index 5ebf7765ef4ac1035cd35b00390eda8da9e64b9e..b4613f1af6d2f90a8d93cd912cf279964dbbd750 100644 --- a/lib/SimpleSAML/Utils/HTTP.php +++ b/lib/SimpleSAML/Utils/HTTP.php @@ -324,12 +324,30 @@ class HTTP preg_match('@^https?://([^/]+)@i', $url, $matches); $hostname = $matches[1]; - // add self host to the white list $self_host = self::getSelfHostWithNonStandardPort(); - $trustedSites[] = $self_host; + + $trustedRegex = \SimpleSAML_Configuration::getInstance()->getValue('trusted.url.regex', false); + + $trusted = false; + if ($trustedRegex) { + // add self host to the white list + $trustedSites[] = preg_quote($self_host); + foreach ($trustedSites as $regex) { + // Add start and end delimiters. + $regex = "@^{$regex}$@"; + if (preg_match($regex, $hostname)) { + $trusted = true; + break; + } + } + } else { + // add self host to the white list + $trustedSites[] = $self_host; + $trusted = in_array($hostname, $trustedSites); + } // throw exception due to redirection to untrusted site - if (!in_array($hostname, $trustedSites)) { + if (!$trusted) { throw new \SimpleSAML_Error_Exception('URL not allowed: '.$url); } } diff --git a/tests/lib/SimpleSAML/Utils/HTTPTest.php b/tests/lib/SimpleSAML/Utils/HTTPTest.php index 417c890f40ee598beae045e03624af8251387d3d..e17ac9ba7fa2f28762068a3bc1032ec7be2c1d52 100644 --- a/tests/lib/SimpleSAML/Utils/HTTPTest.php +++ b/tests/lib/SimpleSAML/Utils/HTTPTest.php @@ -88,4 +88,77 @@ class HTTPTest extends \PHPUnit_Framework_TestCase $_SERVER['SERVER_PORT'] = '443'; $this->assertEquals('localhost', HTTP::getSelfHostWithNonStandardPort()); } + + /** + * Test SimpleSAML\Utils\HTTP::checkURLAllowed(), without regex. + */ + public function testCheckURLAllowedWithoutRegex() + { + \SimpleSAML_Configuration::loadFromArray(array( + 'trusted.url.domains' => array('sp.example.com', 'app.example.com'), + 'trusted.url.regex' => false, + ), '[ARRAY]', 'simplesaml'); + + $_SERVER['REQUEST_URI'] = '/module.php'; + + $allowed = array( + 'https://sp.example.com/', + 'http://sp.example.com/', + 'https://app.example.com/', + 'http://app.example.com/', + ); + foreach ($allowed as $url) + { + $this->assertEquals(HTTP::checkURLAllowed($url), $url); + } + + $this->setExpectedException('SimpleSAML_Error_Exception'); + HTTP::checkURLAllowed('https://evil.com'); + } + + /** + * Test SimpleSAML\Utils\HTTP::checkURLAllowed(), with regex. + */ + public function testCheckURLAllowedWithRegex() + { + \SimpleSAML_Configuration::loadFromArray(array( + 'trusted.url.domains' => array('.*\.example\.com'), + 'trusted.url.regex' => true, + ), '[ARRAY]', 'simplesaml'); + + $_SERVER['REQUEST_URI'] = '/module.php'; + + $allowed = array( + 'https://sp.example.com/', + 'http://sp.example.com/', + 'https://app1.example.com/', + 'http://app1.example.com/', + 'https://app2.example.com/', + 'http://app2.example.com/', + ); + foreach ($allowed as $url) + { + $this->assertEquals(HTTP::checkURLAllowed($url), $url); + } + + $this->setExpectedException('SimpleSAML_Error_Exception'); + HTTP::checkURLAllowed('https://evil.com'); + } + + /** + * Test SimpleSAML\Utils\HTTP::checkURLAllowed(), with the regex as a + * subdomain of an evil domain. + */ + public function testCheckURLAllowedWithRegexWithoutDelimiters() + { + \SimpleSAML_Configuration::loadFromArray(array( + 'trusted.url.domains' => array('app\.example\.com'), + 'trusted.url.regex' => true, + ), '[ARRAY]', 'simplesaml'); + + $_SERVER['REQUEST_URI'] = '/module.php'; + + $this->setExpectedException('SimpleSAML_Error_Exception'); + HTTP::checkURLAllowed('https://app.example.com.evil.com'); + } }