@@ -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