[TikiWiki-commits] [Git][tikiwiki/tiki][24.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 <6a8558d8b7f91_3818c658867ce@gitlab-sidekiq-low-urgency-cpu-bound-v2-78f47cb56c-7xx85.mail>

Alfred Syatsukwa pushed to branch 24.x at Tiki Wiki CMS Groupware / Tiki


Commits:
204ce151 by Alfred Syatsukwa at 2026-08-19T07:12:53+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!10971

(cherry picked from commit 69075b2f3e2381385a7d9aada517a8565ed2ce52)

See merge request tikiwiki/tiki!10972

- - - - -


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
=====================================
@@ -444,62 +444,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/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,145 @@
+<?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;
+        }
+
+        switch ($config['type']) {
+            case 'local':
+                return $this->resolveLocalSource($config['file'] ?? '');
+            case 'http':
+            case '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/204ce151472683773a625bc25777e160d32aa057

-- 
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/204ce151472683773a625bc25777e160d32aa057
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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.