Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/backup/backup.php
+++ b/backup/backup.php
@@ -3,7 +3,7 @@
* Plugin Name: JetBackup
* Plugin URI: https://www.jetbackup.com/jetbackup-for-wordpress
* Description: JetBackup is the most complete WordPress site backup and restore plugin. We offer the easiest way to backup, restore or migrate your site. You can backup your files, database or both.
- * Version: 3.1.19.8
+ * Version: 3.1.20.3
* Author: JetBackup
* Author URI: https://www.jetbackup.com/jetbackup-for-wordpress
* License: GPLv2 or later
--- a/backup/src/JetBackup/Ajax/Calls/AddToQueue.php
+++ b/backup/src/JetBackup/Ajax/Calls/AddToQueue.php
@@ -227,6 +227,12 @@
if(!$this->_getFileUploadId()) {
if(!$this->_getFileSize()) throw new AjaxException("No upload file size was provided");
+ if(
+ $this->_getFileName() != basename($this->_getFileName()) ||
+ $this->_getFileName() == "." ||
+ $this->_getFileName() == ".."
+ ) throw new AjaxException("Invalid filename: ". $this->_getFileName());
+
$upload->setFilename($this->_getFileName());
$upload->setSize($this->_getFileSize());
$upload->setCreated(time());
@@ -244,14 +250,16 @@
if($upload->isCompleted()) {
- if (!( Archive::isTar($upload->getFileLocation()) || Archive::isGzCompressed($upload->getFileLocation()) ) ) {
- Util::rm(dirname($upload->getFileLocation()));
+ $fileLocation = $upload->getFileLocation();
+
+ if (!( Archive::isTar($fileLocation) || Archive::isGzCompressed($fileLocation) ) ) {
+ Util::rm(dirname($fileLocation));
throw new AjaxException("Invalid backup file provided. Only .tar or .tar.gz files are allowed.");
}
// Import the backup file and save it to the database for potential retry
$crossDomain = Factory::getSettingsRestore()->isRestoreAllowCrossDomain();
- $snap = Snapshot::importFromPath($upload->getFileLocation(), $crossDomain);
+ $snap = Snapshot::importFromPath($fileLocation, $crossDomain);
$snap->addToRestoreQueue($options, $excluded_files, $included_files, $exclude_db, $include_db);
$this->setResponseData($this->isCLI() ? $snap->getDisplayCLI() : $snap->getDisplay());
$this->setResponseMessage("Added to queue!");
--- a/backup/src/JetBackup/Ajax/Calls/ManageSchedule.php
+++ b/backup/src/JetBackup/Ajax/Calls/ManageSchedule.php
@@ -6,6 +6,7 @@
use JetBackupAjaxaAjax;
+use JetBackupBackupJobBackupJob;
use JetBackupExceptionAjaxException;
use JetBackupExceptionDBException;
use JetBackupExceptionFieldsValidationException;
@@ -74,6 +75,24 @@
$schedule->validateFields();
$schedule->save();
+ $jobs = BackupJob::query()
+ ->select([JetBackup::ID_FIELD])
+ ->getQuery()
+ ->fetch();
+
+ foreach ($jobs as $jobData) {
+ $job = new BackupJob($jobData[JetBackup::ID_FIELD]);
+
+ foreach ($job->getSchedules() as $scheduleItem) {
+ if ($scheduleItem->getId() == $schedule->getId()) {
+ // update next_run on backup job
+ $job->calculateNextRun();
+ $job->save();
+ break;
+ }
+ }
+ }
+
$this->setResponseMessage('Success');
$this->setResponseData($this->isCLI() ? $schedule->getDisplayCLI() : $schedule->getDisplay());
--- a/backup/src/JetBackup/BackupJob/BackupJob.php
+++ b/backup/src/JetBackup/BackupJob/BackupJob.php
@@ -77,6 +77,8 @@
const BACKUP_ACCOUNT_CONTAINS_HOMEDIR = 1;
const BACKUP_ACCOUNT_CONTAINS_DATABASE = 2;
const BACKUP_ACCOUNT_CONTAINS_FULL = self::BACKUP_ACCOUNT_CONTAINS_HOMEDIR | self::BACKUP_ACCOUNT_CONTAINS_DATABASE;
+ // mask of ALL allowed bits
+ const BACKUP_ACCOUNT_CONTAINS_ALL = self::BACKUP_ACCOUNT_CONTAINS_HOMEDIR | self::BACKUP_ACCOUNT_CONTAINS_DATABASE;
const BACKUP_CONFIG_CONTAINS_CONFIG = 1;
const BACKUP_CONFIG_CONTAINS_DATABASE = 2;
@@ -431,7 +433,9 @@
return implode(", ", $name);
}
-
+ public function isValidBackupType(int $value): bool {
+ return $value > 0 && ($value & ~self::BACKUP_ACCOUNT_CONTAINS_ALL) === 0;
+ }
public function setHidden(bool $hidden) { $this->set(self::HIDDEN, $hidden); }
public function isHidden():bool { return !!$this->get(self::HIDDEN, false); }
@@ -665,8 +669,8 @@
if(!$this->getType()) throw new FieldsValidationException("You must provide backup type");
if(!in_array($this->getType(), [self::TYPE_ACCOUNT,self::TYPE_CONFIG])) throw new FieldsValidationException("Invalid backup type provided");
- if(!$this->getContains()) throw new FieldsValidationException("Backup has to contain at least files or database");
-
+ if (!$this->getContains()) throw new FieldsValidationException("Backup has to contain at least files or database");
+ if (!$this->isValidBackupType($this->getContains())) throw new FieldsValidationException("Invalid backup type");
if($this->getDestinations()) {
$destinations = [];
$is_local = 0;
--- a/backup/src/JetBackup/CLI/Command.php
+++ b/backup/src/JetBackup/CLI/Command.php
@@ -725,19 +725,13 @@
* [--backup_path=<path>]
* : Define the directory path on the remote server for storing backups. Make sure it’s writable and uniquely identifies the domain (e.g., /backups/domain.com/).
*
- * [--enabled=<enabled>]
- * : Set destination enabled or disabled
- *
- * [--export_config=<export_config>]
- * : Set export config enabled or disabled
- *
* [--options=<json-options>]
* : Provide connection details and other advanced options in JSON format.
* Example: '{"host":"sftp_host","username":"sftp_user","password":"sftp_pass","port":22,"timeout":60,"retries":5}'
*
* ## EXAMPLES
*
- * wp jetbackup manageDestination --id=5 --name="sftp" --enabled=0 --export_config=0 --chunk_size=1 --free_disk=0 --backup_path=/foo/boo --notes="This is a note" --options='{"host":"sftp_host","username":"sftp_user","password":"sftp_pass","port":22,"timeout":60,"retries":5}'
+ * wp jetbackup manageDestination --id=5 --name="sftp"--chunk_size=1 --free_disk=0 --backup_path=/foo/boo --notes="This is a note" --options='{"host":"sftp_host","username":"sftp_user","password":"sftp_pass","port":22,"timeout":60,"retries":5}'
* wp jetbackup manageDestination --id=5 --name="google" --type=GoogleDrive --backup_path=/foo/boo --options='{"access_code":"YOU_ACCESS_CODE_HERE"}'
*
* @when after_wp_load
@@ -859,19 +853,25 @@
*
* ## OPTIONS
*
- * [--debug=<debug>]
+ * [--debug_log=<debug_log>]
* : enable or disable debug logging.
*
- * [--log_rotate=<log_rotate>]
+ * [--log_rotate=<bool>]
* : specify the number of days to retain log files. older logs beyond this duration will be automatically deleted. set this value to 0 to keep logs indefinitely.
*
* ## EXAMPLES
*
- * wp jetbackup manageSettingsLogging --debug=1 --log_rotate=7
+ * wp jetbackup manageSettingsLogging --debug2=1 --log_rotate=7
*
* @when after_wp_load
*/
- public function manageSettingsLogging($args, $flags) { self::_command(__FUNCTION__, $args, self::_keyToUpper($flags)); }
+ public function manageSettingsLogging($args, $flags) {
+ // 'debug' is reserved by WP-CLI, so we cannot use it directly as a flag.
+ //// Map our custom 'debug_log' flag to 'debug' internally.
+ $flags['debug'] = $flags['debug_log'] ?? 0; // fallback to 0 if not set
+ self::_command(__FUNCTION__, $args, self::_keyToUpper($flags));
+
+ }
/**
@@ -914,13 +914,33 @@
* [--alternate_email=<alternate_email>]
* : set an alternate email address for notifications instead of the default admin email.
*
+ * [--notification_levels_frequency=<notification_levels_frequency>]
+ * : JSON string for notification levels frequency.
+ * Levels:
+ * 1 = Information
+ * 2 = Warning
+ * 4 = Error
+ *
+ * Frequency:
+ * 0 = Disabled
+ * 1 = Real Time
+ * 2 = Once a day
+ * Example: '{"1":2,"2":0,"4":2}'
+ *
* ## EXAMPLES
*
* wp jetbackup manageSettingsNotifications --emails=1 --alternate_email=youremail@gmail.com
*
* @when after_wp_load
*/
- public function manageSettingsNotifications($args, $flags) { self::_command(__FUNCTION__, $args, self::_keyToUpper($flags)); }
+ public function manageSettingsNotifications($args, $flags) {
+ if (isset($flags['notification_levels_frequency'])) {
+ $decoded = json_decode($flags['notification_levels_frequency'], true);
+ $flags['notification_levels_frequency'] = $decoded;
+ }
+ self::_command(__FUNCTION__, $args, self::_keyToUpper($flags));
+
+ }
/**
@@ -931,7 +951,7 @@
* [--read_chunk_size=<read_chunk_size>]
* : set the chunk size for file uploads. affects upload speed and stability.
*
- * [--execution_time=<execution_time>]
+ * [--performance_execution_time=<performance_execution_time>]
* : define the maximum execution time (in seconds) for queued tasks. applies only to web-based tasks.
*
* [--sql_cleanup_revisions=<sql_cleanup_revisions>]
@@ -954,7 +974,7 @@
*
* ## EXAMPLES
*
- * wp jetbackup manageSettingsPerformance --read_chunk_size=2097152 --execution_time=10 --sql_cleanup_revisions=1 --use_default_excludes=1 --gzip_compress_archive=1
+ * wp jetbackup manageSettingsPerformance --read_chunk_size=2097152 --performance_execution_time=10 --sql_cleanup_revisions=1 --use_default_excludes=1 --gzip_compress_archive=1
*
* @when after_wp_load
*/
@@ -975,8 +995,8 @@
* [--restore_alternate_path=<bool>]
* : enable (1) or disable (0) using alternate public restore path
*
- * [--restore_wp_content_only=<bool>]
- * : enable (1) or disable (0) limit restore to wp-content folder only
+ * [--restore_wp_content_only=<bool>]
+ * : enable (1) or disable (0) limit restore to wp-content folder only
*
* ## EXAMPLES
*
--- a/backup/src/JetBackup/JetBackup.php
+++ b/backup/src/JetBackup/JetBackup.php
@@ -10,7 +10,7 @@
private function __construct() {}
- const VERSION = '3.1.19.8';
+ const VERSION = '3.1.20.3';
const DEVELOPMENT = false;
const DEFAULT_LANGUAGE = 'en_US';
--- a/backup/src/JetBackup/Schedule/Schedule.php
+++ b/backup/src/JetBackup/Schedule/Schedule.php
@@ -54,12 +54,13 @@
self::TYPE_HOURLY => 1,
self::TYPE_DAILY => [1,2,3,4,5,6,7],
self::TYPE_WEEKLY => 1,
- self::TYPE_MONTHLY => [1]
+ self::TYPE_MONTHLY => 1
];
+ CONST ALLOWED_HOURLY_INTERVALS = [1,2,3,4,6,8,12];
const ALLOWED_INTERVALS = [
self::TYPE_DAILY => [1,2,3,4,5,6,7],
- self::TYPE_MONTHLY => [1,7,14,21,28]
+ self::TYPE_MONTHLY => [1,7,14,21,28],
];
const TYPE_NAMES = [
@@ -501,6 +502,16 @@
);
}
}
+ if ($this->getType() == Schedule::TYPE_HOURLY) {
+ $interval = $this->getIntervals();
+ if (!in_array($interval, self::ALLOWED_HOURLY_INTERVALS, true)) {
+ throw new FieldsValidationException(
+ "Schedule interval '{$interval}' is invalid for " .
+ Schedule::TYPE_NAMES[Schedule::TYPE_HOURLY] .
+ ". Allowed intervals: " .implode(',',self::ALLOWED_HOURLY_INTERVALS)
+ );
+ }
+ }
if ($this->getType() == Schedule::TYPE_AFTER_JOB_DONE) {
--- a/backup/src/JetBackup/Settings/General.php
+++ b/backup/src/JetBackup/Settings/General.php
@@ -250,13 +250,11 @@
} catch (Exception $e) {
throw new FieldsValidationException($e->getMessage());
}
-
-
}
if(in_array(self::TIMEZONE, $changedFields)) {
- if((!$this->getTimeZone() || $this->getTimeZone() != (self::DEFAULT_TIMEZONE || self::WORDPRESS_TIMEZONE)) && !isset(Util::generateTimeZoneList()[$this->getTimeZone()]))
- throw new FieldsValidationException("Timezone " . $this->getTimeZone(). " is not valid");
+ if (!$this->getTimeZone() || ($this->getTimeZone() != self::DEFAULT_TIMEZONE && $this->getTimeZone() != self::WORDPRESS_TIMEZONE && !isset(Util::generateTimeZoneList()[$this->getTimeZone()])))
+ throw new FieldsValidationException("Timezone " . $this->getTimeZone() . " is not valid");
}
if(in_array(self::PHP_CLI_LOCATION, $changedFields)) {
--- a/backup/src/JetBackup/Settings/Performance.php
+++ b/backup/src/JetBackup/Settings/Performance.php
@@ -15,7 +15,6 @@
const SECTION = 'performance';
const EXECUTION_TIME = 'PERFORMANCE_EXECUTION_TIME';
-
const READ_CHUNK_SIZE = 'READ_CHUNK_SIZE';
const SQL_CLEANUP_REVISIONS = 'SQL_CLEANUP_REVISIONS';
const USE_DEFAULT_EXCLUDES = 'USE_DEFAULT_EXCLUDES';
@@ -27,7 +26,7 @@
const DEFAULT_EXCLUDES = 'DEFAULT_EXCLUDES';
const DEFAULT_DB_EXCLUDES = 'DEFAULT_DB_EXCLUDES';
const EXECUTION_TIMES = [0, 10, 20, 30, 40, 50, 60, 120, 300, 600];
-
+ const CHUNK_SIZES = [1, 2, 3, 4, 5 ,6 ,8, 10, 12, 14, 16, 20, 24, 32, 64];
/**
* @throws IOException
* @throws ReflectionException
@@ -178,6 +177,11 @@
$changedFields = self::getChangedFields($this->getData(), (new Performance())->getData());
+ if(in_array(self::READ_CHUNK_SIZE, $changedFields)) {
+ if(!in_array($this->getReadChunkSize(), self::CHUNK_SIZES))
+ throw new FieldsValidationException('Chunk size '. $this->getReadChunkSize() . ' is not allowed');
+ }
+
if(in_array(self::EXECUTION_TIME, $changedFields)) {
if(!in_array($this->getExecutionTime(), self::EXECUTION_TIMES))
throw new FieldsValidationException('Execution time of '. $this->getExecutionTime() . ' is not allowed');
--- a/backup/src/JetBackup/Upload/Upload.php
+++ b/backup/src/JetBackup/Upload/Upload.php
@@ -63,7 +63,11 @@
$directory = Factory::getLocations()->getTempDir() . JetBackup::SEP . $this->getUniqueId();
if(!is_dir($directory)) mkdir($directory, 0700);
Util::secureFolder($directory);
- return $directory . JetBackup::SEP . $this->getFilename();
+
+ $safeFilename = basename($this->getFilename());
+ if($safeFilename === '' || $safeFilename === '.' || $safeFilename === '..') throw new IOException("Invalid filename provided");
+
+ return $directory . JetBackup::SEP . $safeFilename;
}
/**