From 88a9dba757fe9ae8eb55c2014657bfc67111c010 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Mon, 12 Oct 2026 09:33:20 +0530 Subject: [PATCH 1/3] fix(backup): leave site credentials out of backup metadata metadata.json, which is uploaded with every backup and kept as a copy in EE_BACKUP_DIR, held the site's database and admin passwords. Restore never reads them (it uses the current site's credentials), so they are now removed before the file is written, and a migration removes them from the local metadata copies of earlier backups. Backups made before this still restore. The EasyDash verification token is no longer written to debug output and is masked in the logged callback payloads. --- ...mand_strip_backup_metadata_credentials.php | 57 +++++++++++++++++++ src/helper/Site_Backup_Restore.php | 15 +++-- 2 files changed, 67 insertions(+), 5 deletions(-) create mode 100644 migrations/db/20261001110000_site-command_strip_backup_metadata_credentials.php diff --git a/migrations/db/20261001110000_site-command_strip_backup_metadata_credentials.php b/migrations/db/20261001110000_site-command_strip_backup_metadata_credentials.php new file mode 100644 index 00000000..d5e61dfd --- /dev/null +++ b/migrations/db/20261001110000_site-command_strip_backup_metadata_credentials.php @@ -0,0 +1,57 @@ +dash_backup_id = $auth_parts[0]; $this->dash_verify_token = $auth_parts[1]; $this->dash_api_url = $ed_api_url; + // The token also goes into the logged callback payloads. + if ( method_exists( 'EE', 'obfuscate' ) ) { + EE::obfuscate( [ $dash_auth, $this->dash_verify_token ] ); + } // Debug: Log parsed values EE::debug( 'Parsed backup_id: ' . $this->dash_backup_id ); - EE::debug( 'Parsed verify_token: ' . $this->dash_verify_token ); EE::debug( 'API URL: ' . $this->dash_api_url ); // Register shutdown handler to send failure callback if backup doesn't complete @@ -434,6 +436,10 @@ function ( $theme ) { $remote_path = $this->get_remote_path(); $backup_data['remote_path'] = explode( ':', $remote_path )[1]; $backup_data = array_merge( $this->site_data, $backup_data ); + // Credentials stay out of the backup: restore uses the current site's, and the metadata is also kept unencrypted on the remote and in EE_BACKUP_DIR. + foreach ( self::METADATA_SECRET_KEYS as $key ) { + unset( $backup_data[ $key ] ); + } $backup_data_file = $backup_dir . '/metadata.json'; $metadata_copy = EE_BACKUP_DIR . '/' . $this->site_data['site_url'] . '.metadata.json'; @@ -1704,7 +1710,6 @@ private function send_dash_success_callback( $ed_api_url, $backup_id, $verify_to // Debug: Log the values being used for callback EE::debug( 'Sending success callback with backup_id: ' . $backup_id ); - EE::debug( 'Sending success callback with verify_token: ' . $verify_token ); // Build metadata for the API call - always include all fields $metadata = [ From ed85fb40d3173d83f3a65116e50ccc15e6110a21 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Mon, 12 Oct 2026 09:42:29 +0530 Subject: [PATCH 2/3] fix(backup): mask the EasyDash token in the logged callback payload The success callback payload is logged at debug level; its verification token is now replaced with ****** in the logged copy, so it doesn't depend on core masking the value. The metadata migration also checks the JSON encoding before replacing a file. --- ...01110000_site-command_strip_backup_metadata_credentials.php | 3 ++- src/helper/Site_Backup_Restore.php | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/migrations/db/20261001110000_site-command_strip_backup_metadata_credentials.php b/migrations/db/20261001110000_site-command_strip_backup_metadata_credentials.php index d5e61dfd..020f2329 100644 --- a/migrations/db/20261001110000_site-command_strip_backup_metadata_credentials.php +++ b/migrations/db/20261001110000_site-command_strip_backup_metadata_credentials.php @@ -40,7 +40,8 @@ public function up() { // A half-written file would make restore refuse the backup, so it is replaced only when complete. $tmp = $file . '.ee-tmp'; - if ( false === file_put_contents( $tmp, json_encode( $data, JSON_PRETTY_PRINT ) ) || ! chmod( $tmp, fileperms( $file ) & 0777 ) || ! rename( $tmp, $file ) ) { + $json = json_encode( $data, JSON_PRETTY_PRINT ); + if ( false === $json || false === file_put_contents( $tmp, $json ) || ! chmod( $tmp, fileperms( $file ) & 0777 ) || ! rename( $tmp, $file ) ) { @unlink( $tmp ); EE::warning( "Could not remove the credentials from $file." ); continue; diff --git a/src/helper/Site_Backup_Restore.php b/src/helper/Site_Backup_Restore.php index b5ded104..58f16e2b 100644 --- a/src/helper/Site_Backup_Restore.php +++ b/src/helper/Site_Backup_Restore.php @@ -1732,7 +1732,7 @@ private function send_dash_success_callback( $ed_api_url, $backup_id, $verify_to 'metadata' => $metadata, ]; - EE::debug( 'Payload being sent: ' . json_encode( $payload ) ); + EE::debug( 'Payload being sent: ' . json_encode( array_merge( $payload, [ 'verify' => '******' ] ) ) ); return $this->send_dash_request( $endpoint, $payload ); } From 4eb129b6e2afb5a89d542df9bb41cf6ca7330a25 Mon Sep 17 00:00:00 2001 From: Riddhesh Sanghvi Date: Mon, 12 Oct 2026 16:17:11 +0530 Subject: [PATCH 3/3] fix(backup): also strip the legacy site auth password from backup metadata Check the temporary file's byte count before it replaces the metadata, so a short write never truncates it. --- ...1110000_site-command_strip_backup_metadata_credentials.php | 4 ++-- src/helper/Site_Backup_Restore.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/migrations/db/20261001110000_site-command_strip_backup_metadata_credentials.php b/migrations/db/20261001110000_site-command_strip_backup_metadata_credentials.php index 020f2329..cb46ba9a 100644 --- a/migrations/db/20261001110000_site-command_strip_backup_metadata_credentials.php +++ b/migrations/db/20261001110000_site-command_strip_backup_metadata_credentials.php @@ -11,7 +11,7 @@ */ class StripBackupMetadataCredentials extends Base { - const SECRET_KEYS = [ 'db_password', 'db_root_password', 'app_admin_password' ]; + const SECRET_KEYS = [ 'db_password', 'db_root_password', 'app_admin_password', 'site_auth_password' ]; public function up() { @@ -41,7 +41,7 @@ public function up() { // A half-written file would make restore refuse the backup, so it is replaced only when complete. $tmp = $file . '.ee-tmp'; $json = json_encode( $data, JSON_PRETTY_PRINT ); - if ( false === $json || false === file_put_contents( $tmp, $json ) || ! chmod( $tmp, fileperms( $file ) & 0777 ) || ! rename( $tmp, $file ) ) { + if ( false === $json || strlen( $json ) !== file_put_contents( $tmp, $json ) || ! chmod( $tmp, fileperms( $file ) & 0777 ) || ! rename( $tmp, $file ) ) { @unlink( $tmp ); EE::warning( "Could not remove the credentials from $file." ); continue; diff --git a/src/helper/Site_Backup_Restore.php b/src/helper/Site_Backup_Restore.php index 58f16e2b..d2620cc4 100644 --- a/src/helper/Site_Backup_Restore.php +++ b/src/helper/Site_Backup_Restore.php @@ -30,7 +30,7 @@ class Site_Backup_Restore { const RCLONE_MIN_VERSION_MT_WRITE_BUFFER = '1.63.0'; // --multi-thread-write-buffer-size (rclone v1.63.0) const RCLONE_MIN_VERSION_MT_CHUNK_SIZE = '1.64.0'; // --multi-thread-chunk-size (rclone v1.64.0) - const METADATA_SECRET_KEYS = [ 'db_password', 'db_root_password', 'app_admin_password' ]; + const METADATA_SECRET_KEYS = [ 'db_password', 'db_root_password', 'app_admin_password', 'site_auth_password' ]; private $fs; public $site_data;