fix(security): make abortEncryptionConversion fail loudly on restore failure

The rollback path restored the .env backup with a suppressed
file_put_contents() and an unchecked file_get_contents(). If the restore
failed after the key had already been rotated, .env was left holding the new
CI4 key while the DB still held CI3-era ciphertext, so the data became
undecryptable after the next restart.

Now the backup read is checked for false and the restore goes through the
existing atomicWriteFile() helper; either failure throws so the error is
surfaced instead of silently corrupting the config. Adds a regression test
that forces an unreadable backup and asserts the throw plus that .env is
left untouched.
This commit is contained in:
jekkos committed 2026-09-21 11:54:41 +00:00
1 parent 23f52ddefd
commit 711e5d6b3f
2 files changed
+36 -2

No files matched your search

+8 -2
View File
@@ -572,9 +572,15 @@ function abortEncryptionConversion(): void
return;
}
@chmod($configPath, 0640);
$configFile = file_get_contents($backupPath);
@file_put_contents($configPath, $configFile);
if ($configFile === false) {
throw new RuntimeException(lang('Error.unable_to_read_env_file', ['filePath' => $backupPath]));
}
if (!atomicWriteFile($configPath, $configFile)) {
throw new RuntimeException(lang('Error.unable_to_persist_encryption_key', ['filePath' => $configPath]));
}
log_message('info', "Restored $configPath from backup");
}
+28
View File
@@ -564,6 +564,34 @@ class security_helperTest extends CIUnitTestCase
$this->assertSame("encryption.key='old'\n", file_get_contents($this->envPath));
}
public function testAbortEncryptionConversionThrowsWhenBackupUnreadable(): void
{
// Force a failed backup read: file_exists() is true (it is a directory)
// but file_get_contents() returns false. The restore must then throw
// instead of silently writing an empty .env and destroying the active key.
if (!is_dir(dirname($this->backupPath))) {
mkdir(dirname($this->backupPath), 0750, true);
}
@unlink($this->backupPath);
mkdir($this->backupPath);
file_put_contents($this->envPath, "encryption.key='new'\n");
$before = (string) file_get_contents($this->envPath);
$threw = false;
try {
abortEncryptionConversion();
} catch (RuntimeException $e) {
$threw = true;
} finally {
@rmdir($this->backupPath);
}
$this->assertTrue($threw, 'a failed backup read must throw instead of failing silently');
$this->assertSame($before, (string) file_get_contents($this->envPath), '.env must be left untouched when the backup is unreadable');
}
public function testRemoveBackupDeletesBackupFile(): void
{
if (!is_dir(dirname($this->backupPath))) {