nilsteampassnet /
TeamPass
This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include, or for example
via PHP's auto-loading mechanism.
| 1 | <?php |
||||
| 2 | |||||
| 3 | declare(strict_types=1); |
||||
| 4 | |||||
| 5 | /** |
||||
| 6 | * Teampass - a collaborative passwords manager. |
||||
| 7 | * --- |
||||
| 8 | * This file is part of the TeamPass project. |
||||
| 9 | * |
||||
| 10 | * TeamPass is free software: you can redistribute it and/or modify it |
||||
| 11 | * under the terms of the GNU General Public License as published by |
||||
| 12 | * the Free Software Foundation, version 3 of the License. |
||||
| 13 | * |
||||
| 14 | * TeamPass is distributed in the hope that it will be useful, |
||||
| 15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
| 16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
| 17 | * GNU General Public License for more details. |
||||
| 18 | * |
||||
| 19 | * You should have received a copy of the GNU General Public License |
||||
| 20 | * along with this program. If not, see <https://www.gnu.org/licenses/>. |
||||
| 21 | * |
||||
| 22 | * Certain components of this file may be under different licenses. For |
||||
| 23 | * details, see the `licenses` directory or individual file headers. |
||||
| 24 | * --- |
||||
| 25 | * @file restore.php |
||||
| 26 | * @author Nils Laumaillé ([email protected]) |
||||
| 27 | * @copyright 2009-2026 Teampass.net |
||||
| 28 | * @license GPL-3.0 |
||||
| 29 | * @see https://www.teampass.net |
||||
| 30 | */ |
||||
| 31 | |||||
| 32 | use TeampassClasses\ConfigManager\ConfigManager; |
||||
| 33 | |||||
| 34 | require_once __DIR__ . '/../sources/main.functions.php'; |
||||
| 35 | require_once __DIR__ . '/../sources/backup.functions.php'; |
||||
| 36 | |||||
| 37 | loadClasses('DB'); |
||||
| 38 | |||||
| 39 | function tpCliOut(string $msg): void |
||||
| 40 | { |
||||
| 41 | fwrite(STDOUT, $msg . PHP_EOL); |
||||
| 42 | } |
||||
| 43 | |||||
| 44 | function tpCliErr(string $msg): void |
||||
| 45 | { |
||||
| 46 | fwrite(STDERR, $msg . PHP_EOL); |
||||
| 47 | } |
||||
| 48 | |||||
| 49 | function tpCliHelp(): void |
||||
| 50 | { |
||||
| 51 | tpCliOut('Usage: php scripts/restore.php --file "/path/to/backup.sql" --auth-token "TOKEN" [--force-disconnect]'); |
||||
| 52 | } |
||||
| 53 | |||||
| 54 | function tpCliIsInteractive(): bool |
||||
| 55 | { |
||||
| 56 | try { |
||||
| 57 | if (function_exists('stream_isatty')) { |
||||
| 58 | return @stream_isatty(STDERR); |
||||
| 59 | } |
||||
| 60 | if (function_exists('posix_isatty')) { |
||||
| 61 | return @posix_isatty(STDERR); |
||||
| 62 | } |
||||
| 63 | } catch (Throwable $e) { |
||||
| 64 | // ignore |
||||
| 65 | } |
||||
| 66 | return false; |
||||
| 67 | } |
||||
| 68 | |||||
| 69 | /** |
||||
| 70 | * Render a single-line progress bar on STDERR. |
||||
| 71 | * - Interactive terminals: updates in place (\r) |
||||
| 72 | * - Non-interactive: prints a line every 5% (and at 100%) |
||||
| 73 | */ |
||||
| 74 | function tpCliProgressBar(int $pct, int $totalExecuted, bool $interactive): void |
||||
| 75 | { |
||||
| 76 | static $lastPct = -1; |
||||
| 77 | static $lastLen = 0; |
||||
| 78 | static $lastTs = 0.0; |
||||
| 79 | |||||
| 80 | $pct = max(0, min(100, $pct)); |
||||
| 81 | |||||
| 82 | if (!$interactive) { |
||||
| 83 | if ($pct !== $lastPct && ($pct % 5 === 0 || $pct === 100)) { |
||||
| 84 | fwrite(STDERR, 'Progress: ' . $pct . '% (statements executed: ' . $totalExecuted . ')' . PHP_EOL); |
||||
| 85 | $lastPct = $pct; |
||||
| 86 | } |
||||
| 87 | return; |
||||
| 88 | } |
||||
| 89 | |||||
| 90 | $now = microtime(true); |
||||
| 91 | if ($pct === $lastPct && ($now - $lastTs) < 0.20) { |
||||
| 92 | return; |
||||
| 93 | } |
||||
| 94 | |||||
| 95 | $lastTs = $now; |
||||
| 96 | $lastPct = $pct; |
||||
| 97 | |||||
| 98 | $width = 30; |
||||
| 99 | $filled = (int) round(($pct / 100) * $width); |
||||
| 100 | $bar = str_repeat('#', $filled) . str_repeat('-', $width - $filled); |
||||
| 101 | $msg = sprintf("\r[%s] %3d%% | statements: %d", $bar, $pct, $totalExecuted); |
||||
| 102 | |||||
| 103 | $pad = max(0, $lastLen - strlen($msg)); |
||||
| 104 | fwrite(STDERR, $msg . str_repeat(' ', $pad)); |
||||
| 105 | $lastLen = strlen($msg); |
||||
| 106 | |||||
| 107 | if ($pct === 100) { |
||||
| 108 | fwrite(STDERR, PHP_EOL); |
||||
| 109 | } |
||||
| 110 | } |
||||
| 111 | |||||
| 112 | if (PHP_SAPI !== 'cli') { |
||||
| 113 | tpCliErr('ERROR: This script must be executed in CLI.'); |
||||
| 114 | exit(2); |
||||
| 115 | } |
||||
| 116 | |||||
| 117 | // Parse CLI options |
||||
| 118 | $options = getopt('', ['file:', 'auth-token:', 'force-disconnect', 'help']); |
||||
| 119 | if (isset($options['help']) || empty($options['file']) || empty($options['auth-token'])) { |
||||
| 120 | tpCliHelp(); |
||||
| 121 | exit(isset($options['help']) ? 0 : 2); |
||||
| 122 | } |
||||
| 123 | |||||
| 124 | $file = (string) $options['file']; |
||||
| 125 | $authToken = (string) $options['auth-token']; |
||||
| 126 | $forceDisconnect = array_key_exists('force-disconnect', $options); |
||||
| 127 | |||||
| 128 | $interactive = tpCliIsInteractive(); |
||||
| 129 | |||||
| 130 | // Load settings |
||||
| 131 | $configManager = new ConfigManager(); |
||||
| 132 | $SETTINGS = $configManager->getAllSettings(); |
||||
| 133 | |||||
| 134 | // Logging |
||||
| 135 | $filesDir = rtrim((string) ($SETTINGS['path_to_files_folder'] ?? sys_get_temp_dir()), '/'); |
||||
| 136 | $logDir = $filesDir . '/restore_cli_logs'; |
||||
| 137 | if (!is_dir($logDir)) { |
||||
| 138 | @mkdir($logDir, 0700, true); |
||||
|
0 ignored issues
–
show
|
|||||
| 139 | } |
||||
| 140 | $logFile = $logDir . '/restore_' . date('Ymd_His') . '.log'; |
||||
| 141 | |||||
| 142 | $log = function (string $level, string $message) use ($logFile): void { |
||||
| 143 | $line = '[' . date('c') . '][' . $level . '] ' . $message . PHP_EOL; |
||||
| 144 | @file_put_contents($logFile, $line, FILE_APPEND); |
||||
|
0 ignored issues
–
show
It seems like you do not handle an error condition for
file_put_contents(). This can introduce security issues, and is generally not recommended.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
If you suppress an error, we recommend checking for the error condition explicitly: // For example instead of
@mkdir($dir);
// Better use
if (@mkdir($dir) === false) {
throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
|
|||||
| 145 | if ($level === 'ERROR') { |
||||
| 146 | tpCliErr($message); |
||||
| 147 | } else { |
||||
| 148 | tpCliOut($message); |
||||
| 149 | } |
||||
| 150 | }; |
||||
| 151 | |||||
| 152 | // Lock (prevent concurrent restores) |
||||
| 153 | // Prefer the TeamPass files folder, but fallback to system temp if not writable for the current user. |
||||
| 154 | $lockFile = ''; |
||||
| 155 | $lockHandle = false; |
||||
| 156 | $lockCandidates = [ |
||||
| 157 | $filesDir . '/restore_cli.lock', |
||||
| 158 | rtrim((string) sys_get_temp_dir(), '/\\') . '/teampass_restore_cli_' . substr(md5($filesDir), 0, 12) . '.lock', |
||||
| 159 | ]; |
||||
| 160 | |||||
| 161 | foreach ($lockCandidates as $candidate) { |
||||
| 162 | $h = @fopen($candidate, 'c'); |
||||
| 163 | if ($h !== false) { |
||||
| 164 | $lockFile = $candidate; |
||||
| 165 | $lockHandle = $h; |
||||
| 166 | break; |
||||
| 167 | } |
||||
| 168 | } |
||||
| 169 | |||||
| 170 | if ($lockHandle === false) { |
||||
| 171 | $log('ERROR', 'Unable to open any lock file. Tried: ' . implode(', ', $lockCandidates)); |
||||
| 172 | exit(20); |
||||
| 173 | } |
||||
| 174 | |||||
| 175 | if (!flock($lockHandle, LOCK_EX | LOCK_NB)) { |
||||
| 176 | $log('ERROR', 'Another restore seems to be in progress (lock: ' . $lockFile . ').'); |
||||
| 177 | exit(20); |
||||
| 178 | } |
||||
| 179 | |||||
| 180 | @ftruncate($lockHandle, 0); |
||||
| 181 | @fwrite($lockHandle, (string) getmypid() . ' ' . date('c') . PHP_EOL); |
||||
| 182 | |||||
| 183 | // Normalize file path |
||||
| 184 | $realFile = realpath($file); |
||||
| 185 | if ($realFile !== false) { |
||||
| 186 | $file = $realFile; |
||||
| 187 | } |
||||
| 188 | |||||
| 189 | if (!is_file($file)) { |
||||
| 190 | $log('ERROR', 'Backup file not found: ' . $file); |
||||
| 191 | exit(3); |
||||
| 192 | } |
||||
| 193 | |||||
| 194 | $tokenHash = hash('sha256', $authToken); |
||||
| 195 | |||||
| 196 | // Token verification (DB) |
||||
| 197 | $fetch = tpRestoreAuthorizationFetchByHash($tokenHash); |
||||
| 198 | if (empty($fetch['success'])) { |
||||
| 199 | $log('ERROR', 'Authorization token not found or invalid.'); |
||||
| 200 | exit(30); |
||||
| 201 | } |
||||
| 202 | |||||
| 203 | $authId = (int) ($fetch['id'] ?? 0); |
||||
| 204 | $payload = $fetch['payload'] ?? []; |
||||
| 205 | if (!is_array($payload)) { |
||||
| 206 | $log('ERROR', 'Authorization payload is invalid.'); |
||||
| 207 | exit(30); |
||||
| 208 | } |
||||
| 209 | |||||
| 210 | $now = time(); |
||||
| 211 | $status = (string) ($payload['status'] ?? ''); |
||||
| 212 | $expiresAt = (int) ($payload['expires_at'] ?? 0); |
||||
| 213 | $payloadFilePath = (string) (($payload['file']['path'] ?? '')); |
||||
| 214 | |||||
| 215 | if ($status !== 'pending') { |
||||
| 216 | $log('ERROR', 'Authorization token is not pending (status=' . $status . ').'); |
||||
| 217 | exit(31); |
||||
| 218 | } |
||||
| 219 | |||||
| 220 | if ($expiresAt > 0 && $expiresAt < $now) { |
||||
| 221 | $log('ERROR', 'Authorization token is expired.'); |
||||
| 222 | exit(32); |
||||
| 223 | } |
||||
| 224 | |||||
| 225 | // Ensure the token matches the file we are about to restore |
||||
| 226 | $cmp1 = $payloadFilePath; |
||||
| 227 | $cmp2 = $file; |
||||
| 228 | $cmp1Real = ($cmp1 !== '') ? realpath($cmp1) : false; |
||||
| 229 | if ($cmp1Real !== false) { |
||||
| 230 | $cmp1 = $cmp1Real; |
||||
| 231 | } |
||||
| 232 | $cmp2Real = realpath($cmp2); |
||||
| 233 | if ($cmp2Real !== false) { |
||||
| 234 | $cmp2 = $cmp2Real; |
||||
| 235 | } |
||||
| 236 | |||||
| 237 | if ($cmp1 === '' || $cmp1 !== $cmp2) { |
||||
| 238 | $log('ERROR', 'Authorization token does not match the provided --file path.'); |
||||
| 239 | $log('ERROR', 'Expected: ' . $payloadFilePath); |
||||
| 240 | $log('ERROR', 'Provided: ' . $file); |
||||
| 241 | exit(33); |
||||
| 242 | } |
||||
| 243 | |||||
| 244 | // Always disconnect the restore initiator right away (so the admin can safely run the CLI). |
||||
| 245 | $initiatorId = (int) (($payload['initiator']['id'] ?? 0)); |
||||
| 246 | if ($initiatorId > 0) { |
||||
| 247 | try { |
||||
| 248 | DB::update( |
||||
| 249 | prefixTable('users'), |
||||
| 250 | [ |
||||
| 251 | 'timestamp' => '', |
||||
| 252 | 'key_tempo' => '', |
||||
| 253 | 'session_end' => '', |
||||
| 254 | ], |
||||
| 255 | 'id = %i', |
||||
| 256 | $initiatorId |
||||
| 257 | ); |
||||
| 258 | $log('INFO', 'Disconnected restore initiator (user id=' . $initiatorId . ').'); |
||||
| 259 | } catch (Throwable $e) { |
||||
| 260 | $log('WARN', 'Unable to disconnect restore initiator: ' . $e->getMessage()); |
||||
| 261 | } |
||||
| 262 | } |
||||
| 263 | |||||
| 264 | // Connected users detection (web + API if possible) |
||||
| 265 | $webUsers = []; |
||||
| 266 | $apiUsers = []; |
||||
| 267 | try { |
||||
| 268 | if ($initiatorId > 0) { |
||||
| 269 | $webUsers = DB::query( |
||||
| 270 | 'SELECT id, login, name, lastname, session_end |
||||
| 271 | FROM ' . prefixTable('users') . ' |
||||
| 272 | WHERE session_end >= %i AND id != %i', |
||||
| 273 | $now, |
||||
| 274 | $initiatorId |
||||
| 275 | ); |
||||
| 276 | } else { |
||||
| 277 | $webUsers = DB::query( |
||||
| 278 | 'SELECT id, login, name, lastname, session_end |
||||
| 279 | FROM ' . prefixTable('users') . ' |
||||
| 280 | WHERE session_end >= %i', |
||||
| 281 | $now |
||||
| 282 | ); |
||||
| 283 | } |
||||
| 284 | } catch (Throwable $e) { |
||||
| 285 | // Ensure any in-place progress bar line is terminated before printing errors |
||||
| 286 | if ($interactive) { |
||||
| 287 | @fwrite(STDERR, PHP_EOL); |
||||
|
0 ignored issues
–
show
It seems like you do not handle an error condition for
fwrite(). This can introduce security issues, and is generally not recommended.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
If you suppress an error, we recommend checking for the error condition explicitly: // For example instead of
@mkdir($dir);
// Better use
if (@mkdir($dir) === false) {
throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
|
|||||
| 288 | } |
||||
| 289 | $log('WARN', 'Unable to check web sessions: ' . $e->getMessage()); |
||||
| 290 | } |
||||
| 291 | |||||
| 292 | try { |
||||
| 293 | $apiTokenDuration = 3600; |
||||
| 294 | if (isset($SETTINGS['api_token_duration']) && is_numeric($SETTINGS['api_token_duration'])) { |
||||
| 295 | $apiTokenDuration = (int) $SETTINGS['api_token_duration']; |
||||
| 296 | } |
||||
| 297 | if ($apiTokenDuration <= 0) { |
||||
| 298 | $apiTokenDuration = 3600; |
||||
| 299 | } |
||||
| 300 | $apiConnectedAfter = $now - ($apiTokenDuration + 600); |
||||
| 301 | |||||
| 302 | $apiUsers = DB::query( |
||||
| 303 | 'SELECT u.id, u.login, u.name, u.lastname, api_conn.last_api_date AS last_api |
||||
| 304 | FROM ' . prefixTable('users') . ' u |
||||
| 305 | INNER JOIN ( |
||||
| 306 | SELECT qui, MAX(date) AS last_api_date |
||||
| 307 | FROM ' . prefixTable('log_system') . ' |
||||
| 308 | WHERE type = %s AND label = %s AND date >= %i |
||||
| 309 | GROUP BY qui |
||||
| 310 | ) api_conn ON api_conn.qui = u.id', |
||||
| 311 | 'api', |
||||
| 312 | 'user_connection', |
||||
| 313 | $apiConnectedAfter |
||||
| 314 | ); |
||||
| 315 | } catch (Throwable $e) { |
||||
| 316 | // Optional: ignore API checks if unavailable |
||||
| 317 | $log('WARN', 'Unable to check API activity: ' . $e->getMessage()); |
||||
| 318 | } |
||||
| 319 | |||||
| 320 | // Enforce disconnect policy for web sessions |
||||
| 321 | if (!empty($webUsers) && $forceDisconnect === false) { |
||||
| 322 | $log('ERROR', 'Active web sessions detected. Re-run with --force-disconnect or disconnect users first.'); |
||||
| 323 | foreach ($webUsers as $u) { |
||||
| 324 | $log('ERROR', 'WEB user connected: ' . ($u['login'] ?? '') . ' (' . ($u['name'] ?? '') . ' ' . ($u['lastname'] ?? '') . ')'); |
||||
| 325 | } |
||||
| 326 | if (!empty($apiUsers)) { |
||||
| 327 | $log('WARN', 'API activity detected as well (best-effort detection):'); |
||||
| 328 | foreach ($apiUsers as $u) { |
||||
| 329 | $log('WARN', 'API activity: ' . ($u['login'] ?? '') . ' (' . ($u['name'] ?? '') . ' ' . ($u['lastname'] ?? '') . ')'); |
||||
| 330 | } |
||||
| 331 | } |
||||
| 332 | // IMPORTANT: keep token pending so the operator can retry after disconnecting users. |
||||
| 333 | $payload['last_error'] = 'active_web_sessions'; |
||||
| 334 | $payload['last_error_at'] = time(); |
||||
| 335 | tpRestoreAuthorizationUpdatePayload($authId, $payload); |
||||
| 336 | |||||
| 337 | exit(10); |
||||
| 338 | } |
||||
| 339 | |||||
| 340 | if (!empty($webUsers) && $forceDisconnect === true) { |
||||
| 341 | $log('INFO', 'Active web sessions detected: forcing disconnection.'); |
||||
| 342 | try { |
||||
| 343 | DB::update( |
||||
| 344 | prefixTable('users'), |
||||
| 345 | [ |
||||
| 346 | 'timestamp' => '', |
||||
| 347 | 'key_tempo' => '', |
||||
| 348 | 'session_end' => '', |
||||
| 349 | ], |
||||
| 350 | 'session_end >= %i', |
||||
| 351 | $now |
||||
| 352 | ); |
||||
| 353 | } catch (Throwable $e) { |
||||
| 354 | $log('WARN', 'Failed to force-disconnect web users: ' . $e->getMessage()); |
||||
| 355 | } |
||||
| 356 | } |
||||
| 357 | |||||
| 358 | // From this point, we consider the restore authorized to start. |
||||
| 359 | $payload['status'] = 'in_progress'; |
||||
| 360 | $payload['started_at'] = time(); |
||||
| 361 | tpRestoreAuthorizationUpdatePayload($authId, $payload); |
||||
| 362 | |||||
| 363 | if (!empty($apiUsers)) { |
||||
| 364 | $log('WARN', 'API activity detected (best-effort): those sessions may not be revocable. Proceeding.'); |
||||
| 365 | foreach ($apiUsers as $u) { |
||||
| 366 | $log('WARN', 'API activity: ' . ($u['login'] ?? '') . ' (' . ($u['name'] ?? '') . ' ' . ($u['lastname'] ?? '') . ')'); |
||||
| 367 | } |
||||
| 368 | } |
||||
| 369 | |||||
| 370 | // Maintenance mode toggle (best-effort) |
||||
| 371 | // Keep maintenance enabled at the end of the restore (same behavior as the legacy web restore), |
||||
| 372 | // because the SQL dump may restore maintenance_mode to 0. |
||||
| 373 | try { |
||||
| 374 | $affected = DB::update( |
||||
| 375 | prefixTable('misc'), |
||||
| 376 | [ |
||||
| 377 | 'valeur' => '1', |
||||
| 378 | 'updated_at' => time(), |
||||
| 379 | ], |
||||
| 380 | 'intitule = %s AND type = %s', |
||||
| 381 | 'maintenance_mode', |
||||
| 382 | 'admin' |
||||
| 383 | ); |
||||
| 384 | if ($affected === 0) { |
||||
| 385 | DB::insert( |
||||
| 386 | prefixTable('misc'), |
||||
| 387 | [ |
||||
| 388 | 'intitule' => 'maintenance_mode', |
||||
| 389 | 'type' => 'admin', |
||||
| 390 | 'valeur' => '1', |
||||
| 391 | 'updated_at' => time(), |
||||
| 392 | ] |
||||
| 393 | ); |
||||
| 394 | } |
||||
| 395 | } catch (Throwable $e) { |
||||
| 396 | // Fallback without updated_at |
||||
| 397 | try { |
||||
| 398 | $affected = DB::update( |
||||
| 399 | prefixTable('misc'), |
||||
| 400 | [ |
||||
| 401 | 'valeur' => '1', |
||||
| 402 | ], |
||||
| 403 | 'intitule = %s AND type = %s', |
||||
| 404 | 'maintenance_mode', |
||||
| 405 | 'admin' |
||||
| 406 | ); |
||||
| 407 | if ($affected === 0) { |
||||
| 408 | DB::insert( |
||||
| 409 | prefixTable('misc'), |
||||
| 410 | [ |
||||
| 411 | 'intitule' => 'maintenance_mode', |
||||
| 412 | 'type' => 'admin', |
||||
| 413 | 'valeur' => '1', |
||||
| 414 | ] |
||||
| 415 | ); |
||||
| 416 | } |
||||
| 417 | } catch (Throwable $e2) { |
||||
| 418 | $log('WARN', 'Unable to enable maintenance mode: ' . $e2->getMessage()); |
||||
| 419 | } |
||||
| 420 | } |
||||
| 421 | |||||
| 422 | $log('INFO', 'Starting restore from: ' . $file); |
||||
| 423 | $log('INFO', 'Log file: ' . $logFile); |
||||
| 424 | |||||
| 425 | // Build decryption keys to try |
||||
| 426 | $keysToTry = []; |
||||
| 427 | |||||
| 428 | // Secrets from payload (encrypted in DB) |
||||
| 429 | try { |
||||
| 430 | $secrets = $payload['secrets'] ?? []; |
||||
| 431 | if (is_array($secrets)) { |
||||
| 432 | if (!empty($secrets['encryptionKey'])) { |
||||
| 433 | $tmp = cryption((string) $secrets['encryptionKey'], '', 'decrypt', $SETTINGS); |
||||
| 434 | $k = isset($tmp['string']) ? (string) $tmp['string'] : ''; |
||||
| 435 | if ($k !== '') $keysToTry[] = $k; |
||||
| 436 | } |
||||
| 437 | if (!empty($secrets['overrideKey'])) { |
||||
| 438 | $tmp = cryption((string) $secrets['overrideKey'], '', 'decrypt', $SETTINGS); |
||||
| 439 | $k = isset($tmp['string']) ? (string) $tmp['string'] : ''; |
||||
| 440 | if ($k !== '') $keysToTry[] = $k; |
||||
| 441 | } |
||||
| 442 | } |
||||
| 443 | } catch (Throwable $e) { |
||||
| 444 | $log('WARN', 'Unable to decrypt stored secrets: ' . $e->getMessage()); |
||||
| 445 | } |
||||
| 446 | |||||
| 447 | // Instance key (scheduled backups) |
||||
| 448 | try { |
||||
| 449 | if (!empty($SETTINGS['bck_script_passkey'] ?? '') === true) { |
||||
| 450 | $rawInstanceKey = (string) $SETTINGS['bck_script_passkey']; |
||||
| 451 | $tmp = cryption($rawInstanceKey, '', 'decrypt', $SETTINGS); |
||||
| 452 | $decInstanceKey = isset($tmp['string']) ? (string) $tmp['string'] : ''; |
||||
| 453 | |||||
| 454 | if ($decInstanceKey !== '') { |
||||
| 455 | $keysToTry[] = $decInstanceKey; |
||||
| 456 | } |
||||
| 457 | // Some environments may store bck_script_passkey already in clear |
||||
| 458 | if ($rawInstanceKey !== '' && $rawInstanceKey !== $decInstanceKey) { |
||||
| 459 | $keysToTry[] = $rawInstanceKey; |
||||
| 460 | } |
||||
| 461 | } |
||||
| 462 | } catch (Throwable $e) { |
||||
| 463 | $log('WARN', 'Unable to decrypt instance backup key: ' . $e->getMessage()); |
||||
| 464 | } |
||||
| 465 | |||||
| 466 | $keysToTry = array_values(array_unique(array_filter($keysToTry, function ($v) { return $v !== ''; }))); |
||||
| 467 | |||||
| 468 | // Decrypt backup file to temp SQL |
||||
| 469 | // IMPORTANT: do NOT use tempnam() here. It creates the file and can break Defuse file decrypt. |
||||
| 470 | $tmpRand = ''; |
||||
| 471 | try { |
||||
| 472 | $tmpRand = bin2hex(random_bytes(4)); |
||||
| 473 | } catch (Throwable $ignored) { |
||||
| 474 | $tmpRand = uniqid('', true); |
||||
| 475 | } |
||||
| 476 | |||||
| 477 | $tmpSql = rtrim((string) sys_get_temp_dir(), '/\\') . '/defuse_temp_restore_' . getmypid() . '_' . time() . '_' . $tmpRand . '.sql'; |
||||
| 478 | |||||
| 479 | $dec = tpDefuseDecryptWithCandidates($file, $tmpSql, $keysToTry, $SETTINGS); |
||||
| 480 | if (empty($dec['success'])) { |
||||
| 481 | @unlink($tmpSql); |
||||
|
0 ignored issues
–
show
It seems like you do not handle an error condition for
unlink(). This can introduce security issues, and is generally not recommended.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
If you suppress an error, we recommend checking for the error condition explicitly: // For example instead of
@mkdir($dir);
// Better use
if (@mkdir($dir) === false) {
throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
|
|||||
| 482 | $log('ERROR', 'Decrypt failed: ' . (string) ($dec['message'] ?? '')); |
||||
| 483 | $payload['status'] = 'failed'; |
||||
| 484 | $payload['finished_at'] = time(); |
||||
| 485 | $payload['error'] = 'decrypt_failed'; |
||||
| 486 | tpRestoreAuthorizationUpdatePayload($authId, $payload); |
||||
| 487 | exit(41); |
||||
| 488 | } |
||||
| 489 | |||||
| 490 | $log('INFO', 'Decrypt OK. Importing SQL...'); |
||||
| 491 | |||||
| 492 | // Restore SQL (chunked) |
||||
| 493 | $totalSize = filesize($tmpSql); |
||||
| 494 | if ($totalSize === false) { |
||||
| 495 | $totalSize = 0; |
||||
| 496 | } |
||||
| 497 | $offset = 0; |
||||
| 498 | $batchSize = 500; |
||||
| 499 | $totalExecuted = 0; |
||||
| 500 | |||||
| 501 | $handle = fopen($tmpSql, 'r'); |
||||
| 502 | if ($handle === false) { |
||||
| 503 | @unlink($tmpSql); |
||||
| 504 | $log('ERROR', 'Unable to open decrypted SQL file.'); |
||||
| 505 | exit(42); |
||||
| 506 | } |
||||
| 507 | |||||
| 508 | try { |
||||
| 509 | while (true) { |
||||
| 510 | if ($offset > 0) { |
||||
| 511 | fseek($handle, $offset); |
||||
| 512 | } |
||||
| 513 | |||||
| 514 | $query = ''; |
||||
| 515 | $executed = 0; |
||||
| 516 | $inMultiLineComment = false; |
||||
| 517 | $errors = []; |
||||
| 518 | |||||
| 519 | try { |
||||
| 520 | DB::startTransaction(); |
||||
| 521 | DB::query("SET FOREIGN_KEY_CHECKS = 0"); |
||||
| 522 | DB::query("SET UNIQUE_CHECKS = 0"); |
||||
| 523 | |||||
| 524 | while (!feof($handle) && $executed < $batchSize) { |
||||
| 525 | $line = fgets($handle); |
||||
| 526 | if ($line === false) { |
||||
| 527 | break; |
||||
| 528 | } |
||||
| 529 | |||||
| 530 | $trimmedLine = trim($line); |
||||
| 531 | |||||
| 532 | // Skip empty lines or comments |
||||
| 533 | if ( |
||||
| 534 | $trimmedLine === '' |
||||
| 535 | || strpos($trimmedLine, '--') === 0 |
||||
| 536 | || strpos($trimmedLine, '#') === 0 |
||||
| 537 | ) { |
||||
| 538 | continue; |
||||
| 539 | } |
||||
| 540 | |||||
| 541 | // Handle multi-line comments |
||||
| 542 | if (strpos($trimmedLine, '/*') === 0 && strpos($trimmedLine, '*/') === false) { |
||||
| 543 | $inMultiLineComment = true; |
||||
| 544 | continue; |
||||
| 545 | } |
||||
| 546 | if ($inMultiLineComment) { |
||||
| 547 | if (strpos($trimmedLine, '*/') !== false) { |
||||
| 548 | $inMultiLineComment = false; |
||||
| 549 | } |
||||
| 550 | continue; |
||||
| 551 | } |
||||
| 552 | |||||
| 553 | $query .= $line; |
||||
| 554 | |||||
| 555 | // Execute query if it ends with semicolon |
||||
| 556 | if (substr(rtrim($query), -1) === ';') { |
||||
| 557 | try { |
||||
| 558 | DB::query($query); |
||||
| 559 | $executed++; |
||||
| 560 | $totalExecuted++; |
||||
| 561 | } catch (Throwable $e) { |
||||
| 562 | $errors[] = $e->getMessage(); |
||||
| 563 | $log('ERROR', 'SQL error: ' . $e->getMessage()); |
||||
| 564 | // Stop early on first error |
||||
| 565 | break; |
||||
| 566 | } |
||||
| 567 | $query = ''; |
||||
| 568 | } |
||||
| 569 | } |
||||
| 570 | |||||
| 571 | if (empty($errors)) { |
||||
| 572 | DB::commit(); |
||||
| 573 | } else { |
||||
| 574 | DB::rollback(); |
||||
| 575 | } |
||||
| 576 | } catch (Throwable $e) { |
||||
| 577 | try { |
||||
| 578 | DB::query("SET FOREIGN_KEY_CHECKS = 1"); |
||||
| 579 | DB::query("SET UNIQUE_CHECKS = 1"); |
||||
| 580 | } catch (Throwable $ignored) { |
||||
| 581 | // ignore |
||||
| 582 | } |
||||
| 583 | DB::rollback(); |
||||
| 584 | $errors[] = $e->getMessage(); |
||||
| 585 | $log('ERROR', 'Restore chunk failed: ' . $e->getMessage()); |
||||
| 586 | } finally { |
||||
| 587 | try { |
||||
| 588 | DB::query("SET FOREIGN_KEY_CHECKS = 1"); |
||||
| 589 | DB::query("SET UNIQUE_CHECKS = 1"); |
||||
| 590 | } catch (Throwable $ignored) { |
||||
| 591 | // ignore |
||||
| 592 | } |
||||
| 593 | } |
||||
| 594 | |||||
| 595 | $newOffset = ftell($handle); |
||||
| 596 | if ($newOffset === false) { |
||||
| 597 | $newOffset = $offset; |
||||
| 598 | } |
||||
| 599 | |||||
| 600 | |||||
| 601 | // Progress (single-line bar on STDERR to avoid flooding the terminal) |
||||
| 602 | if ($totalSize > 0) { |
||||
| 603 | $pct = (int) floor(($newOffset / $totalSize) * 100); |
||||
| 604 | if ($pct > 100) { |
||||
| 605 | $pct = 100; |
||||
| 606 | } |
||||
| 607 | tpCliProgressBar($pct, $totalExecuted, $interactive); |
||||
| 608 | |||||
| 609 | // Log progress in file every 10% |
||||
| 610 | static $lastLoggedPct = -1; |
||||
| 611 | if ($pct !== $lastLoggedPct && ($pct % 10 === 0 || $pct === 100)) { |
||||
| 612 | $lastLoggedPct = $pct; |
||||
| 613 | @file_put_contents( |
||||
|
0 ignored issues
–
show
It seems like you do not handle an error condition for
file_put_contents(). This can introduce security issues, and is generally not recommended.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
If you suppress an error, we recommend checking for the error condition explicitly: // For example instead of
@mkdir($dir);
// Better use
if (@mkdir($dir) === false) {
throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
|
|||||
| 614 | $logFile, |
||||
| 615 | '[' . date('c') . '][INFO] Progress: ' . $pct . '% (statements executed: ' . $totalExecuted . ')' . PHP_EOL, |
||||
| 616 | FILE_APPEND |
||||
| 617 | ); |
||||
| 618 | } |
||||
| 619 | } |
||||
| 620 | |||||
| 621 | if (!empty($errors)) { |
||||
| 622 | throw new RuntimeException('SQL restore failed: ' . implode(' | ', $errors)); |
||||
| 623 | } |
||||
| 624 | |||||
| 625 | // End conditions |
||||
| 626 | if (feof($handle) || $newOffset >= $totalSize) { |
||||
| 627 | break; |
||||
| 628 | } |
||||
| 629 | |||||
| 630 | // Continue with next offset |
||||
| 631 | $offset = $newOffset; |
||||
| 632 | } |
||||
| 633 | |||||
| 634 | $log('INFO', 'SQL import completed. Total statements executed: ' . $totalExecuted); |
||||
| 635 | |||||
| 636 | $payload['status'] = 'success'; |
||||
| 637 | $payload['finished_at'] = time(); |
||||
| 638 | tpRestoreAuthorizationUpdatePayload($authId, $payload); |
||||
| 639 | |||||
| 640 | $exitCode = 0; |
||||
| 641 | } catch (Throwable $e) { |
||||
| 642 | $payload['status'] = 'failed'; |
||||
| 643 | $payload['finished_at'] = time(); |
||||
| 644 | $payload['error'] = $e->getMessage(); |
||||
| 645 | tpRestoreAuthorizationUpdatePayload($authId, $payload); |
||||
| 646 | |||||
| 647 | $log('ERROR', 'Restore failed: ' . $e->getMessage()); |
||||
| 648 | $exitCode = 50; |
||||
| 649 | } finally { |
||||
| 650 | if (is_resource($handle)) { |
||||
| 651 | fclose($handle); |
||||
| 652 | } |
||||
| 653 | @unlink($tmpSql); |
||||
| 654 | |||||
| 655 | // Re-force maintenance mode ON at the end (dump may have restored it to 0). |
||||
| 656 | try { |
||||
| 657 | DB::update( |
||||
| 658 | prefixTable('misc'), |
||||
| 659 | [ |
||||
| 660 | 'valeur' => '1', |
||||
| 661 | 'updated_at' => time(), |
||||
| 662 | ], |
||||
| 663 | 'intitule = %s AND type = %s', |
||||
| 664 | 'maintenance_mode', |
||||
| 665 | 'admin' |
||||
| 666 | ); |
||||
| 667 | } catch (Throwable $e) { |
||||
| 668 | try { |
||||
| 669 | DB::update( |
||||
| 670 | prefixTable('misc'), |
||||
| 671 | [ |
||||
| 672 | 'valeur' => '1', |
||||
| 673 | ], |
||||
| 674 | 'intitule = %s AND type = %s', |
||||
| 675 | 'maintenance_mode', |
||||
| 676 | 'admin' |
||||
| 677 | ); |
||||
| 678 | } catch (Throwable $ignored) { |
||||
| 679 | // ignore |
||||
| 680 | } |
||||
| 681 | } |
||||
| 682 | |||||
| 683 | // Release lock |
||||
| 684 | try { |
||||
| 685 | flock($lockHandle, LOCK_UN); |
||||
| 686 | } catch (Throwable $ignored) { |
||||
| 687 | // ignore |
||||
| 688 | } |
||||
| 689 | @fclose($lockHandle); |
||||
|
0 ignored issues
–
show
It seems like you do not handle an error condition for
fclose(). This can introduce security issues, and is generally not recommended.
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
If you suppress an error, we recommend checking for the error condition explicitly: // For example instead of
@mkdir($dir);
// Better use
if (@mkdir($dir) === false) {
throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
|
|||||
| 690 | } |
||||
| 691 | |||||
| 692 | exit($exitCode); |
||||
| 693 |
If you suppress an error, we recommend checking for the error condition explicitly: