tmp); ini_set('session.use_cookies', '0'); ini_set('session.cache_limiter', ''); } protected function tearDown(): void { if (session_status() === PHP_SESSION_ACTIVE) session_destroy(); parent::tearDown(); } public function testCsrfTokenIsRandomAndKept(): void { $token = csrf_token(); $this->assertMatchesRegularExpression('/^[0-9a-f]{64}$/', $token); $this->assertSame($token, csrf_token()); $this->assertSame($token, $_SESSION['csrf_token']); $_SESSION = array(); $this->assertNotSame($token, csrf_token()); } public function testCsrfTokenReplacesABadSessionValue(): void { $_SESSION['csrf_token'] = array('not a string'); $this->assertMatchesRegularExpression('/^[0-9a-f]{64}$/', csrf_token()); } public function testCsrfField(): void { $this->assertSame('', csrf_field()); } public function testCsrfValidAcceptsOnlyTheSessionToken(): void { $token = csrf_token(); $this->assertTrue(csrf_valid($token)); $this->assertFalse(csrf_valid(strtoupper($token))); $this->assertFalse(csrf_valid('')); $this->assertFalse(csrf_valid($token . 'x')); $this->assertFalse(csrf_valid(array($token))); } public function testCsrfValidReadsThePostedToken(): void { $token = csrf_token(); $this->assertFalse(csrf_valid()); $this->request(array(), array('csrf_token' => $token)); $this->assertTrue(csrf_valid()); $this->request(array('csrf_token' => $token)); $this->assertFalse(csrf_valid(), 'a token in the query string is not enough'); } public function testCsrfValidFailsWithoutASessionToken(): void { $this->assertFalse(csrf_valid('')); $this->assertFalse(csrf_valid('anything')); } public function testLoginEditorGivesANewSessionId(): void { session_start(); $before = session_id(); $_SESSION['csrf_token'] = 'old token'; $_SESSION['referrer'] = '/about'; login_editor('ed'); $this->assertNotSame($before, session_id()); $this->assertSame('ed', $_SESSION['editor']); $this->assertArrayNotHasKey('csrf_token', $_SESSION); $this->assertSame('/about', $_SESSION['referrer']); $this->assertFileDoesNotExist($this->tmp . '/sess_' . $before); } public function testPersistentCookieParts(): void { $this->assertSame(array('', ''), persistent_cookie()); $_COOKIE['PersistentAuth'] = 'ed|key|with|bars'; $this->assertSame(array('ed', 'key|with|bars'), persistent_cookie()); $_COOKIE['PersistentAuth'] = 'nobar'; $this->assertSame(array('nobar', ''), persistent_cookie()); $_COOKIE['PersistentAuth'] = array('ed', 'key'); $this->assertSame(array('', ''), persistent_cookie()); } public function testPersistentKeyHash(): void { $this->assertSame(hash('sha256', 'key'), persistent_key_hash('key')); } public function testCreatePersistentLoginStoresOnlyAHash(): void { $this->assertTrue(create_persistent_login('ed')); $rows = SiteData::$tables['persistent_sessions']; $this->assertCount(1, $rows); $this->assertSame('ed', $rows[0]['username']); $this->assertMatchesRegularExpression('/^[0-9a-f]{64}$/', $rows[0]['session']); $this->assertNull($GLOBALS['db'], 'the connection is closed again'); } public function testCheckPersistentLoginWithoutACookie(): void { $this->assertFalse(check_persistent_login()); $this->assertSame(array(), FakeDb::$log); } public function testCheckPersistentLoginWithAnIncompleteCookie(): void { foreach (array('', 'ed', 'ed|', '|key') as $cookie) { $_COOKIE['PersistentAuth'] = $cookie; $this->assertFalse(check_persistent_login(), $cookie); } $this->assertSame(array(), FakeDb::$log); } public function testCheckPersistentLoginWithAWrongKey(): void { SiteData::$tables['persistent_sessions'][] = array('username' => 'ed', 'session' => persistent_key_hash('right'), 'created' => gmdate('Y-m-d H:i:s')); $_COOKIE['PersistentAuth'] = 'ed|wrong'; $this->assertFalse(check_persistent_login()); $this->assertArrayNotHasKey('editor', $_SESSION); $query = FakeDb::matching('/FROM persistent_sessions/')[0]; $this->assertSame(array('ed', persistent_key_hash('wrong')), $query['params']); } public function testCheckPersistentLoginWithAKeyOfAnotherEditor(): void { SiteData::$tables['persistent_sessions'][] = array('username' => 'ann', 'session' => persistent_key_hash('key'), 'created' => gmdate('Y-m-d H:i:s')); $_COOKIE['PersistentAuth'] = 'ed|key'; $this->assertFalse(check_persistent_login()); } public function testCheckPersistentLoginLogsInAndSwapsTheKey(): void { session_start(); $before = session_id(); SiteData::$tables['persistent_sessions'][] = array('username' => 'ed', 'session' => persistent_key_hash('key'), 'created' => gmdate('Y-m-d H:i:s')); $_COOKIE['PersistentAuth'] = 'ed|key'; $this->assertTrue(check_persistent_login()); $this->assertSame('ed', $_SESSION['editor']); $this->assertNotSame($before, session_id()); $rows = SiteData::$tables['persistent_sessions']; $this->assertCount(1, $rows); $this->assertNotSame(persistent_key_hash('key'), $rows[0]['session'], 'the used key is gone'); } public function testDeletePersistentLoginRemovesThisKeyAndOldKeys(): void { SiteData::$tables['persistent_sessions'] = array( array('username' => 'ed', 'session' => persistent_key_hash('mine'), 'created' => gmdate('Y-m-d H:i:s')), array('username' => 'ed', 'session' => persistent_key_hash('other device'), 'created' => gmdate('Y-m-d H:i:s')), array('username' => 'ann', 'session' => persistent_key_hash('old'), 'created' => '2000-01-01 00:00:00'), array('username' => 'bo', 'session' => 'an unhashed key from before', 'created' => gmdate('Y-m-d H:i:s')), ); $_COOKIE['PersistentAuth'] = 'ed|mine'; $this->assertTrue(delete_persistent_login()); $this->assertSame(array(persistent_key_hash('other device')), array_column(SiteData::$tables['persistent_sessions'], 'session')); $this->assertCount(1, FakeDb::matching('/INTERVAL 1 YEAR.*CHAR_LENGTH\(session\) <> 64/s')); } public function testDeletePersistentLoginWithoutACookieStillReaps(): void { delete_persistent_login(); $this->assertCount(0, FakeDb::matching('/WHERE username = \?/')); $this->assertCount(1, FakeDb::matching('/CHAR_LENGTH/')); } // The top of session.lib.php runs on every page; it needs HTTPS and a session public function testSessionStartsOnlyOverHttps(): void { $code = '$_SERVER["HTTPS"] = %s; session_id("abc123"); session_start(); $_SESSION["editor"] = "ed"; session_write_close();' . ' $_COOKIE[session_name()] = "abc123"; include XXX_ROOT . "/session.lib.php";' . ' echo json_encode(array($is_admin, $username ?? NULL, session_status() === PHP_SESSION_ACTIVE));'; $ini = array('session.save_path' => $this->tmp, 'session.use_cookies' => '0', 'session.cache_limiter' => ''); $https = PhpProcess::runBare(sprintf($code, '"on"'), array(), $ini); $this->assertSame('[true,"ed",true]', $https['stdout'], $https['stderr']); } public function testNoSessionOverPlainHttp(): void { $result = PhpProcess::runBare('include XXX_ROOT . "/session.lib.php"; echo json_encode(array($is_admin, session_status() === PHP_SESSION_ACTIVE));', array(), array('session.save_path' => $this->tmp)); $this->assertSame('[false,false]', $result['stdout'], $result['stderr']); } public function testPersistentCookieLogsInOverHttps(): void { $store = $this->tmp . '/tables.json'; file_put_contents($store, json_encode(array('persistent_sessions' => array( array('username' => 'ed', 'session' => persistent_key_hash('key'), 'created' => gmdate('Y-m-d H:i:s')))))); $code = 'Xxx\Tests\Support\SiteData::install(json_decode(file_get_contents(' . var_export($store, TRUE) . '), TRUE));' . ' $_SERVER["HTTPS"] = "on"; $_COOKIE["PersistentAuth"] = "ed|key"; include XXX_ROOT . "/session.lib.php";' . ' echo json_encode(array($is_admin, $username ?? NULL));'; $result = PhpProcess::runBare($code, array(), array('session.save_path' => $this->tmp, 'session.use_cookies' => '0', 'session.cache_limiter' => '')); $this->assertSame('[true,"ed"]', $result['stdout'], $result['stderr']); } }