Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,19 @@ All notable changes to **Tiger Core** (`webtigers/tiger-core`). Format follows

## [Unreleased]

## [1.8.2] — 2026-09-16

### Fixed

- **A stale session cookie no longer 500s the site.** With PHP's files handler, a `PHPSESSID` the
browser still carries can name a file this process cannot read — cPanel's shared session directory
after an account is deleted and recreated (the file belongs to the old uid, and every returning
visitor to the domain hits it). `session_start()` failed, and because PHP defines `SID` even on a
failed start there is no retry through `Zend_Session`. The bootstrap now checks the presented id's
file first and starts under a fresh id when it exists but is unreadable
(`Tiger_Application_Bootstrap::dropUnreadableSessionId()`), logging the drop. Found by the second
outside install round (TIGER-138).

## [1.8.1] — 2026-09-15

Findings from an AI-driven install test of the web installer on a shared cPanel host (TIGER-138).
Expand Down
27 changes: 27 additions & 0 deletions library/Tiger/Application/Bootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -662,10 +662,37 @@ protected function _initSession()
}

if (!Zend_Session::isStarted()) {
if (!$useDb) { self::dropUnreadableSessionId(); }
Zend_Session::start();
}
}

/**
* With PHP's files handler, a session id the browser still carries can name a file this process
* cannot read — cPanel's shared session directory after an account is deleted and recreated (the
* file belongs to the old uid; every returning visitor hits it), a save_path that changed hands, a
* corrupt file. session_start() then fails, and because PHP defines SID even on failure there is no
* retry through Zend_Session. So look first: if the presented id's file exists and is unreadable,
* start under a fresh id — the visitor is a guest either way, and a guest must never see a 500.
*
* @param string|null $sid the presented id (default: the session cookie)
* @param string|null $path session.save_path (default: the ini value)
* @return bool true when the presented id was dropped
*/
public static function dropUnreadableSessionId($sid = null, $path = null)
{
$sid = (string) ($sid ?? ($_COOKIE[session_name()] ?? ''));
if ($sid === '' || !preg_match('/^[A-Za-z0-9,-]{1,128}$/', $sid)) { return false; }
$path = (string) ($path ?? ini_get('session.save_path'));
if ($path === '') { $path = sys_get_temp_dir(); }
if (strpos($path, ';') !== false) { $path = substr($path, strrpos($path, ';') + 1); } // "N;/path" and "N;mode;/path" forms
$file = rtrim($path, '/') . '/sess_' . $sid;
if (!is_file($file) || is_readable($file)) { return false; }
error_log('Tiger session: the presented session file is not readable (' . $file . '); starting a new session');
if (session_status() !== PHP_SESSION_ACTIVE) { session_id(bin2hex(random_bytes(16))); }
return true;
}

/** True when this request carries a Tiger personal access token (stateless mode). */
protected function _bearerRequest()
{
Expand Down
2 changes: 1 addition & 1 deletion library/Tiger/Version.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@
class Tiger_Version
{
/** Current Tiger Core version. Keep in lockstep with the git tag cut for a release. */
const VERSION = '1.8.1';
const VERSION = '1.8.2';
}
26 changes: 26 additions & 0 deletions tests/Unit/Application/BootstrapHelpersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,4 +124,30 @@ public function language_files_return_the_existing_cascade_members(): void
}
$this->assertContains(TIGER_CORE_PATH . '/core/languages/en/core.php', $files, 'the core language file leads the cascade');
}

/**
* A presented session id whose file cannot be read (cPanel's shared session dir after an account is
* recreated) must be dropped BEFORE session_start — PHP defines SID even on a failed start, so there
* is no retry afterwards. A readable file, a missing file, or a malformed id are left alone.
*/
#[Test]
public function an_unreadable_presented_session_file_is_dropped_before_start(): void
{
$dir = sys_get_temp_dir() . '/tiger-sess-' . bin2hex(random_bytes(4)); mkdir($dir, 0700);
$log = ini_set('error_log', $dir . '/error.log'); // the guard logs what it dropped; keep that off the test output
try {
$bad = str_repeat('b', 26); file_put_contents("$dir/sess_$bad", 'x'); chmod("$dir/sess_$bad", 0000);
$this->assertTrue(Tiger_Application_Bootstrap::dropUnreadableSessionId($bad, '5;' . $dir), 'unreadable → dropped ("N;/path" form parsed)');
$this->assertTrue(Tiger_Application_Bootstrap::dropUnreadableSessionId($bad, '5;0600;' . $dir), '"N;mode;/path" form parsed');
$good = str_repeat('c', 26); file_put_contents("$dir/sess_$good", 'x');
$this->assertFalse(Tiger_Application_Bootstrap::dropUnreadableSessionId($good, $dir), 'readable → kept');
$this->assertFalse(Tiger_Application_Bootstrap::dropUnreadableSessionId(str_repeat('d', 26), $dir), 'no such file → kept (PHP creates it)');
$this->assertFalse(Tiger_Application_Bootstrap::dropUnreadableSessionId('../../etc/passwd', $dir), 'junk never reaches the filesystem');
$this->assertFalse(Tiger_Application_Bootstrap::dropUnreadableSessionId('', $dir));
$this->assertStringContainsString('not readable', (string) file_get_contents($dir . '/error.log'), 'the drop is logged');
} finally {
ini_set('error_log', (string) $log);
@chmod("$dir/sess_$bad", 0600); array_map('unlink', glob("$dir/sess_*") ?: []); @unlink($dir . '/error.log'); @rmdir($dir);
}
}
}
Loading