Skip to content

Commit 5bca3f3

Browse files
Merge pull request #146 from dreamfactorysoftware/develop
Merge develop into master
2 parents 16d9dbc + 2785760 commit 5bca3f3

File tree

1 file changed

+139
-14
lines changed

1 file changed

+139
-14
lines changed

src/Components/Package/Package.php

Lines changed: 139 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,139 @@ public function getFileFromZip($file)
635635
return null;
636636
}
637637

638+
/**
639+
* Encrypts zip file using native PHP ZipArchive encryption (PHP 7.2+).
640+
* This is the preferred method as it avoids shell execution entirely.
641+
*
642+
* @param string $password The password to encrypt the zip with
643+
* @return bool True if encryption succeeded, false otherwise
644+
*/
645+
protected function encryptZipWithNativeMethod($password)
646+
{
647+
// Check if native encryption is available (PHP 7.2+)
648+
if (!method_exists('\ZipArchive', 'setEncryptionName')) {
649+
return false;
650+
}
651+
652+
try {
653+
$tmpDir = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
654+
$encryptedZipFile = $tmpDir . 'encrypted_' . basename($this->zipFile);
655+
656+
// Create new encrypted zip
657+
$encryptedZip = new \ZipArchive();
658+
if (true !== $encryptedZip->open($encryptedZipFile, \ZipArchive::CREATE | \ZipArchive::OVERWRITE)) {
659+
return false;
660+
}
661+
662+
// Open original zip to read files
663+
$originalZip = new \ZipArchive();
664+
if (true !== $originalZip->open($this->zipFile)) {
665+
$encryptedZip->close();
666+
@unlink($encryptedZipFile);
667+
return false;
668+
}
669+
670+
// Copy all files from original to encrypted zip with password protection
671+
for ($i = 0; $i < $originalZip->numFiles; $i++) {
672+
$filename = $originalZip->getNameIndex($i);
673+
$fileContent = $originalZip->getFromIndex($i);
674+
675+
if ($fileContent === false) {
676+
// Skip directories
677+
continue;
678+
}
679+
680+
// Add file to encrypted zip
681+
$encryptedZip->addFromString($filename, $fileContent);
682+
683+
// Set encryption for this file using AES-256
684+
if (defined('ZipArchive::EM_AES_256')) {
685+
$encryptedZip->setEncryptionName($filename, \ZipArchive::EM_AES_256, $password);
686+
} else {
687+
// Fallback to AES-128 if AES-256 not available
688+
$encryptedZip->setEncryptionName($filename, \ZipArchive::EM_AES_128, $password);
689+
}
690+
}
691+
692+
$originalZip->close();
693+
$encryptedZip->close();
694+
695+
// Replace original with encrypted version
696+
@unlink($this->zipFile);
697+
if (!rename($encryptedZipFile, $this->zipFile)) {
698+
@unlink($encryptedZipFile);
699+
throw new InternalServerErrorException('Failed to replace original zip with encrypted version.');
700+
}
701+
702+
\Log::info('Encrypting zip file with a password using native PHP ZipArchive.');
703+
return true;
704+
} catch (\Exception $e) {
705+
\Log::error('Native zip encryption failed: ' . $e->getMessage());
706+
return false;
707+
}
708+
}
709+
710+
/**
711+
* Encrypts zip file using system zip command with PROPER shell escaping.
712+
* This is a secure fallback when native encryption is not available.
713+
*
714+
* SECURITY: All variables are properly escaped using escapeshellarg()
715+
* to prevent command injection (CVE-ZDI-CAN-26589).
716+
*
717+
* @param string $password The password to encrypt the zip with
718+
* @return void
719+
* @throws InternalServerErrorException
720+
*/
721+
protected function encryptZipWithShellCommand($password)
722+
{
723+
$tmpDir = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
724+
$extractDir = $tmpDir . substr(basename($this->zipFile), 0, strlen(basename($this->zipFile)) - 4);
725+
726+
try {
727+
// Extract the zip file
728+
$tmpZip = new \ZipArchive();
729+
if (true !== $tmpZip->open($this->zipFile)) {
730+
throw new InternalServerErrorException('Failed to open zip file for encryption.');
731+
}
732+
$tmpZip->extractTo($extractDir);
733+
$tmpZip->close();
734+
@unlink($this->zipFile);
735+
736+
// SECURITY FIX: Properly escape ALL shell arguments
737+
$escapedExtractDir = escapeshellarg($extractDir);
738+
$escapedPassword = escapeshellarg($password);
739+
$escapedZipFile = escapeshellarg($this->zipFile);
740+
741+
// Build secure command
742+
$server = strtolower(php_uname('s'));
743+
if (strpos($server, 'windows') !== false) {
744+
// Windows command
745+
$command = "cd $escapedExtractDir && zip -r -P $escapedPassword $escapedZipFile .";
746+
} else {
747+
// Unix/Linux command
748+
$command = "cd $escapedExtractDir && zip -r -P $escapedPassword $escapedZipFile .";
749+
}
750+
751+
// Execute with proper escaping - command injection is now prevented
752+
$output = [];
753+
$returnCode = 0;
754+
@exec($command, $output, $returnCode);
755+
756+
if ($returnCode !== 0) {
757+
throw new InternalServerErrorException('Failed to encrypt zip file using system command. Return code: ' . $returnCode);
758+
}
759+
760+
\Log::info('Encrypting zip file with a password using system zip command.', $output);
761+
762+
// Clean up extraction directory
763+
@FileUtilities::deleteTree($extractDir, true);
764+
} catch (\Exception $e) {
765+
// Clean up on failure
766+
@FileUtilities::deleteTree($extractDir, true);
767+
throw new InternalServerErrorException('Failed to encrypt zip file: ' . $e->getMessage());
768+
}
769+
}
770+
638771
/**
639772
* Saves ZipArchive.
640773
*
@@ -651,21 +784,13 @@ public function saveZipFile($storageService, $storageFolder)
651784

652785
if ($this->isSecured()) {
653786
$password = $this->getPassword();
654-
$tmpDir = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
655-
$extractDir = $tmpDir . substr(basename($this->zipFile), 0, strlen(basename($this->zipFile)) - 4);
656-
$tmpZip = new \ZipArchive();
657-
$tmpZip->open($this->zipFile);
658-
$tmpZip->extractTo($extractDir);
659-
$tmpZip->close();
660-
@unlink($this->zipFile);
661-
$server = strtolower(php_uname('s'));
662-
$commandSeparator = ';';
663-
if (strpos($server, 'windows') !== false) {
664-
$commandSeparator = '&';
787+
788+
// SECURITY FIX: Use native PHP ZipArchive encryption instead of shell exec
789+
// This eliminates command injection vulnerability (ZDI-CAN-26589)
790+
if (!$this->encryptZipWithNativeMethod($password)) {
791+
// Fallback to shell command with PROPER escaping (secure)
792+
$this->encryptZipWithShellCommand($password);
665793
}
666-
@exec("cd $extractDir $commandSeparator zip -r -P $password $this->zipFile .", $output);
667-
\Log::info('Encrypting zip file with a password.', $output);
668-
@FileUtilities::deleteTree($extractDir, true);
669794
}
670795

671796
/** @type FileServiceInterface $storage */

0 commit comments

Comments
 (0)