<?php /** * Tests for XSS * * @link http://www.egroupware.org * @author Nathan Gray * @package api * @copyright (c) 2017 Nathan Gray * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License */ namespace EGroupware\Api; require_once realpath(__DIR__.'/../../src/loader/common.php'); // autoloader & check_load_extension // // We're testing security.php require_once realpath(__DIR__.'/../../src/loader/security.php'); use PHPUnit\Framework\TestCase as TestCase; class SecurityTest extends TestCase { protected function setUp() : void { // _check_script_tag uses HtmLawed, which calls GLOBALS['egw']->link() $GLOBALS['egw'] = $this->getMockBuilder('Egw') ->disableOriginalConstructor() ->setMethods(['link', 'setup']) ->getMock(); } protected function tearDown() : void { unset($GLOBALS['egw_inset_vars']); // Must remember to clear this, or other tests may break unset($GLOBALS['egw']); } /** * Test some strings for bad stuff * * @param String $pattern String to check * @param boolean $should_fail If we expect this string to fail * * @dataProvider patternProvider */ public function testPatterns($pattern, $should_fail) { $test = array($pattern); unset($GLOBALS['egw_unset_vars']); _check_script_tag($test,'test', false); $this->assertEquals(isset($GLOBALS['egw_unset_vars']), $should_fail); } public function patternProvider() { return array( // pattern, true: should fail, false: should not fail Array('< script >alert(1)< / script >', true), Array('<span onMouseOver ="alert(1)">blah</span>', true), Array('<a href= "JaVascript: alert(1)">Click Me</a>', true), // from https://www.acunetix.com/websitesecurity/cross-site-scripting/ Array('<body onload=alert("XSS")>', true), Array('<body background="javascript:alert("XSS")">', true), Array('<iframe src=”http://evil.com/xss.html”>', true), Array('<input type="image" src="javascript:alert(\'XSS\');">', true), Array('<link rel="stylesheet" href="javascript:alert(\'XSS\');">', true), Array('<table background="javascript:alert(\'XSS\')">', true), Array('<td background="javascript:alert(\'XSS\')">', true), Array('<div style="background-image: url(javascript:alert(\'XSS\'))">', true), Array('<div style="width: expression(alert(\'XSS\'));">', true), Array('<object type="text/x-scriptlet" data="http://hacker.com/xss.html">', true), // false positiv tests Array('If 1 < 2, what does that mean for description, if 2 > 1.', false), Array('If 1 < 2, what does that mean for a script, if 2 > 1.', false), Array('<div>Script and Javascript: not evil ;-)', false), Array('<span>style=background-color', false), Array('<font face="Script MT Bold" size="4"><span style="font-size:16pt;">Hugo Sonstwas</span></font>', false), Array('<mathias@stylite.de>', false) ); } /** * Test some URLs with bad stuff * * @param String $url * @param Array $vectors * * @dataProvider urlProvider */ public function testURLs($url, $vectors = FALSE) { // no all xss attack vectors from http://ha.ckers.org/xssAttacks.xml are relevant here! (needs interpretation) if (!$vectors) { $this->markTestSkipped("Could not download or parse $url with attack vectors"); return; } foreach($vectors as $line => $pattern) { $test = array($pattern); _check_script_tag($test, 'line '.(1+$line), false); $this->assertTrue(isset($GLOBALS['egw_unset_vars']), $line . ': ' . $pattern); } } public function urlProvider() { $urls = array( // we currently fail 76 of 666 test, thought they seem not to apply to our use case, as we check request data 'https://gist.github.com/JohannesHoppe/5612274' => file( 'https://gist.githubusercontent.com/JohannesHoppe/5612274/raw/60016bccbfe894dcd61a6be658a4469e403527de/666_lines_of_XSS_vectors.html'), // we currently fail 44 of 140 tests, thought they seem not to apply to our use case, as we check request data 'https://html5sec.org/' => call_user_func(function() { $payloads = $items = null; try { if (!($items_js = file_get_contents('https://html5sec.org/items.js')) || !preg_match_all("|^\s+'data'\s+:\s+'(.*)',$|m", $items_js, $items, PREG_PATTERN_ORDER) || !($payload_js = file_get_contents('https://html5sec.org/payloads.js')) || !preg_match_all("|^\s+'([^']+)'\s+:\s+'(.*)',$|m", $payload_js, $payloads, PREG_PATTERN_ORDER)) { return false; } } catch (Exception $e) { unset($e); return false; } $replace = array( "\\'" => "'", '\\\\'=> '\\,', '\r' => "\r", '\n' => "\n", ); foreach($payloads[1] as $n => $from) { $replace['%'.$from.'%'] = $payloads[2][$n]; } return array_map(function($item) use ($replace) { return strtr($item, $replace); }, $items[1]); }), ); $test_data = array(); foreach($urls as $url => $vectors) { $test_data[] = array( $url, $vectors ); } return $test_data; } /** * Test safe unserialization * * @param String $str Serialized string to be checked * @param boolean $result If we expect the string to fail or not * * @dataProvider unserializeProvider * @requires PHP < 7 */ public function testObjectsCannotBeUnserializedInPhp5($str, $result) { $r=@php_safe_unserialize($str); $this->assertSame($result, (bool)$r, 'Save unserialize failed'); } /** * Test safe unserialization * * @param String $str Serialized string to be checked * @param boolean $result If we expect the string to fail or not * * @dataProvider unserializeProvider * @requires PHP 7 */ public function testObjectsCannotBeUnserializedInPhp7($str, $result) { $r=@php_safe_unserialize($str); if((bool)($r) !== $result) { if (!$result) { $matches = null; if (preg_match_all('/([^ ]+) Object\(/', array2string($r), $matches)) { foreach($matches[1] as $class) { if (!preg_match('/^__PHP_Incomplete_Class(#\d+)?$/', $class)) { $this->fail($str); } } } } else { $this->fail("false positive: $str"); } } // Avoid this test getting reported as no assertions, we do the testing // in the foreach loop $this->assertTrue(true); } /** * Data set for unserialize test */ public function unserializeProvider() { $tests = array( // Serialized string, expected result // things unsafe to unserialize Array("O:34:\"Horde_Kolab_Server_Decorator_Clean\":2:{s:43:\"\x00Horde_Kolab_Server_Decorator_Clean\x00_server\";", false), Array("O:20:\"Horde_Prefs_Identity\":2:{s:9:\"\x00*\x00_prefs\";O:11:\"Horde_Prefs\":2:{s:8:\"\x00*\x00_opts\";a:1:{s:12:\"sizecallback\";", false), Array("a:2:{i:0;O:12:\"Horde_Config\":1:{s:13:\"\x00*\x00_oldConfig\";s:#{php_injection.length}:\"#{php_injection}\";}i:1;s:13:\"readXMLConfig\";}}", false), Array('a:6:{i:0;i:0;i:1;d:2;i:2;s:4:"ABCD";i:3;r:3;i:4;O:8:"my_Class":2:{s:1:"a";r:6;s:1:"b";N;};i:5;C:16:"SplObjectStorage":14:{x:i:0;m:a:0:{}}', false), Array(serialize(new \stdClass()), false), Array(serialize(array(new \stdClass(), new \SplObjectStorage())), false), // string content, safe to unserialize Array(serialize('O:8:"stdClass"'), true), Array(serialize('C:16:"SplObjectStorage"'), true), Array(serialize(array('a', 'O:8:"stdClass"', 'b', 'C:16:"SplObjectStorage"')), true) ); if (PHP_VERSION >= 7) { // Fails our php<7 regular expression, because it has correct delimiter (^|;|{) in front of pattern :-( $tests[] = Array(serialize('O:8:"stdClass";C:16:"SplObjectStorage"'), true); } return $tests; } }