[TikiWiki-commits] [Git][tikiwiki/tiki][30.x] [ENH] Implemented a hardening patch that closes the dangerous base-image ingestion path
"Alfred Syatsukwa \(@alfredsyatsukwa\) via TikiWiki-cvs" <[email protected]>
| Newsgroups | gmane.comp.cms.tiki.cvs |
|---|---|
| Message-ID | <6a85443ca2d3_3818c6d07388@gitlab-sidekiq-low-urgency-cpu-bound-v2-54bcdfbdc9-b6gd2.mail> |
Alfred Syatsukwa pushed to branch 30.x at Tiki Wiki CMS Groupware / Tiki Commits: ebc086cd by Alfred Syatsukwa at 2026-08-19T05:44:34+00:00 [ENH] Implemented a hardening patch that closes the dangerous base-image ingestion path --- * [ENH] Implemented a hardening patch that closes the dangerous base-image ingestion path --- See merge request tikiwiki/tiki!10968 (cherry picked from commit 398c7a8dfcceb542a4d43fe328fbcaf890b0c885) See merge request tikiwiki/tiki!10969 - - - - - 4 changed files: - db/install.ini.dist - installer/Installer.php - + lib/core/Tiki/Installer/BaseImageResolver.php - + lib/test/Core/Security/InstallerDatabaseSetupTest.php Changes: ===================================== db/install.ini.dist ===================================== @@ -1,12 +1,9 @@ ; Rename this file to install.ini ; Uncomment the appropriate section and modify it to your parameters -; Local file (or through the local filesystem) +; Local file stored under the db/ directory ;source.type = local -;source.file = /home/example/backups/dump.sql - -; Remote file - Make sure the file access is limited through a firewall or authentication -; MD5 hash is optional, but recommended. -;source.type = http -;source.file = "https://user:password-nmTBJg/dIHSkmABc3PwzPQC/[email protected]/backups/dump.sql" -;source.md5 = 2af0507e3468d796fa6a204e410e34a5 +;source.file = custom_tiki.sql +; +; Relative paths are resolved from db/, so "backups/dump.sql" will load db/backups/dump.sql. +; Remote URLs are no longer supported. Download the SQL dump into db/ before running the installer. ===================================== installer/Installer.php ===================================== @@ -475,62 +475,7 @@ class Installer extends TikiDb_Bridge implements SplSubject } private function getBaseImage() { - $iniFile = __DIR__ . '/../db/install.ini'; - - $ini = []; - if (is_readable($iniFile)) { - $ini = parse_ini_file($iniFile); - } - - $direct = __DIR__ . '/../db/custom_tiki.sql'; - $fetch = null; - $check = null; - - if (isset($ini['source.type'])) { - switch ($ini['source.type']) { - case 'local': - $direct = $ini['source.file']; - break; - case 'http': - $fetch = $ini['source.file']; - if (isset($ini['source.md5'])) { - $check = $ini['source.md5']; - } - break; - } - } - - if (is_readable($direct)) { - return $direct; - } - - if (! $fetch) { - return; - } - - $cacheFile = __DIR__ . '/../' . TEMP_CACHE_PATH . '/sql' . md5($fetch); - - if (is_readable($cacheFile)) { - return $cacheFile; - } - - $read = fopen($fetch, 'r'); - $write = fopen($cacheFile, 'w+'); - - if ($read && $write) { - while (! feof($read)) { - fwrite($write, fread($read, 1024 * 100)); - } - - fclose($read); - fclose($write); - - if (! $check || $check == md5_file($cacheFile)) { - return $cacheFile; - } else { - unlink($cacheFile); - } - } + return (new BaseImageResolver(__DIR__ . '/..'))->resolve(); } /** ===================================== lib/core/Tiki/Installer/BaseImageResolver.php ===================================== @@ -0,0 +1,141 @@ +<?php + +// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project +// +// All Rights Reserved. See copyright.txt for details and a complete list of authors. +// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details. +namespace Tiki\Installer; + +use Exception; + +class BaseImageResolver +{ + private string $tikiRoot; + private string $dbDirectory; + + public function __construct(string $tikiRoot) + { + $this->tikiRoot = rtrim($tikiRoot, DIRECTORY_SEPARATOR); + $dbDirectory = realpath($this->tikiRoot . DIRECTORY_SEPARATOR . 'db'); + + if ($dbDirectory === false || ! is_dir($dbDirectory)) { + throw new Exception('Fatal: Cannot access installer db directory'); + } + + $this->dbDirectory = $dbDirectory; + } + + public function resolve(): ?string + { + $config = $this->loadConfiguration(); + + if ($config === null) { + return null; + } + + return match ($config['type']) { + 'local' => $this->resolveLocalSource($config['file'] ?? ''), + 'http', 'https' => throw new Exception( + 'Fatal: Remote install base images are no longer supported. Download the SQL dump into db/ and use source.type=local.' + ), + default => throw new Exception('Fatal: Unsupported install base image source type "' . $config['type'] . '".'), + }; + } + + private function loadConfiguration(): ?array + { + $iniFile = $this->dbDirectory . DIRECTORY_SEPARATOR . 'install.ini'; + + if (! is_readable($iniFile)) { + return null; + } + + $ini = parse_ini_file($iniFile); + + if ($ini === false) { + throw new Exception('Fatal: Cannot parse ' . $iniFile); + } + + if (empty($ini['source.type'])) { + return null; + } + + return [ + 'type' => strtolower(trim($ini['source.type'])), + 'file' => $ini['source.file'] ?? '', + ]; + } + + private function resolveLocalSource(string $file): string + { + $file = trim($file); + + if ($file === '') { + $file = 'custom_tiki.sql'; + } + + if (str_contains($file, '://')) { + throw new Exception('Fatal: Local install base image path is invalid.'); + } + + $resolved = $this->resolvePath($file); + + if ($resolved === null || ! is_file($resolved) || ! is_readable($resolved)) { + throw new Exception('Fatal: Cannot open ' . $file); + } + + if (! $this->pathIsWithinDirectory($resolved, $this->dbDirectory)) { + throw new Exception('Fatal: Local install base images must be stored inside db/.'); + } + + return $resolved; + } + + private function resolvePath(string $file): ?string + { + $candidate = $this->isAbsolutePath($file) + ? $file + : $this->buildRelativeCandidate($file); + + $resolved = realpath($candidate); + + return $resolved === false ? null : $resolved; + } + + private function buildRelativeCandidate(string $file): string + { + $normalized = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, ltrim($file, '/\\')); + + if ($normalized === 'db' || str_starts_with($normalized, 'db' . DIRECTORY_SEPARATOR)) { + return $this->tikiRoot . DIRECTORY_SEPARATOR . $normalized; + } + + return $this->dbDirectory . DIRECTORY_SEPARATOR . $normalized; + } + + private function pathIsWithinDirectory(string $path, string $directory): bool + { + $normalizedPath = $this->normalizePath($path); + $normalizedDirectory = rtrim($this->normalizePath($directory), '/'); + + return $normalizedPath === $normalizedDirectory + || str_starts_with($normalizedPath, $normalizedDirectory . '/'); + } + + private function normalizePath(string $path): string + { + $normalized = str_replace('\\', '/', $path); + + if (DIRECTORY_SEPARATOR === '\\') { + $normalized = strtolower($normalized); + } + + return $normalized; + } + + private function isAbsolutePath(string $path): bool + { + return str_starts_with($path, DIRECTORY_SEPARATOR) + || preg_match('/^[A-Za-z]:[\\\\\\/]/', $path) === 1; + } +} ===================================== lib/test/Core/Security/InstallerDatabaseSetupTest.php ===================================== @@ -0,0 +1,125 @@ +<?php + +// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project +// +// All Rights Reserved. See copyright.txt for details and a complete list of authors. +// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details. +namespace Test\Core\Security; + +use Exception; +use PHPUnit\Framework\TestCase; +use Tiki\Installer\BaseImageResolver; + +class InstallerDatabaseSetupTest extends TestCase +{ + private string $tempRoot; + private string $dbDirectory; + + protected function setUp(): void + { + $this->tempRoot = sys_get_temp_dir() . '/tiki_installer_' . uniqid('', true); + $this->dbDirectory = $this->tempRoot . '/db'; + + if (! mkdir($this->dbDirectory, 0777, true) && ! is_dir($this->dbDirectory)) { + $this->fail('Unable to create temporary installer directory for tests.'); + } + } + + protected function tearDown(): void + { + $this->removeDir($this->tempRoot); + } + + public function testCustomSqlIsIgnoredWithoutInstallIni(): void + { + $this->createFile($this->dbDirectory . '/custom_tiki.sql', "SELECT 1;\n"); + + $resolver = new BaseImageResolver($this->tempRoot); + + $this->assertNull($resolver->resolve()); + } + + public function testExplicitLocalSourceDefaultsToCustomSql(): void + { + $expected = $this->dbDirectory . '/custom_tiki.sql'; + $this->createFile($this->dbDirectory . '/install.ini', "source.type = local\n"); + $this->createFile($expected, "SELECT 1;\n"); + + $resolver = new BaseImageResolver($this->tempRoot); + + $this->assertSame(realpath($expected), $resolver->resolve()); + } + + public function testExplicitLocalSourceCanUseDbSubdirectories(): void + { + $expected = $this->dbDirectory . '/backups/dump.sql'; + $this->createFile($this->dbDirectory . '/install.ini', "source.type = local\nsource.file = backups/dump.sql\n"); + $this->createFile($expected, "SELECT 1;\n"); + + $resolver = new BaseImageResolver($this->tempRoot); + + $this->assertSame(realpath($expected), $resolver->resolve()); + } + + public function testRejectsLocalSourceOutsideDbDirectory(): void + { + $this->createFile($this->tempRoot . '/outside.sql', "SELECT 1;\n"); + $this->createFile($this->dbDirectory . '/install.ini', "source.type = local\nsource.file = ../outside.sql\n"); + + $resolver = new BaseImageResolver($this->tempRoot); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Local install base images must be stored inside db/.'); + $resolver->resolve(); + } + + public function testRejectsRemoteSourceTypes(): void + { + $this->createFile( + $this->dbDirectory . '/install.ini', + "source.type = http\nsource.file = https://example.com/dump.sql\n" + ); + + $resolver = new BaseImageResolver($this->tempRoot); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Remote install base images are no longer supported.'); + $resolver->resolve(); + } + + private function createFile(string $path, string $contents): void + { + $directory = dirname($path); + + if (! is_dir($directory) && ! mkdir($directory, 0777, true) && ! is_dir($directory)) { + $this->fail('Unable to create fixture directory: ' . $directory); + } + + if (file_put_contents($path, $contents) === false) { + $this->fail('Unable to write fixture file: ' . $path); + } + } + + private function removeDir(string $dir): void + { + if (! is_dir($dir)) { + return; + } + + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS), + \RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($iterator as $item) { + if ($item->isDir()) { + @rmdir($item->getPathname()); + } else { + @chmod($item->getPathname(), 0644); + @unlink($item->getPathname()); + } + } + + @rmdir($dir); + } +} View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/ebc086cd560b651733301d841cc4326c5656da48 -- View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/ebc086cd560b651733301d841cc4326c5656da48 You're receiving this email because of your account on gitlab.com. Manage all notifications: https://gitlab.com/-/profile/notifications | Help: https://gitlab.com/help _______________________________________________ TikiWiki-cvs mailing list [email protected] https://lists.sourceforge.net/lists/listinfo/tikiwiki-cvs