High severity8.6NVD Advisory· Published May 21, 2025· Updated Apr 15, 2026
CVE-2025-48201
CVE-2025-48201
Description
The ns_backup extension through 13.0.0 for TYPO3 has a Predictable Resource Location.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
nitsan/ns-backupPackagist | < 13.0.1 | 13.0.1 |
Patches
167b8102a19e8[TASK] Security fix
15 files changed · +455 −157
Classes/Controller/BackupBaseController.php+173 −96 modified@@ -4,11 +4,10 @@ use RuntimeException; use TYPO3\CMS\Core\Core\Environment; -use TYPO3\CMS\Core\Exception; use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3\CMS\Extbase\Mvc\Controller\ActionController; use NITSAN\NsBackup\Domain\Repository\BackupglobalRepository; -use TYPO3\CMS\Extbase\Utility\LocalizationUtility as transalte; +use TYPO3\CMS\Extbase\Utility\LocalizationUtility; /*** * @@ -144,7 +143,7 @@ class BackupBaseController extends ActionController public function __construct( protected BackupglobalRepository $backupglobalRepository ) { - $this->exceptionMessage = transalte::translate('something.wrong.here', 'ns_backup'); + $this->exceptionMessage = LocalizationUtility::translate('something.wrong.here', 'ns_backup'); } @@ -161,7 +160,7 @@ public function globalErrorValidation(): string $arrKeys = ['emails', 'emailSubject', 'compress', 'php', 'root', 'siteurl', 'cleanup', 'cleanupQuantity']; $arrValidation = []; foreach ($arrKeys as $key) { - $arrValidation[$key] = transalte::translate("global.error.$key", 'ns_backup'); + $arrValidation[$key] = LocalizationUtility::translate("global.error.$key", 'ns_backup'); } $errorValidation = implode('', array_map(function ($key, $value) { @@ -173,12 +172,12 @@ public function globalErrorValidation(): string $arrExtensionsToCheck = ['curl', 'dom', 'json']; foreach ($arrExtensionsToCheck as $extension) { if (!in_array($extension, $arrGetLoadedExtensions)) { - $errorValidation .= '<li>' . transalte::translate("global.error.$extension", 'ns_backup') . '</li>'; + $errorValidation .= '<li>' . LocalizationUtility::translate("global.error.$extension", 'ns_backup') . '</li>'; } } // Check if exec() works if (!exec('echo EXEC') == 'EXEC') { - $errorValidation .= '<li>' . transalte::translate('global.error.exec', 'ns_backup') . '</li>'; + $errorValidation .= '<li>' . LocalizationUtility::translate('global.error.exec', 'ns_backup') . '</li>'; } return $errorValidation; } @@ -200,7 +199,6 @@ public function generateBackup(array $arrPost): array // Get TYPO3 Path $this->rootPath = $this->globalSettingsData[0]->root ?? (Environment::getProjectPath() ?? ''); - $this->phpbuPath = $this->rootPath.'/typo3conf/ext/ns_backup/phpbu.phar'; // Let's change root path to /public in Composer-based installation if(Environment::isComposerMode()) { @@ -210,10 +208,17 @@ public function generateBackup(array $arrPost): array } // Get Local Storage Path - $this->localStoragePath = $this->rootPath.'/uploads/tx_nsbackup/'; + $globalBackupStorePath = $this->globalSettingsData[0]->getBackupStorePath(); + $isPublicPath = $this->isPathPublic($globalBackupStorePath); + if ($globalBackupStorePath == '') { + $this->localStoragePath = $this->rootPath . '/tx_nsbackup/'; + $jsonFolder = $this->rootPath . '/tx_nsbackup/json/'; + } else { + $this->localStoragePath = $globalBackupStorePath . '/tx_nsbackup/'; + $jsonFolder = $globalBackupStorePath . '/tx_nsbackup/json/'; + } try { if (!file_exists($this->localStoragePath)) { - GeneralUtility::mkdir_deep($this->localStoragePath); } } catch (RuntimeException $e) { @@ -224,8 +229,19 @@ public function generateBackup(array $arrPost): array } // Get Base URL - $this->siteUrl = $this->globalSettingsData[0]->siteurl ?? ''; - $this->baseURL = $this->siteUrl . '/uploads/tx_nsbackup/'; + $this->siteUrl = ''; + if (!empty($this->globalSettingsData[0]->siteurl)) { + $this->siteUrl = $this->globalSettingsData[0]->siteurl; + } + $path = str_replace($this->rootPath, '', $globalBackupStorePath); + $this->baseURL = $this->siteUrl . $path . '/tx_nsbackup/'; + + // Get PHPHBU Path + if (Environment::isComposerMode()) { + $this->phpbuPath = $this->composerRootPath . '/vendor/nitsan/ns-backup/phpbu.phar'; + } else { + $this->phpbuPath = $this->rootPath . '/typo3conf/ext/ns_backup/phpbu.phar'; + } // Get Database Configuration $this->arrDatabase = $GLOBALS['TYPO3_CONF_VARS']['DB']['Connections']['Default']; @@ -234,22 +250,28 @@ public function generateBackup(array $arrPost): array // Get Current Date time $this->prefixFileName = date('dmY_Hi'); + // Prepare backup filename + $backupName = $arrPost['backupName'].'_'.$this->prefixFileName; + $currentDateTime = ''; $backupNameOriginal = $arrPost['backupName']; - $backupName = $this->prefixFileName.'_'.$arrPost['backupName']; + $backupFileName = strtolower(trim($backupName)); + $backupFileName = preg_replace(['/[\s-]+/', '/[^A-Za-z0-9_]/', '/_+/'], '_', $backupFileName); $backupType = $arrPost['backupFolderSettings']; + // Generates an 8-character random string + $randomString = substr(md5(uniqid(mt_rand(), true)), 0, 8); + $backupBaseName = GeneralUtility::trimExplode('_', $backupFileName, true, 3)[1]; + + $jsonFile = $backupBaseName . '_' . $randomString . '_' . $backupType . '_configuration.json'; + $logFile = $jsonFolder . $backupBaseName . '_' . $randomString . '_' . $backupType . '_log.json'; + $jsonPath = $jsonFolder . $jsonFile; + // Prepare backup filename $backupFileName = preg_replace( '/[^A-Za-z0-9]+/', '_', preg_replace('/[\s-]+/', '_', strtolower(trim($backupName))) ); - - $jsonFolder = $this->rootPath.'/uploads/tx_nsbackup/json/'; - $jsonFile = GeneralUtility::trimExplode('_', $backupFileName, true, 3)[2] . '_' . $backupType . '_configuration.json'; - $logFile = $jsonFolder . GeneralUtility::trimExplode('_', $backupFileName, true, 3)[2] . '_' . $backupType . '_log.json'; - $jsonPath = $jsonFolder.$jsonFile; - // Let's create LOG file if not existis if (!file_exists($logFile)) { $fh = @fopen($logFile, 'a'); @@ -272,7 +294,7 @@ public function generateBackup(array $arrPost): array "options": { "transport": "mail", "recipients": "'.$this->globalSettingsData[0]->emails.'", - "subject": "['.$backupType.'] '.$backupNameOriginal. ' - '.$this->globalSettingsData[0]->emailSubject.'", + "subject": "[' . $backupType . '] ' . $backupNameOriginal . ' - ' . $this->globalSettingsData[0]->emailSubject . '", "sendOnlyOnError": "'.$this->globalSettingsData[0]->emailNotificationOnError.'" } } @@ -281,11 +303,16 @@ public function generateBackup(array $arrPost): array // Let's check if admin wants "Backup Everyting" if ($backupType == 'all') { + + // store date and time before backup + $currentDateTime = date('Ymd-Hi'); // Create Database Backup - $json .= $this->getPhpbuBackup($backupName, 'mysqldump', $backupFileName). ','; + $json .= $this->getPhpbuBackup($backupName, 'mysqldump', $backupFileName) . ','; // Create Code Backup $json .= $this->getPhpbuBackup($backupName, $backupType, $backupFileName); + } elseif ($backupType == 'other') { + $json .= $this->getPhpbuBackup($backupName, $backupType, $backupFileName, $arrPost['custompath']); } else { // Create Specific Selected Type of Backup $json .= $this->getPhpbuBackup($backupName, $backupType, $backupFileName); @@ -297,23 +324,28 @@ public function generateBackup(array $arrPost): array '; try { - // Let's create JSCON folder does not exists + // Ensure the directory exists if (!file_exists($jsonFolder)) { - GeneralUtility::mkdir_deep($jsonFolder); + GeneralUtility::mkdir($jsonFolder); } - // Let's create JSON file + // Write JSON content to file file_put_contents($jsonPath, $json); + // Validate and sanitize PHP path + if (!is_string($this->phpPath) || !file_exists($this->phpPath) || !is_executable($this->phpPath)) { + throw new RuntimeException("Invalid PHP executable path."); + } + // Prepare SSH Command - $command = $this->phpPath. ' '. $this->phpbuPath.' --configuration='.$jsonPath.' --verbose'; + $command = $this->phpPath . ' ' . $this->phpbuPath . ' --configuration=' . $jsonPath . ' --verbose'; // Execute Backup SSH Command - exec($command, $log); + exec($command, $log, $return_var); } catch (RuntimeException $e) { return [ 'log' => 'error', - 'backup_file' => $this->exceptionMessage, + 'backup_file' => 'Something is wrong here.' . $e->getMessage(), ]; } @@ -327,42 +359,65 @@ public function generateBackup(array $arrPost): array // If Backup Everything, Then let's first-insert MySQL as special case if ($backupType == 'all') { $arrPost['backup_type'] = 'mysqldump'; - $arrPost['download_url'] = $this->backupDownloadPathMySQL; - $fileSize = $this->convertFilesize(filesize($this->backupFileMySQL)); + $arrPost['download_url'] = ''; + if ($isPublicPath) { + $arrPost['download_url'] = $this->backupDownloadPathMySQL; + } + $compressTechnique = $this->globalSettingsData[0]->compress; + $compressTechniques = [ + 'bzip2' => '.bz2', + 'zip' => '', + 'gzip' => '.gz', + 'xz' => '.xz', + ]; + + $compressTechnique = $compressTechniques[$compressTechnique] ?? '.bz2'; + + $path = str_replace("/all", "", $this->backupFilePath); + $backupFileMySQL = $path . '/mysqldump' . '/mysqldump' . '-' . $currentDateTime . '.sql' . $compressTechnique; + $fileSize = $this->convertFilesize(filesize($backupFileMySQL)); $arrPost['size'] = $fileSize; - $arrPost['filenames'] = $this->backupFileMySQL; + + $arrPost['filenames'] = $backupFileMySQL; $this->backupglobalRepository->addBackupData($arrPost); + } // Insert to Database > Backup History - $arrPost['download_url'] = $this->backupDownloadPath; + $arrPost['download_url'] = ''; + if ($isPublicPath) { + $arrPost['download_url'] = $this->backupDownloadPath; + } $arrPost['log'] = $log; + try { $fileSize = $this->convertFilesize(filesize($this->backupFile)); - } catch (Exception $e) { + } catch (\Exception $e) { return [ 'log' => 'error', - 'backup_file' => $this->exceptionMessage, + 'backup_file' => 'Something is wrong here. Please check you Global Settings', ]; } $arrPost['size'] = $fileSize; $arrPost['filenames'] = $this->backupFile; + $downloadURL = ''; + if ($isPublicPath) { + $downloadURL = $this->backupDownloadPath; + } $this->backupglobalRepository->addBackupData($arrPost); - $arrReturn = [ 'log' => $log, 'backup_file' => $this->backupFile, - 'download_url' => $this->backupDownloadPath, + 'download_url' => $downloadURL, ]; } else { $arrReturn = [ 'log' => 'error', 'backup_file' => $this->backupFile, ]; } - return $arrReturn; } @@ -371,18 +426,20 @@ public function generateBackup(array $arrPost): array * @param string $backupName * @param string $backupType * @param string $backupFileName + * @param string|null $rawName * @return string */ - protected function getPhpbuBackup(string $backupName, string $backupType, string $backupFileName): string + protected function getPhpbuBackup(string $backupName, string $backupType, string $backupFileName, ?string $rawName = null): string { - $json = ''; + $json = $ignoreUploads = ''; $json .= ' { - "name": "'.$backupName.'",'; + "name": "' . $backupName . '",'; $backupExtFile = '.tar'; - if ($backupType == 'mysqldump') { - $json .= ' + switch ($backupType) { + case 'mysqldump': + $json .= ' "source": { "type": "mysqldump", "options": { @@ -393,94 +450,100 @@ protected function getPhpbuBackup(string $backupName, string $backupType, string "password": "' . $this->arrDatabase['password'] . '" } },'; - $backupExtFile = '.sql'; - } else { - $targetPath = ($backupType == 'all') ? '' : $backupType; - // Exclude uploads/tx_nsbackup - if ($backupType == 'uploads') { - $ignoreUploads = ',"exclude": "tx_nsbackup"'; - } - if ($backupType == 'all') { - $ignoreUploads = ',"exclude": "uploads/tx_nsbackup,typo3temp"'; - } + $backupExtFile = '.sql'; + break; - $sourcePath = $this->rootPath . '/' . $targetPath; + default: + $targetPath = ($backupType == 'all') ? '' : $backupType; - // In composer-mode, let's figure out vendor folder - if ($backupType == 'vendor' && $this->composerRootPath && strlen($this->composerRootPath) > 0) { - $sourcePath = $this->composerRootPath . '/' . $targetPath; - } + // Exclude uploads/tx_nsbackup + if ($backupType == 'uploads') { + $ignoreUploads = ',"exclude": "tx_nsbackup"'; + } + if ($backupType == 'all') { + $ignoreUploads = ',"exclude": "uploads/tx_nsbackup,typo3temp"'; + } - $ignoreUploads = $ignoreUploads ?? ''; - $json .= ' - "source": { - "type": "tar", - "options": { - "path": "' . $sourcePath . '"' . $ignoreUploads . ' - } - },'; + $sourcePath = $this->rootPath . '/' . $targetPath; + + // In composer-mode, let's figure out vendor folder + if (($backupType == 'vendor') && ($this->composerRootPath !== null && strlen($this->composerRootPath) > 0)) { + $sourcePath = $this->composerRootPath . '/' . $targetPath; + } + + if ($backupType == 'other') { + $json .= ' + "source": { + "type": "tar", + "options": { + "path": "' . $rawName . '"' . $ignoreUploads . ' + } + },'; + } else { + $json .= ' + "source": { + "type": "tar", + "options": { + "path": "' . $sourcePath . '"' . $ignoreUploads . ' + } + },'; + } } // PATCH If compress=bzip2 $compressTechnique = $this->globalSettingsData[0]->compress; - switch ($compressTechnique) { - case 'bzip2': - case '': - $compressTechnique = '.bz2'; - break; - case 'zip': - $compressTechnique = ''; - break; - case 'gzip': - $compressTechnique = '.gz'; - break; - case 'xz': - $compressTechnique = '.xz'; - break; - default: - break; - } + $compressTechniques = [ + 'bzip2' => '.bz2', + 'zip' => '', + 'gzip' => '.gz', + 'xz' => '.xz', + ]; + + $compressTechnique = $compressTechniques[$compressTechnique] ?? '.bz2'; + //echo $compressTechnique;exit; + + $this->backupFilePath = $this->localStoragePath . $backupType; - $this->backupFilePath = $this->localStoragePath.$backupType; - $this->backupFileName = $backupFileName.$backupExtFile; // Physical file - $this->backupFile = $this->backupFilePath . '/' . $backupType.'-'.date('Ymd-Hi') . $backupExtFile . $compressTechnique; + $this->backupFile = $this->backupFilePath . '/' . md5($backupType).'-'.date('Ymd-Hi') . $backupExtFile . $compressTechnique; // Download file $this->backupDownloadPath = $this->baseURL . $backupType . '/' . - $backupType.'-'.date('Ymd-Hi') . $backupExtFile . $compressTechnique; + md5($backupType) . '-' . date('Ymd-Hi') . $backupExtFile . $compressTechnique; // If Backup Type = ALL then, Let's consider mysql as special-case if ($backupType == 'mysqldump') { - $compressTechnique = $this->globalSettingsData[0]->compress == 'zip' ? '' : $compressTechnique; - $fileName = 'mysqldump' . '-' . date('Ymd-Hi') . $backupExtFile . $compressTechnique; - $this->backupFileMySQL = $this->backupFilePath . '/' . $fileName; - $this->backupDownloadPathMySQL = $this->baseURL . $backupType . '/' . $fileName; + if ($this->globalSettingsData[0]->compress == 'zip') { + $compressTechnique = ''; + } + $this->backupFileMySQL = $this->backupFilePath . '/' . md5($backupType) . '-' . date('Ymd-Hi') . $backupExtFile . $compressTechnique; + $this->backupDownloadPathMySQL = + $this->baseURL . $this->backupFileMySQL = + $backupType . '/' . md5($backupType) . '-' . date('Ymd-Hi') . $backupExtFile . $compressTechnique; } - - $this->backupFileName = $backupType.'-%Y%m%d-%H%i' . $backupExtFile; - + $this->backupFileName = md5($backupType) . '-%Y%m%d-%H%i' . $backupExtFile; $json .= ' "target": { - "dirname": "'.$this->backupFilePath.'", - "filename": "'.$this->backupFileName.'", - "compress": "'.$this->globalSettingsData[0]->compress.'" + "dirname": "' . $this->backupFilePath . '", + "filename": "' . $this->backupFileName . '", + "compress": "' . $this->globalSettingsData[0]->compress . '" },'; $json .= ' "cleanup": { - "type": "'.$this->globalSettingsData[0]->cleanup.'", + "type": "' . $this->globalSettingsData[0]->cleanup . '", "options": { - "amount": '.$this->globalSettingsData[0]->cleanupQuantity.' + "amount": "' . $this->globalSettingsData[0]->cleanupQuantity . '" } } } '; return $json; } + /** * Convert File Size * @param mixed $bytes @@ -503,4 +566,18 @@ protected function convertFilesize(mixed $bytes): string } return $bytes; } + + /** + * @param string $path + * @return boolean + */ + public function isPathPublic(string $path): bool + { + if (!Environment::isComposerMode()) { + $valuesToCheck = ['typo3', 'typo3conf', 'vendor', 'typo3temp', 'bin']; + $parts = array_filter(explode('/', rtrim($path, '/'))); + return empty(array_intersect($parts, $valuesToCheck)); + } + return str_contains(rtrim($path, '/'), Environment::getPublicPath()); + } }
Classes/Controller/BackupglobalController.php+33 −13 modified@@ -12,7 +12,8 @@ use TYPO3\CMS\Backend\Template\ModuleTemplateFactory; use TYPO3\CMS\Extbase\Mvc\Controller\ActionController; use NITSAN\NsBackup\Domain\Repository\BackupglobalRepository; -use TYPO3\CMS\Extbase\Utility\LocalizationUtility as transalte; +use TYPO3\CMS\Core\Core\Environment; +use TYPO3\CMS\Extbase\Utility\LocalizationUtility; use TYPO3\CMS\Extbase\Persistence\Exception\UnknownObjectException; use TYPO3\CMS\Extbase\Persistence\Exception\IllegalObjectTypeException; @@ -66,24 +67,20 @@ public function initializeView(): void */ public function globalsettingAction(): ResponseInterface { - if(!empty($this->errorValidation)) { - $header = transalte::translate('global.errorvalidation', 'ns_backup'); - $message = transalte::translate('global.errorvalidation.message', 'ns_backup'); - $this->addFlashMessage($message, $header, ContextualFeedbackSeverity::ERROR); - } - $pageRenderer = GeneralUtility::makeInstance(className: PageRenderer::class); $pageRenderer->loadJavaScriptModule('@nitsan/ns-backup/jquery.js'); $pageRenderer->loadJavaScriptModule('@nitsan/ns-backup/Main.js'); $view = $this->initializeModuleTemplate($this->request); $globalSettingsData = $this->backupglobalRepository->findAll(); + $varPath = Environment::getVarPath(); $view->assignMultiple([ 'cleanup' => constant('cleanup'), 'backupglobal' => $globalSettingsData[0], 'compress' => constant('compress'), 'action' => 'globalsetting', 'errorValidation' => $this->errorValidation, - 'modalAttr' => 'data-bs-' + 'modalAttr' => 'data-bs-', + 'varPath' => $varPath ]); return $view->renderResponse('Backupglobal/Globalsetting'); } @@ -100,13 +97,24 @@ public function createAction(Backupglobal $backupglobal): ResponseInterface $emails = GeneralUtility::trimExplode(',', $backupglobal->getEmails()); foreach ($emails as $email) { if(!GeneralUtility::validEmail($email)) { - $msg = transalte::translate('email.not.valid', 'ns_backup'); + $msg = LocalizationUtility::translate('email.not.valid', 'ns_backup'); $this->addFlashMessage('', $msg); return $this->redirect('globalsetting', ContextualFeedbackSeverity::ERROR); } } - - $msg = transalte::translate('globalsettings.create', 'ns_backup'); + if (!is_dir($backupglobal->getBackupStorePath())) { + $msg = LocalizationUtility::translate('storePath.not.valid', 'ns_backup'); + $this->addFlashMessage('', $msg, ContextualFeedbackSeverity::ERROR); + return $this->redirect('globalsetting'); + } + $phpPath = trim($backupglobal->getPhp()); + $backupglobal->setPhp($phpPath); + if (!is_executable($backupglobal->getPhp())) { + $msg = LocalizationUtility::translate('phpPath.not.valid', 'ns_backup'); + $this->addFlashMessage('', $msg, ContextualFeedbackSeverity::ERROR); + return $this->redirect('globalsetting'); + } + $msg = LocalizationUtility::translate('globalsettings.create', 'ns_backup'); $this->addFlashMessage('', $msg); $this->backupglobalRepository->add($backupglobal); @@ -126,12 +134,24 @@ public function updateAction(Backupglobal $backupglobal): ResponseInterface $emails = GeneralUtility::trimExplode(',', $backupglobal->getEmails()); foreach ($emails as $email) { if(!GeneralUtility::validEmail($email)) { - $msg = transalte::translate('email.not.valid', 'ns_backup'); + $msg = LocalizationUtility::translate('email.not.valid', 'ns_backup'); $this->addFlashMessage('', $msg); return $this->redirect('globalsetting', ContextualFeedbackSeverity::ERROR); } } - $msg = transalte::translate('globalsettings.update', 'ns_backup'); + if (!is_dir($backupglobal->getBackupStorePath())) { + $msg = LocalizationUtility::translate('storePath.not.valid', 'ns_backup'); + $this->addFlashMessage('', $msg, ContextualFeedbackSeverity::ERROR); + return $this->redirect('globalsetting'); + } + $phpPath = trim($backupglobal->getPhp()); + $backupglobal->setPhp($phpPath); + if (!is_executable($backupglobal->getPhp())) { + $msg = LocalizationUtility::translate('phpPath.not.valid', 'ns_backup'); + $this->addFlashMessage('', $msg, ContextualFeedbackSeverity::ERROR); + return $this->redirect('globalsetting'); + } + $msg = LocalizationUtility::translate('globalsettings.update', 'ns_backup'); $this->addFlashMessage('', $msg); $this->backupglobalRepository->update($backupglobal);
Classes/Controller/BackupsController.php+50 −4 modified@@ -109,6 +109,11 @@ public function backuprestoreAction(): ResponseInterface $pageRenderer->loadJavaScriptModule('@nitsan/ns-backup/Main.js'); $globalSettingsData = $this->backupglobalRepository->findAll(); + // Get Local Storage Path + if ($globalSettingsData[0]) { + $globalBackupStorePath = $globalSettingsData[0]->getBackupStorePath(); + $isPublicPath = $this->isPathPublic($globalBackupStorePath); + } $arrMultipleVars = [ 'cleanup' => constant('cleanup'), 'backuptype' => constant('backuptype'), @@ -119,6 +124,19 @@ public function backuprestoreAction(): ResponseInterface ]; $arrPost = $this->request->getArguments(); + $backupName = trim($arrPost['backuprestore']['backupName'] ?? ''); + if (preg_match('/[^0-9A-Za-z _-]/', $backupName)) { + $sanitizedName = htmlspecialchars($backupName, ENT_QUOTES, 'UTF-8'); + + $this->addFlashMessage( + "Invalid backup name: '{$sanitizedName}'. " . transalte::translate('manualbackup.error.description', 'ns_backup'), + transalte::translate('manualbackup.error', 'ns_backup'), + ContextualFeedbackSeverity::ERROR + ); + + return $this->redirect('backuprestore'); + } + // "RUN" Backup from "Manual Backup Module" $arrPost = $arrPost['backuprestore'] ?? ''; @@ -137,7 +155,7 @@ public function backuprestoreAction(): ResponseInterface $mesHeader = transalte::translate('manualbackup.success', 'ns_backup'); $backup_file = transalte::translate('backup.downloaded', 'ns_backup').' '.$arrResponse['backup_file']; $this->addFlashMessage($backup_file, $mesHeader); - + $response = (array) json_decode($arrResponse['log']); if (isset($response['errorCount']) && $response['errorCount'] > 0) { $globalSettingsData = $this->backupglobalRepository->findAll(); @@ -160,7 +178,10 @@ public function backuprestoreAction(): ResponseInterface // Pass to Fluid $arrMultipleVars['isManualBackup'] = '1'; $arrMultipleVars['log'] = '<pre class="pre-scrollable"><code class="json">'. json_encode(json_decode($arrResponse['log']), JSON_PRETTY_PRINT) .'</code></pre>'; - $arrMultipleVars['download_url'] = $arrResponse['download_url']; + $arrMultipleVars['download_url'] = ''; + if ($isPublicPath) { + $arrMultipleVars['download_url'] = $arrResponse['download_url']; + } } } @@ -194,6 +215,15 @@ public function deletebackupbackupAction(): ResponseInterface $request = $this->request->getQueryParams(); $uid = $request['uid']; + $globalSettingsData = $this->backupglobalRepository->findAll(); + $globalSettingsData = !empty($globalSettingsData[0]) ? $globalSettingsData[0] : null; + + if (!$globalSettingsData) { + $headerMsg = transalte::translate('something.wrong.here', 'ns_backup'); + $this->addFlashMessage($headerMsg, '', ContextualFeedbackSeverity::ERROR, true); + die; + } + $arrBackup = $this->backupglobalRepository->findBackupByUid($uid); // Let's delete it $this->backupglobalRepository->removeBackupData($uid); @@ -203,11 +233,13 @@ public function deletebackupbackupAction(): ResponseInterface unlink($arrBackup['filenames']); } - $rootPath = $this->globalSettingsData[0]->root ?? (Environment::getProjectPath() ?? ''); + $rootPath = $globalSettingsData->root ?? (Environment::getProjectPath() ?? ''); if(Environment::isComposerMode()) { $rootPath = Environment::getPublicPath(); } - $jsonFolder = $rootPath.'/uploads/tx_nsbackup/json/'; + + $rootPath = $globalSettingsData->backupStorePath ?? ($rootPath . '/uploads'); + $jsonFolder = $rootPath.'/tx_nsbackup/json/'; if(file_exists($jsonFolder.$arrBackup['jsonfile'])) { unlink($jsonFolder.$arrBackup['jsonfile']); } @@ -231,4 +263,18 @@ protected function initializeModuleTemplate( ): ModuleTemplate { return $this->moduleTemplateFactory->create($request); } + + /** + * @param string $path + * @return boolean + */ + public function isPathPublic(string $path): bool + { + if (!Environment::isComposerMode()) { + $valuesToCheck = ['typo3', 'typo3conf', 'vendor', 'typo3temp', 'bin']; + $parts = array_filter(explode('/', rtrim($path, '/'))); + return empty(array_intersect($parts, $valuesToCheck)); + } + return str_contains(rtrim($path, '/'), Environment::getPublicPath()); + } }
Classes/Domain/Model/Backupglobal.php+28 −0 modified@@ -27,6 +27,13 @@ class Backupglobal extends AbstractEntity */ public string $emails = ''; + /** + * backupStorePath + * + * @var string + */ + public string $backupStorePath = ''; + /** * emailFrom * @@ -301,4 +308,25 @@ public function setCleanup(string $cleanup): void { $this->cleanup = $cleanup; } + + /** + * Returns the backupStorePath + * + * @return string backupStorePath + */ + public function getBackupStorePath(): string + { + return $this->backupStorePath; + } + + /** + * Sets the backupStorePath + * + * @param string $backupStorePath + * @return void + */ + public function setBackupStorePath(string $backupStorePath): void + { + $this->backupStorePath = $backupStorePath; + } }
Configuration/Backend/Modules.php+2 −2 modified@@ -7,8 +7,8 @@ 'nitsan_nsbackup' => [ 'parent' => 'tools', 'position' => ['before' => 'top'], - 'access' => 'user', - 'path' => '/module/nitsan/NsBackupBackup ', + 'access' => 'admin', + 'path' => '/module/nitsan/NsBackupBackup', 'icon' => 'EXT:ns_backup/Resources/Public/Icons/module-nsbackup.svg', 'labels' => 'LLL:EXT:ns_backup/Resources/Private/Language/locallang_backup.xlf', 'extensionName' => 'NsBackup',
Configuration/TCA/tx_nsbackup_domain_model_backupglobal.php+9 −0 modified@@ -176,5 +176,14 @@ 'eval' => 'trim' ], ], + 'backup_store_path' => [ + 'exclude' => true, + 'label' => 'LLL:EXT:ns_backup/Resources/Private/Language/locallang_db.xlf:tx_nsbackup_domain_model_backupglobal.backup_store_path', + 'config' => [ + 'type' => 'input', + 'size' => 30, + 'eval' => 'trim' + ], + ], ], ];
ext_emconf.php+2 −2 modified@@ -6,13 +6,13 @@ *** Live Demo: https://demo.t3planet.com/t3-extensions/backup *** Premium Version, Documentation & Free Support: https://t3planet.com/typo3-backup-extension', 'category' => 'module', - 'author' => 'T3: Rohan Parmar, T3: Divya Goklani, T3: Nilesh Malankiya, QA: Gautam Kunjadiya', + 'author' => 'T3: Rohan Parmar, T3: Divya Goklani, T3: Nilesh Malankiya, QA: Krishna Dhapa', 'author_email' => 'sanjay@nitsan.in', 'author_company' => 'T3Planet // NITSAN', 'state' => 'stable', 'uploadfolder' => 1, 'createDirs' => '', - 'version' => '13.0.0', + 'version' => '13.0.1', 'constraints' => [ 'depends' => [ 'typo3' => '12.0.0-13.9.99',
ext_tables.sql+1 −0 modified@@ -34,6 +34,7 @@ CREATE TABLE tx_nsbackup_domain_model_backupglobal ( sys_language_uid int(11) DEFAULT '0' NOT NULL, l10n_parent int(11) DEFAULT '0' NOT NULL, l10n_diffsource mediumblob, + backup_store_path varchar(255) DEFAULT '', l10n_state text, PRIMARY KEY (uid), KEY parent (pid),
Resources/Private/Language/locallang_db.xlf+3 −0 modified@@ -82,6 +82,9 @@ <trans-unit id="tx_nsbackup_domain_model_backupdata.status"> <source>Status</source> </trans-unit> + <trans-unit id="tx_nsbackup_domain_model_backupglobal.backup_store_path"> + <source>Backup Store Path</source> + </trans-unit> </body> </file> </xliff>
Resources/Private/Language/locallang.xlf+24 −0 modified@@ -193,6 +193,12 @@ <trans-unit id="globalsettings.form.cleanup_quantity.desc"> <source>Enter at how many backups clean or remove backups at your web-server and remote servers/cloud too, Min:1 And Max:500 allow</source> </trans-unit> + <trans-unit id="globalsettings.form.backup_store_path"> + <source>Backup Store Path</source> + </trans-unit> + <trans-unit id="globalsettings.form.backup_store_path.desc"> + <source>Please enter a path to store the backup (e.g., /var/www/html/public/, /var/www/html/). We recommend using a protected path to restrict external access.</source> + </trans-unit> <trans-unit id="globalsettings.form.servercleanup"> <source>Server Cleanups?</source> </trans-unit> @@ -445,9 +451,27 @@ <trans-unit id="email.not.valid"> <source>Email format is not valid</source> </trans-unit> + <trans-unit id="phpPath.not.valid"> + <source>PHP path is not executable</source> + </trans-unit> + <trans-unit id="storePath.not.valid"> + <source>The backup storage does not exist.</source> + </trans-unit> <trans-unit id="something.wrong.here"> <source>Something is wrong here. Please check you Global Settings</source> </trans-unit> + <trans-unit id="servercloud.content.downloadBackupError"> + <source>This backup contains the private path. Due to security purposes, it is not able to download. You can download it manually from your server.</source> + </trans-unit> + <trans-unit id="servercloud.content.downloadErrorTitle"> + <source>Download Backup</source> + </trans-unit> + <trans-unit id="manualbackup.error.description"> + <source>Allowed characters: letters, numbers, spaces, dashes (-), and underscores (_).</source> + </trans-unit> + <trans-unit id="servercloud.content.downloadBackupNotFound"> + <source>This backup is no longer available.</source> + </trans-unit> </body> </file> </xliff>
Resources/Private/Layouts/Default.html+43 −1 modified@@ -50,7 +50,7 @@ <f:flashMessages /> <f:if condition="{errorValidation}"> <div class="alert alert-warning"> - <f:format.raw>{errorValidation}</f:format.raw> + <f:sanitize.html>{errorValidation}</f:sanitize.html> </div> </f:if> <f:render section="content" /> @@ -203,5 +203,47 @@ <h5 class="modal-title">Download Backup</h5> </div> </div> + <!-- Private Backup Download Error --> + <div class="modal fade" id="backupDownloadError" role="dialog" + aria-labelledby="nsBackupDeleteRecordModal" aria-hidden="true"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title"><f:translate key="servercloud.content.downloadErrorTitle" /> <span class="backup-title"></span></h5> + <button type="button" class="close" {modalAttr}dismiss="modal" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + <div class="modal-body"> + <p class="delete-msg"><f:translate key="servercloud.content.downloadBackupError" /></p> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-warning" {modalAttr}dismiss="modal"><em class="fa fa-close" + aria-hidden="true"></em><f:translate key="servercloud.content.cancel" /></button> + </div> + </div> + </div> + </div> + <!-- Private Backup Download Error --> + <div class="modal fade" id="backupNotAvailable" tabindex="-1" role="dialog" + aria-labelledby="nsBackupNotAvailableModal" aria-hidden="true"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"> + <h5 class="modal-title"><f:translate key="servercloud.content.downloadErrorTitle" /> <span class="backup-title"></span></h5> + <button type="button" class="close" {modalAttr}dismiss="modal" aria-label="Close"> + <span aria-hidden="true">×</span> + </button> + </div> + <div class="modal-body"> + <p class="delete-msg"><f:translate key="servercloud.content.downloadBackupNotFound" /></p> + </div> + <div class="modal-footer"> + <button type="button" class="btn btn-warning" {modalAttr}dismiss="modal"><em class="fa fa-close" + aria-hidden="true"></em><f:translate key="servercloud.content.cancel" /></button> + </div> + </div> + </div> + </div> </html>
Resources/Private/Partials/GlobalSetting/Globalform.html+26 −16 modified@@ -35,21 +35,6 @@ </div> </div> </div> -<div class="form-group"> - <div class="row"> - <div class="col-md-3"> - <label><f:translate key="globalsettings.form.notification" /></label> - </div> - <div class="col-md-6"> - <div class="form-check"> - <f:form.checkbox name="emailNotificationOnError" property="emailNotificationOnError" value="1" class="form-check-input check" checked="{f:if(condition: '{backupglobal.emailNotificationOnError}==1', then: 'checked')}" id="emailNotificationOnError" /> - <label class="form-check-label" for="emailNotificationOnError"> - <f:translate key="globalsettings.form.emailNotificationOnError" /> - </label> - </div> - </div> - </div> -</div> <div class="form-group"> <div class="row"> @@ -85,6 +70,31 @@ </div> </div> </div> +<div class="form-group"> + <div class="row"> + <div class="col-md-3"> + <div class="d-flex justify-content-between"> + <label for="backupStorePath"><f:translate key="globalsettings.form.backup_store_path" /></label> + </div> + </div> + <div class="col-md-6"> + <div class="input-group"> + <span class="input-group-text custom-reset" data-id="backupStorePath"> + <i aria-hidden="true" class="fa fa-repeat"></i> + </span> + <f:variable name="backupStorePathValue">{f:if(condition: backupglobal.backupStorePath, then: '{backupglobal.backupStorePath}', else: '{varPath}')}</f:variable> + <f:form.textfield name="backupStorePath" + type="input" property="backupStorePath" + class="form-control" id="backupStorePath" value="{backupStorePathValue}"/> + </div> + <div class="field-info-text"> + <p class="form-control-field"><f:translate key="globalsettings.form.backup_store_path.desc" /></p> + </div> + <p class="backupStorePath-error error" style="display: none;"><f:translate key="servercloud.nodata" /></p> + </div> + </div> +</div> + <div class="form-group"> <div class="row"> <div class="col-md-3"> @@ -128,4 +138,4 @@ aria-hidden="true"></em><f:translate key="globalsettings.form.savesettings" /></button> </div> </div> -</div> \ No newline at end of file +</div>
Resources/Private/Templates/Backups/Backuprestore.html+40 −17 modified@@ -4,11 +4,21 @@ <f:section name="content"> <f:flashMessages /> <f:if condition="{isManualBackup} == '1'"> - <a href="{download_url}" target="_blank" class="btn btn-success btn-backupnow"><em class="fa fa-download" aria-hidden="true"></em><f:translate key="download.backup.now" extensionName = "NsBackup" /></a> + <f:if condition="{download_url}==''"> + <f:then> + <a href="javascript:;" class="backup-download-btn btn btn-primary btn-sm" {modalAttr}toggle="modal" + {modalAttr}target="#backupDownloadError" data-title="{backup.title}" data-msg="<f:translate key='servercloud.content.deleterecord' />" data-id="{backup.uid}"> + <em class="fa fa-cloud-download" aria-hidden="true"></em><f:translate key="download.backup" /> + </a> + </f:then> + <f:else> + <a href="{download_url}" target="_blank" class="btn btn-success btn-backupnow"><em class="fa fa-download" aria-hidden="true"></em><f:translate key="download.backup.now" extensionName = "NsBackup" /></a> + </f:else> + </f:if> <p> </p> <div class="alert alert-secondary json-data"> <p><strong><f:translate key="logs" extensionName = "NsBackup" />:</strong></p> - <f:format.raw><p>{log}</p></f:format.raw> + <f:sanitize.html><p>{log}</p></f:sanitize.html> </div> </f:if> @@ -46,27 +56,40 @@ <h5><f:translate key="menu.backupsrestore" extensionName = "NsBackup" /></h5> </f:for> </td> <td>{backup.size}</td> - <td> - <div class="button-group ns-backup-actions-wrap"> - <f:if condition="{backup.isDownload}"> + <td class="text-center"> + <div class="button-group ns-backup-actions-wrap ns-ext-actions-wrap"> + <f:if condition="{backup.download_url}==''"> <f:then> - <a target="_blank" href="{backup.download_url}" class="btn btn-primary"><em class="fa fa-cloud-download" aria-hidden="true"></em><f:translate key="download.backup" extensionName = "NsBackup" /></a> + <a href="javascript:;" class="backup-download-btn btn btn-primary btn-sm" {modalAttr}toggle="modal" + {modalAttr}target="#backupDownloadError" data-title="{backup.title}" data-msg="<f:translate key='servercloud.content.deleterecord' />" data-id="{backup.uid}"> + <em class="fa fa-cloud-download" aria-hidden="true"></em><f:translate key="download.backup" /> + </a> </f:then> <f:else> - <a href="javascript:;" class="btn btn-primary btn-sm" {modalAttr}toggle="modal" - {modalAttr}target="#downloadBackup"><em class="fa fa-cloud-download" aria-hidden="true"></em><f:translate key="download.backup" extensionName = "NsBackup" /></a> + <f:if condition="!{backup.isDownload}"> + <f:then> + <a href="javascript:;" class="backup-download-btn btn btn-primary btn-sm" {modalAttr}toggle="modal" + {modalAttr}target="#backupNotAvailable" data-title="{backup.title}" data-msg="<f:translate key='servercloud.content.deleterecord' />" data-id="{backup.uid}"> + <em class="fa fa-cloud-download" aria-hidden="true"></em><f:translate key="download.backup" /> + </a> + </f:then> + <f:else> + <a target="_blank" href="{backup.download_url}" class="backup-download-btn btn btn-primary btn-sm"><em class="fa fa-cloud-download" aria-hidden="true"></em><f:translate key="download.backup" /></a> + </f:else> + </f:if> + </f:else> </f:if> <a href="javascript:;" class="btn btn-success btn-sm btn-log-show" {modalAttr}toggle="modal" - {modalAttr}target="#logBackup" data-id="{backup.uid}"><em class="fa fa-file-text-o" aria-hidden="true"></em>Logs</a> - <a href="javascript:;" class="btn btn-danger btn-sm delete-backup" {modalAttr}toggle="modal" - {modalAttr}target="#nsBackupDeletebackupModal" data-title="{backup.title}" data-msg="<f:translate key='servercloud.content.deleterecord' extensionName = 'NsBackup' />" data-id="{backup.uid}"> - <em class="fa fa-trash-o" aria-hidden="true"></em><f:translate key="servercloud.content.delete" extensionName = "NsBackup" /> - </a> - </div> - <div style="display: none;" id="logDiv_{backup.uid}"> - <f:format.raw>{backup.logs}</f:format.raw> - </div> + {modalAttr}target="#logBackup" data-id="{backup.uid}"><em class="fa fa-file-text-o" aria-hidden="true"></em>Logs</a> + <a href="javascript:;" class="btn btn-danger btn-sm delete-backup" {modalAttr}toggle="modal" + {modalAttr}target="#nsBackupDeletebackupModal" data-title="{backup.title}" data-msg="<f:translate key='servercloud.content.deleterecord' />" data-id="{backup.uid}"> + <em class="fa fa-trash-o" aria-hidden="true"></em><f:translate key="servercloud.content.delete" /> + </a> + </div> + <div style="display: none;" id="logDiv_{backup.uid}"> + <f:sanitize.html>{backup.logs}</f:sanitize.html> + </div> </td> </tr> </f:for>
Resources/Public/Css/main.css+4 −0 modified@@ -1149,4 +1149,8 @@ div.dt-container .dt-paging .dt-paging-button.disabled:active { font-size: 14px; border-color: light-dark(#c2c2c2, #464646); box-shadow: none; +} + +.close { + font-size: 22px; } \ No newline at end of file
Resources/Public/JavaScript/Main.js+17 −6 modified@@ -97,25 +97,26 @@ $(document).ready(function() { return isError !== 1; }); - // Remove Backup Data + // Remove Backup Data (XSS-Safe) $('.delete-backup').on('click', function () { const title = $(this).data('title'); const id = $(this).data('id'); const msg = $(this).data('msg'); - $("#nsBackupDeletebackupModal .backup-title").html(title); - $("#nsBackupDeletebackupModal .delete-msg").html(msg); + + $("#nsBackupDeletebackupModal .backup-title").text(title); + $("#nsBackupDeletebackupModal .delete-msg").text(msg); + $("#nsBackupDeletebackupModal .delete-backup-id").val(id); $("#nsBackupDeletebackupModal .deletetype").val('single'); $("#nsBackupDeletebackupModal .delete-backup-backup-del").removeAttr("disabled"); }); $('.paginate_button').on('click', function () { - console.log("hello") const title = $('.delete-backup').data('title'); const id = $('.delete-backup').data('id'); const msg = $('.delete-backup').data('msg'); - $("#nsBackupDeletebackupModal .backup-title").html(title); - $("#nsBackupDeletebackupModal .delete-msg").html(msg); + $("#nsBackupDeletebackupModal .backup-title").text(title); + $("#nsBackupDeletebackupModal .delete-msg").text(msg); $("#nsBackupDeletebackupModal .delete-backup-id").val(id); $("#nsBackupDeletebackupModal .deletetype").val('single'); $("#nsBackupDeletebackupModal .delete-backup-backup-del").removeAttr("disabled"); @@ -146,6 +147,16 @@ $(document).ready(function() { // Code Highlight // hljs.initHighlightingOnLoad(); + $('.custom-reset').on('click', function () { + var that = $(this); + var hideId = this.dataset.id; + that.find('i').addClass('fa-spin'); + $('#' + hideId).val(''); + setTimeout(function () { + that.find('i').removeClass('fa-spin'); + }, 2000); + }); + $('.ns-backup-datatable').DataTable({ language: { zeroRecords: "Nothing found - sorry",
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-hq4f-5qjv-fwrgghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-48201ghsaADVISORY
- github.com/FriendsOfPHP/security-advisories/blob/master/nitsan/ns-backup/CVE-2025-48201.yamlghsaWEB
- github.com/nitsan-technologies/ns_backup/commit/67b8102a19e8e516dc4228f5c42f9e4fba5046cbghsaWEB
- typo3.org/security/advisory/typo3-ext-sa-2025-007nvdWEB
News mentions
0No linked articles in our index yet.