mirror of
https://github.com/itflow-org/itflow
synced 2026-08-05 07:07:14 +00:00
Backups are now AES-256 encrypted zips in three types (full, database only, master key), catalogued in a new backups table, built by cron rather than the web request, and kept under uploads/backups with retention in the nightly job. The encryption key is one value per install held in config.php, never in the database and never in the file name. Restore is shared by the setup wizard and the new scripts/restore_cli.php, which is the only path without an upload size limit. It verifies the key and unpacks the archive before dropping anything, and dumps the current database first so a failed import is rolled back. A backup dumps, zips and encrypts for minutes without issuing a query, so on a server with a short wait_timeout the connection is closed underneath it and the UPDATE marking the backup complete is what fails - long after the archive was written correctly. The connection is now held open for the job and re-established before any write that follows long file work, including the database phase of a restore. Retention recovers rows a dropped connection left behind: still Running after six hours becomes Complete if the archive is on disk, Failed if it is not. cron.php's own failure path is hardened to match. It recorded job failures through the same connection the failing job had just killed, so an uncaught exception ended the dispatch and no trace of the original error survived. Failures now also echo to stdout, so cron mails something useful when the database is unreachable. Security: the setup wizard's restore step is now closed on any install that has users, whatever config.php says. $config_enable_setup defaulted to enabled when the flag was absent, and the flag is only written at the end of a successful install, so an install abandoned partway left an unauthenticated endpoint that would drop every table, import an attacker-supplied archive, and overwrite uploads/ including the .htaccess that stops PHP running there. Affects 26.07 and earlier. Restoring over a live install is now CLI only.
114 lines
4.3 KiB
PHP
114 lines
4.3 KiB
PHP
<?php
|
|
|
|
/*
|
|
* ITFlow - GET/POST request handler for backups
|
|
*
|
|
* Archives are not built here. Everything on this page records intent and lets
|
|
* cron/backup.php do the work - see backupQueue() for why.
|
|
*/
|
|
|
|
defined('FROM_POST_HANDLER') || die("Direct file access is not allowed");
|
|
|
|
if (isset($_GET['queue_backup'])) {
|
|
|
|
validateCSRFToken();
|
|
|
|
$type = strtolower(trim($_GET['queue_backup']));
|
|
|
|
$error = null;
|
|
$backup_id = backupQueue($mysqli, $type, $session_name ?? 'Unknown User', $error);
|
|
|
|
if ($backup_id > 0) {
|
|
logAudit("Backup", "Queue", ($session_name ?? 'Unknown User') . " queued a " . backupTypeLabel($type));
|
|
flashAlert(backupTypeLabel($type) . " queued - it will start within a minute and you will be notified when it is ready.");
|
|
} else {
|
|
flashAlert($error ?? "Could not queue the backup.", 'error');
|
|
}
|
|
|
|
redirect("backup.php");
|
|
}
|
|
|
|
if (isset($_GET['delete_backup'])) {
|
|
|
|
validateCSRFToken();
|
|
|
|
$backup_id = intval($_GET['delete_backup']);
|
|
|
|
$sql = mysqli_query($mysqli, "SELECT backup_file_name, backup_type FROM backups WHERE backup_id = $backup_id");
|
|
|
|
if (mysqli_num_rows($sql) === 1) {
|
|
$row = mysqli_fetch_assoc($sql);
|
|
backupDeleteById($mysqli, $backup_id);
|
|
logAudit("Backup", "Delete", ($session_name ?? 'Unknown User') . " deleted backup " . escapeSql($row['backup_file_name']));
|
|
flashAlert("Backup deleted.");
|
|
} else {
|
|
flashAlert("Backup not found.", 'error');
|
|
}
|
|
|
|
redirect("backup.php");
|
|
}
|
|
|
|
if (isset($_POST['edit_backup_settings'])) {
|
|
|
|
validateCSRFToken();
|
|
|
|
$retention_days = intval($_POST['config_backup_retention_days']);
|
|
$retention_count = intval($_POST['config_backup_retention_count']);
|
|
$cron_type = escapeSql($_POST['config_backup_cron_type']);
|
|
|
|
if ($retention_days < 0) { $retention_days = 0; }
|
|
if ($retention_count < 1) { $retention_count = 1; }
|
|
|
|
// The scheduled job runs without a session, so it can only produce the unattended types
|
|
if (!in_array($cron_type, backupUnattendedTypes(), true)) {
|
|
$cron_type = BACKUP_TYPE_FULL;
|
|
}
|
|
|
|
mysqli_query($mysqli, "UPDATE settings SET config_backup_retention_days = $retention_days, config_backup_retention_count = $retention_count, config_backup_cron_type = '$cron_type' WHERE company_id = 1");
|
|
|
|
logAudit("Backup", "Edit", ($session_name ?? 'Unknown User') . " updated the backup settings");
|
|
flashAlert("Backup settings saved.");
|
|
|
|
redirect("backup.php");
|
|
}
|
|
|
|
if (isset($_POST['backup_master_key'])) {
|
|
|
|
validateCSRFToken();
|
|
|
|
$password = $_POST['password'];
|
|
|
|
$sql = mysqli_query($mysqli, "SELECT user_password, user_specific_encryption_ciphertext FROM users WHERE user_id = $session_user_id");
|
|
$row = mysqli_fetch_assoc($sql);
|
|
|
|
if (!$row || !password_verify($password, $row['user_password'])) {
|
|
logAudit("Master Key", "Download", ($session_name ?? 'Unknown User') . " attempted to retrieve the master encryption key but failed");
|
|
flashAlert("Incorrect password.", 'error');
|
|
redirect("backup.php");
|
|
}
|
|
|
|
$site_encryption_master_key = decryptUserSpecificKey($row['user_specific_encryption_ciphertext'], $password);
|
|
|
|
if (empty($site_encryption_master_key)) {
|
|
logAudit("Master Key", "Download", ($session_name ?? 'Unknown User') . " could not unwrap the master encryption key");
|
|
flashAlert("Your password is correct but the master key could not be unwrapped from your account.", 'error');
|
|
redirect("backup.php");
|
|
}
|
|
|
|
logAudit("Master Key", "Download", ($session_name ?? 'Unknown User') . " retrieved the master encryption key");
|
|
appNotify("Master Key", ($session_name ?? 'Unknown User') . " retrieved the master encryption key", "/admin/backup.php");
|
|
|
|
// Written as an encrypted archive too, so it can be filed with the other backups.
|
|
// This is the one type cron can never produce - the key only exists inside a session.
|
|
$error = null;
|
|
backupCreate($mysqli, BACKUP_TYPE_MASTER_KEY, $session_name ?? 'Unknown User', 'Manual', $error, ['master_key' => $site_encryption_master_key]);
|
|
|
|
$_SESSION['backup_master_key_reveal'] = $site_encryption_master_key;
|
|
|
|
if ($error) {
|
|
flashAlert("Master key shown below, but the encrypted copy could not be written: $error", 'error');
|
|
}
|
|
|
|
redirect("backup.php");
|
|
}
|