Skip to content
Open
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
19 changes: 15 additions & 4 deletions src/Codeception/Command/GenerateScenarios.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
33 changes: 26 additions & 7 deletions src/Codeception/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)) {
Expand Down Expand Up @@ -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;
}

/**
Expand Down
27 changes: 21 additions & 6 deletions src/Codeception/Lib/Driver/Sqlite.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Expand Down
15 changes: 13 additions & 2 deletions src/Codeception/Module/Filesystem.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -250,7 +261,7 @@ public function dontSeeFileFound($filename, $path = '')
*/
public function cleanDir($dirname)
{
$path = $this->absolutizePath($dirname);
$path = $this->sanitizePath($dirname);
Util::doEmptyDir($path);
}

Expand Down