Skip to content

Commit 67b8102

Browse files
[TASK] Security fix
1 parent 52511d0 commit 67b8102

File tree

15 files changed

+455
-157
lines changed

15 files changed

+455
-157
lines changed

Classes/Controller/BackupBaseController.php

Lines changed: 173 additions & 96 deletions
Large diffs are not rendered by default.

Classes/Controller/BackupglobalController.php

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
1313
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
1414
use NITSAN\NsBackup\Domain\Repository\BackupglobalRepository;
15-
use TYPO3\CMS\Extbase\Utility\LocalizationUtility as transalte;
15+
use TYPO3\CMS\Core\Core\Environment;
16+
use TYPO3\CMS\Extbase\Utility\LocalizationUtility;
1617
use TYPO3\CMS\Extbase\Persistence\Exception\UnknownObjectException;
1718
use TYPO3\CMS\Extbase\Persistence\Exception\IllegalObjectTypeException;
1819

@@ -66,24 +67,20 @@ public function initializeView(): void
6667
*/
6768
public function globalsettingAction(): ResponseInterface
6869
{
69-
if(!empty($this->errorValidation)) {
70-
$header = transalte::translate('global.errorvalidation', 'ns_backup');
71-
$message = transalte::translate('global.errorvalidation.message', 'ns_backup');
72-
$this->addFlashMessage($message, $header, ContextualFeedbackSeverity::ERROR);
73-
}
74-
7570
$pageRenderer = GeneralUtility::makeInstance(className: PageRenderer::class);
7671
$pageRenderer->loadJavaScriptModule('@nitsan/ns-backup/jquery.js');
7772
$pageRenderer->loadJavaScriptModule('@nitsan/ns-backup/Main.js');
7873
$view = $this->initializeModuleTemplate($this->request);
7974
$globalSettingsData = $this->backupglobalRepository->findAll();
75+
$varPath = Environment::getVarPath();
8076
$view->assignMultiple([
8177
'cleanup' => constant('cleanup'),
8278
'backupglobal' => $globalSettingsData[0],
8379
'compress' => constant('compress'),
8480
'action' => 'globalsetting',
8581
'errorValidation' => $this->errorValidation,
86-
'modalAttr' => 'data-bs-'
82+
'modalAttr' => 'data-bs-',
83+
'varPath' => $varPath
8784
]);
8885
return $view->renderResponse('Backupglobal/Globalsetting');
8986
}
@@ -100,13 +97,24 @@ public function createAction(Backupglobal $backupglobal): ResponseInterface
10097
$emails = GeneralUtility::trimExplode(',', $backupglobal->getEmails());
10198
foreach ($emails as $email) {
10299
if(!GeneralUtility::validEmail($email)) {
103-
$msg = transalte::translate('email.not.valid', 'ns_backup');
100+
$msg = LocalizationUtility::translate('email.not.valid', 'ns_backup');
104101
$this->addFlashMessage('', $msg);
105102
return $this->redirect('globalsetting', ContextualFeedbackSeverity::ERROR);
106103
}
107104
}
108-
109-
$msg = transalte::translate('globalsettings.create', 'ns_backup');
105+
if (!is_dir($backupglobal->getBackupStorePath())) {
106+
$msg = LocalizationUtility::translate('storePath.not.valid', 'ns_backup');
107+
$this->addFlashMessage('', $msg, ContextualFeedbackSeverity::ERROR);
108+
return $this->redirect('globalsetting');
109+
}
110+
$phpPath = trim($backupglobal->getPhp());
111+
$backupglobal->setPhp($phpPath);
112+
if (!is_executable($backupglobal->getPhp())) {
113+
$msg = LocalizationUtility::translate('phpPath.not.valid', 'ns_backup');
114+
$this->addFlashMessage('', $msg, ContextualFeedbackSeverity::ERROR);
115+
return $this->redirect('globalsetting');
116+
}
117+
$msg = LocalizationUtility::translate('globalsettings.create', 'ns_backup');
110118
$this->addFlashMessage('', $msg);
111119
$this->backupglobalRepository->add($backupglobal);
112120

