diff --git a/src/Codeception/Command/GenerateScenarios.php b/src/Codeception/Command/GenerateScenarios.php index adf76ba7e7..127512391f 100644 --- a/src/Codeception/Command/GenerateScenarios.php +++ b/src/Codeception/Command/GenerateScenarios.php @@ -52,14 +52,25 @@ protected function execute(InputInterface $input, OutputInterface $output) : Configuration::dataDir() . 'scenarios'; $format = $input->getOption('format'); + // Define a safe base directory, e.g., the data directory + $baseDir = Configuration::dataDir(); + $path = $input->getOption('path') + ? $input->getOption('path') + : $baseDir . 'scenarios'; + + // Resolve and validate the path + $realPath = realpath($path) ?: $path; + if (strpos($realPath, $baseDir) !== 0) { + throw new ConfigurationException("Invalid path detected for scenarios output."); + } - @mkdir($path); + @mkdir($realPath); - if (!is_writable($path)) { - throw new ConfigurationException("Path $path is not writable. Please, set valid permissions for folder to store scenarios."); + if (!is_writable($realPath)) { + throw new ConfigurationException("Path $realPath is not writable. Please, set valid permissions for folder to store scenarios."); } - $path = $path . DIRECTORY_SEPARATOR . $suite; + $path = $realPath . DIRECTORY_SEPARATOR . $suite; if (!$input->getOption('single-file')) @mkdir($path); $suiteManager = new \Codeception\SuiteManager(new EventDispatcher(), $suite, $suiteconf); diff --git a/src/Codeception/Configuration.php b/src/Codeception/Configuration.php index a17319dd28..782e9724be 100644 --- a/src/Codeception/Configuration.php +++ b/src/Codeception/Configuration.php @@ -81,6 +81,14 @@ class Configuration 'error_level' => 'E_ALL & ~E_STRICT & ~E_DEPRECATED', ); + // Add this helper function to validate the path + protected static function isPathInProjectDir($path) + { + $realProjectDir = realpath(self::$dir); + $realPath = realpath($path) ?: $path; + return strpos($realPath, $realProjectDir) === 0; + } + /** * Loads global config file which is `codeception.yml` by default. * When config is already loaded - returns it. @@ -313,7 +321,7 @@ public static function createModule($class, $config, $namespace = '') return new $class($config); } - // try find module under users suite namespace setting + // try to find module under users suite namespace setting $className = $namespace.'\\Codeception\\Module\\' . $class; if (!@class_exists($className)) { @@ -401,18 +409,29 @@ public static function outputDir() if (!self::$logDir) { throw new ConfigurationException("Path for logs not specified. Please, set log path in global config"); } - $dir = self::$dir . DIRECTORY_SEPARATOR . self::$logDir . DIRECTORY_SEPARATOR; + // Normalize logDir to prevent traversal + $projectDir = realpath(self::$dir); + $logDirName = basename(self::$logDir); // strips any path traversal + $targetDir = $projectDir . DIRECTORY_SEPARATOR . $logDirName . DIRECTORY_SEPARATOR; - if (!is_writable($dir)) { - @mkdir($dir); - @chmod($dir, 0777); + if (strpos($targetDir, $projectDir) !== 0) { + throw new ConfigurationException("Log directory path traversal detected."); + } + + if (!is_writable($targetDir)) { + @mkdir($targetDir); + $resolvedDir = realpath($targetDir); + if ($resolvedDir === false || strpos($resolvedDir, $projectDir) !== 0) { + throw new ConfigurationException("Log directory path traversal detected after creation."); + } + @chmod($resolvedDir, 0777); } - if (!is_writable($dir)) { + if (!is_writable($targetDir)) { throw new ConfigurationException("Path for logs is not writable. Please, set appropriate access mode for log path."); } - return $dir; + return $targetDir; } /** diff --git a/src/Codeception/Lib/Driver/Sqlite.php b/src/Codeception/Lib/Driver/Sqlite.php index f89e67d101..7c41b965c5 100644 --- a/src/Codeception/Lib/Driver/Sqlite.php +++ b/src/Codeception/Lib/Driver/Sqlite.php @@ -14,26 +14,41 @@ public function __construct($dsn, $user, $password) $this->filename = \Codeception\Configuration::projectDir() . substr($this->dsn, 7); $this->dsn = 'sqlite:' . $this->filename; } - + + private function sanitizeFilename($filename) + { + $projectDir = \Codeception\Configuration::projectDir(); + $base = basename($filename); + $realPath = realpath($projectDir . '/' . $base); + if ($realPath === false || strpos($realPath, $projectDir) !== 0) { + throw new \RuntimeException('Invalid database filename'); + } + return $realPath; + } + public function cleanup() { + $safeFilename = $this->sanitizeFilename($this->filename); $this->dbh = null; - file_put_contents($this->filename, ''); + file_put_contents($safeFilename, ''); $this->dbh = self::connect($this->dsn, $this->user, $this->password); } public function load($sql) { + $safeFilename = $this->sanitizeFilename($this->filename); + $safeSnapshot = $safeFilename . '_snapshot'; + if ($this->hasSnapshot) { $this->dbh = null; - file_put_contents($this->filename, file_get_contents($this->filename . '_snapshot')); + file_put_contents($safeFilename, file_get_contents($safeSnapshot)); $this->dbh = new \PDO($this->dsn, $this->user, $this->password); } else { - if (file_exists($this->filename . '_snapshot')) { - unlink($this->filename . '_snapshot'); + if (file_exists($safeSnapshot)) { + unlink($safeSnapshot); } parent::load($sql); - copy($this->filename, $this->filename . '_snapshot'); + copy($safeFilename, $safeSnapshot); $this->hasSnapshot = true; } } diff --git a/src/Codeception/Module/Filesystem.php b/src/Codeception/Module/Filesystem.php index e703bd6cd1..4a62aba500 100644 --- a/src/Codeception/Module/Filesystem.php +++ b/src/Codeception/Module/Filesystem.php @@ -27,6 +27,16 @@ public function _before(\Codeception\TestCase $test) $this->path = \Codeception\Configuration::projectDir(); } + protected function sanitizePath($path) + { + $projectDir = \Codeception\Configuration::projectDir(); + $realPath = realpath($this->absolutizePath($path)); + if ($realPath === false || strpos($realPath, $projectDir) !== 0) { + throw new \RuntimeException('Invalid file path'); + } + return $realPath; + } + /** * Enters a directory In local filesystem. * Project root directory is used by default @@ -65,7 +75,8 @@ protected function absolutizePath($path) */ public function openFile($filename) { - $this->file = file_get_contents($this->absolutizePath($filename)); + $safePath = $this->sanitizePath($filename); + $this->file = file_get_contents($safePath); } /** @@ -250,7 +261,7 @@ public function dontSeeFileFound($filename, $path = '') */ public function cleanDir($dirname) { - $path = $this->absolutizePath($dirname); + $path = $this->sanitizePath($dirname); Util::doEmptyDir($path); }