@@ -126,12 +134,24 @@ public function updateAction(Backupglobal $backupglobal): ResponseInterface
126134
$emails = GeneralUtility::trimExplode(',', $backupglobal->getEmails());
127135
foreach ($emails as $email) {
128136
if(!GeneralUtility::validEmail($email)) {
129-
$msg = transalte::translate('email.not.valid', 'ns_backup');
137+
$msg = LocalizationUtility::translate('email.not.valid', 'ns_backup');
130138
$this->addFlashMessage('', $msg);
131139
return $this->redirect('globalsetting', ContextualFeedbackSeverity::ERROR);
132140
}
133141
}
134-
$msg = transalte::translate('globalsettings.update', 'ns_backup');
142+
if (!is_dir($backupglobal->getBackupStorePath())) {
143+
$msg = LocalizationUtility::translate('storePath.not.valid', 'ns_backup');
144+
$this->addFlashMessage('', $msg, ContextualFeedbackSeverity::ERROR);
145+
return $this->redirect('globalsetting');
146+
}
147+
$phpPath = trim($backupglobal->getPhp());
148+
$backupglobal->setPhp($phpPath);
149+
if (!is_executable($backupglobal->getPhp())) {
150+
$msg = LocalizationUtility::translate('phpPath.not.valid', 'ns_backup');
151+
$this->addFlashMessage('', $msg, ContextualFeedbackSeverity::ERROR);
152+
return $this->redirect('globalsetting');
153+
}
154+
$msg = LocalizationUtility::translate('globalsettings.update', 'ns_backup');
135155
$this->addFlashMessage('', $msg);
136156
$this->backupglobalRepository->update($backupglobal);
137157

Classes/Controller/BackupsController.php

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,11 @@ public function backuprestoreAction(): ResponseInterface
109109
$pageRenderer->loadJavaScriptModule('@nitsan/ns-backup/Main.js');
110110

111111
$globalSettingsData = $this->backupglobalRepository->findAll();
112+
// Get Local Storage Path
113+
if ($globalSettingsData[0]) {
114+
$globalBackupStorePath = $globalSettingsData[0]->getBackupStorePath();
115+
$isPublicPath = $this->isPathPublic($globalBackupStorePath);
116+
}
112117
$arrMultipleVars = [
113118
'cleanup' => constant('cleanup'),
114119
'backuptype' => constant('backuptype'),
@@ -119,6 +124,19 @@ public function backuprestoreAction(): ResponseInterface
119124
];
120125

121126
$arrPost = $this->request->getArguments();
127+
$backupName = trim($arrPost['backuprestore']['backupName'] ?? '');
128+
if (preg_match('/[^0-9A-Za-z _-]/', $backupName)) {
129+
$sanitizedName = htmlspecialchars($backupName, ENT_QUOTES, 'UTF-8');
130+
131+
$this->addFlashMessage(
132+
"Invalid backup name: '{$sanitizedName}'. " . transalte::translate('manualbackup.error.description', 'ns_backup'),
133+
transalte::translate('manualbackup.error', 'ns_backup'),
134+
ContextualFeedbackSeverity::ERROR
135+
);
136+
137+
return $this->redirect('backuprestore');
138+
}
139+
122140
// "RUN" Backup from "Manual Backup Module"
123141
$arrPost = $arrPost['backuprestore'] ?? '';
124142

@@ -137,7 +155,7 @@ public function backuprestoreAction(): ResponseInterface
137155
$mesHeader = transalte::translate('manualbackup.success', 'ns_backup');
138156
$backup_file = transalte::translate('backup.downloaded', 'ns_backup').' '.$arrResponse['backup_file'];
139157
$this->addFlashMessage($backup_file, $mesHeader);
140-
158+
141159
$response = (array) json_decode($arrResponse['log']);
142160
if (isset($response['errorCount']) && $response['errorCount'] > 0) {
143161
$globalSettingsData = $this->backupglobalRepository->findAll();
@@ -160,7 +178,10 @@ public function backuprestoreAction(): ResponseInterface
160178
// Pass to Fluid
161179
$arrMultipleVars['isManualBackup'] = '1';
162180
$arrMultipleVars['log'] = '<pre class="pre-scrollable"><code class="json">'. json_encode(json_decode($arrResponse['log']), JSON_PRETTY_PRINT) .'</code></pre>';
163-
$arrMultipleVars['download_url'] = $arrResponse['download_url'];
181+
$arrMultipleVars['download_url'] = '';
182+
if ($isPublicPath) {
183+
$arrMultipleVars['download_url'] = $arrResponse['download_url'];
184+
}
164185
}
165186
}
166187

@@ -194,6 +215,15 @@ public function deletebackupbackupAction(): ResponseInterface
194215
$request = $this->request->getQueryParams();
195216
$uid = $request['uid'];
196217

218+
$globalSettingsData = $this->backupglobalRepository->findAll();
219+
$globalSettingsData = !empty($globalSettingsData[0]) ? $globalSettingsData[0] : null;
220+
221+
if (!$globalSettingsData) {
222+
$headerMsg = transalte::translate('something.wrong.here', 'ns_backup');
223+
$this->addFlashMessage($headerMsg, '', ContextualFeedbackSeverity::ERROR, true);
224+
die;
225+
}
226+
197227
$arrBackup = $this->backupglobalRepository->findBackupByUid($uid);
198228
// Let's delete it
199229
$this->backupglobalRepository->removeBackupData($uid);
@@ -203,11 +233,13 @@ public function deletebackupbackupAction(): ResponseInterface
203233
unlink($arrBackup['filenames']);
204234
}
205235

206-
$rootPath = $this->globalSettingsData[0]->root ?? (Environment::getProjectPath() ?? '');
236+
$rootPath = $globalSettingsData->root ?? (Environment::getProjectPath() ?? '');
207237
if(Environment::isComposerMode()) {
208238
$rootPath = Environment::getPublicPath();
209239
}
210-
$jsonFolder = $rootPath.'/uploads/tx_nsbackup/json/';
240+
241+
$rootPath = $globalSettingsData->backupStorePath ?? ($rootPath . '/uploads');
242+
$jsonFolder = $rootPath.'/tx_nsbackup/json/';
211243
if(file_exists($jsonFolder.$arrBackup['jsonfile'])) {
212244
unlink($jsonFolder.$arrBackup['jsonfile']);
213245
}
@@ -231,4 +263,18 @@ protected function initializeModuleTemplate(
231263
): ModuleTemplate {
232264
return $this->moduleTemplateFactory->create($request);
233265
}
266+
267+
/**
268+
* @param string $path
269+
* @return boolean
270+
*/
271+
public function isPathPublic(string $path): bool
272+
{
273+
if (!Environment::isComposerMode()) {
274+
$valuesToCheck = ['typo3', 'typo3conf', 'vendor', 'typo3temp', 'bin'];
275+
$parts = array_filter(explode('/', rtrim($path, '/')));
276+
return empty(array_intersect($parts, $valuesToCheck));
277+
}
278+
return str_contains(rtrim($path, '/'), Environment::getPublicPath());
279+
}
234280
}

Classes/Domain/Model/Backupglobal.php

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@ class Backupglobal extends AbstractEntity
2727
*/
2828
public string $emails = '';
2929

30+
/**
31+
* backupStorePath
32+
*
33+
* @var string
34+
*/
35+
public string $backupStorePath = '';
36+
3037
/**
3138
* emailFrom
3239
*
@@ -301,4 +308,25 @@ public function setCleanup(string $cleanup): void
301308
{
302309
$this->cleanup = $cleanup;
303310
}
311+
312+
/**
313+
* Returns the backupStorePath
314+
*
315+
* @return string backupStorePath
316+
*/
317+
public function getBackupStorePath(): string
318+
{
319+
return $this->backupStorePath;
320+
}
321+
322+
/**
323+
* Sets the backupStorePath
324+
*
325+
* @param string $backupStorePath
326+
* @return void
327+
*/
328+
public function setBackupStorePath(string $backupStorePath): void
329+
{
330+
$this->backupStorePath = $backupStorePath;
331+
}
304332
}

Configuration/Backend/Modules.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
'nitsan_nsbackup' => [
88
'parent' => 'tools',
99
'position' => ['before' => 'top'],
10-
'access' => 'user',
11-
'path' => '/module/nitsan/NsBackupBackup ',
10+
'access' => 'admin',
11+
'path' => '/module/nitsan/NsBackupBackup',
1212
'icon' => 'EXT:ns_backup/Resources/Public/Icons/module-nsbackup.svg',
1313
'labels' => 'LLL:EXT:ns_backup/Resources/Private/Language/locallang_backup.xlf',
1414
'extensionName' => 'NsBackup',

Configuration/TCA/tx_nsbackup_domain_model_backupglobal.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,5 +176,14 @@
176176
'eval' => 'trim'
177177
],
178178
],
179+
'backup_store_path' => [
180+
'exclude' => true,
181+
'label' => 'LLL:EXT:ns_backup/Resources/Private/Language/locallang_db.xlf:tx_nsbackup_domain_model_backupglobal.backup_store_path',
182+
'config' => [
183+
'type' => 'input',
184+
'size' => 30,
185+
'eval' => 'trim'
186+
],
187+
],
179188
],
180189
];

Resources/Private/Language/locallang.xlf

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,12 @@
193193
<trans-unit id="globalsettings.form.cleanup_quantity.desc">
194194
<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>
195195
</trans-unit>
196+
<trans-unit id="globalsettings.form.backup_store_path">
197+
<source>Backup Store Path</source>
198+
</trans-unit>
199+
<trans-unit id="globalsettings.form.backup_store_path.desc">
200+
<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>
201+
</trans-unit>
196202
<trans-unit id="globalsettings.form.servercleanup">
197203
<source>Server Cleanups?</source>
198204
</trans-unit>
@@ -445,9 +451,27 @@
445451
<trans-unit id="email.not.valid">
446452
<source>Email format is not valid</source>
447453
</trans-unit>
454+
<trans-unit id="phpPath.not.valid">
455+
<source>PHP path is not executable</source>
456+
</trans-unit>
457+
<trans-unit id="storePath.not.valid">
458+
<source>The backup storage does not exist.</source>
459+
</trans-unit>
448460
<trans-unit id="something.wrong.here">
449461
<source>Something is wrong here. Please check you Global Settings</source>
450462
</trans-unit>
463+
<trans-unit id="servercloud.content.downloadBackupError">
464+
<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>
465+
</trans-unit>
466+
<trans-unit id="servercloud.content.downloadErrorTitle">
467+
<source>Download Backup</source>
468+
</trans-unit>
469+
<trans-unit id="manualbackup.error.description">
470+
<source>Allowed characters: letters, numbers, spaces, dashes (-), and underscores (_).</source>
471+
</trans-unit>
472+
<trans-unit id="servercloud.content.downloadBackupNotFound">
473+
<source>This backup is no longer available.</source>
474+
</trans-unit>
451475
</body>
452476
</file>
453477
</xliff>

Resources/Private/Language/locallang_db.xlf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@
8282
<trans-unit id="tx_nsbackup_domain_model_backupdata.status">
8383
<source>Status</source>
8484
</trans-unit>
85+
<trans-unit id="tx_nsbackup_domain_model_backupglobal.backup_store_path">
86+
<source>Backup Store Path</source>
87+
</trans-unit>
8588
</body>
8689
</file>
8790
</xliff>

Resources/Private/Layouts/Default.html

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
<f:flashMessages />
5151
<f:if condition="{errorValidation}">
5252
<div class="alert alert-warning">
53-
<f:format.raw>{errorValidation}</f:format.raw>
53+
<f:sanitize.html>{errorValidation}</f:sanitize.html>
5454
</div>
5555
</f:if>
5656
<f:render section="content" />
@@ -203,5 +203,47 @@ <h5 class="modal-title">Download Backup</h5>
203203
</div>
204204
</div>
205205

206+
<!-- Private Backup Download Error -->
207+
<div class="modal fade" id="backupDownloadError" role="dialog"
208+
aria-labelledby="nsBackupDeleteRecordModal" aria-hidden="true">
209+
<div class="modal-dialog" role="document">
210+
<div class="modal-content">
211+
<div class="modal-header">
212+
<h5 class="modal-title"><f:translate key="servercloud.content.downloadErrorTitle" /> <span class="backup-title"></span></h5>
213+
<button type="button" class="close" {modalAttr}dismiss="modal" aria-label="Close">
214+
<span aria-hidden="true">&times;</span>
215+
</button>
216+
</div>
217+
<div class="modal-body">
218+
<p class="delete-msg"><f:translate key="servercloud.content.downloadBackupError" /></p>
219+
</div>
220+
<div class="modal-footer">
221+
<button type="button" class="btn btn-warning" {modalAttr}dismiss="modal"><em class="fa fa-close"
222+
aria-hidden="true"></em><f:translate key="servercloud.content.cancel" /></button>
223+
</div>
224+
</div>
225+
</div>
226+
</div>
206227

228+
<!-- Private Backup Download Error -->
229+
<div class="modal fade" id="backupNotAvailable" tabindex="-1" role="dialog"
230+
aria-labelledby="nsBackupNotAvailableModal" aria-hidden="true">
231+
<div class="modal-dialog" role="document">
232+
<div class="modal-content">
233+
<div class="modal-header">
234+
<h5 class="modal-title"><f:translate key="servercloud.content.downloadErrorTitle" /> <span class="backup-title"></span></h5>
235+
<button type="button" class="close" {modalAttr}dismiss="modal" aria-label="Close">
236+
<span aria-hidden="true">&times;</span>
237+
</button>
238+
</div>
239+
<div class="modal-body">
240+
<p class="delete-msg"><f:translate key="servercloud.content.downloadBackupNotFound" /></p>
241+
</div>
242+
<div class="modal-footer">
243+
<button type="button" class="btn btn-warning" {modalAttr}dismiss="modal"><em class="fa fa-close"
244+
aria-hidden="true"></em><f:translate key="servercloud.content.cancel" /></button>
245+
</div>
246+
</div>
247+
</div>
248+
</div>
207249
</html>

0 commit comments

Comments
 (0)