Skip to content

Commit cd47e8a

Browse files
committed
feat(update): enhance backup filename security with random hash
- Add 8-char hex hash to backup filenames for unpredictability - Implement caching of generated filename in Update class - Update tests to validate new filename format with regex BREAKING CHANGE: Backup filename format changed from phpmyfaq-config-backup.YYYY-MM-DD.zip to phpmyfaq-config-backup.YYYY-MM-DD.XXXXXXXX.zip
1 parent 87bf24a commit cd47e8a

File tree

2 files changed

+38
-5
lines changed

2 files changed

+38
-5
lines changed

phpmyfaq/src/phpMyFAQ/Setup/Update.php

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
use phpMyFAQ\Forms;
3030
use phpMyFAQ\System;
3131
use phpMyFAQ\User;
32+
use Random\RandomException;
3233
use RecursiveDirectoryIterator;
3334
use RecursiveIteratorIterator;
3435
use SplFileInfo;
@@ -47,6 +48,8 @@ class Update extends AbstractSetup
4748
/** @var string[] */
4849
private array $dryRunQueries = [];
4950

51+
private ?string $backupFilename = null;
52+
5053
public function __construct(
5154
protected System $system,
5255
private readonly Configuration $configuration,
@@ -72,6 +75,7 @@ public function isConfigTableNotAvailable(DatabaseDriver $databaseDriver): bool
7275
/**
7376
* Creates a backup of the current config files
7477
* @throws Exception
78+
* @throws RandomException
7579
*/
7680
public function createConfigBackup(string $configDir): string
7781
{
@@ -113,7 +117,7 @@ public function createConfigBackup(string $configDir): string
113117
continue;
114118
}
115119

116-
// Compute relative path inside archive
120+
// Compute a relative path inside the archive
117121
$relativePath = str_replace($configDir . DIRECTORY_SEPARATOR, '', $filePath);
118122
$relativePath = ltrim($relativePath, DIRECTORY_SEPARATOR);
119123

@@ -1187,8 +1191,15 @@ private function updateVersion(): void
11871191
$this->configuration->update(['main.currentVersion' => System::getVersion()]);
11881192
}
11891193

1194+
/**
1195+
* @throws RandomException
1196+
*/
11901197
private function getBackupFilename(): string
11911198
{
1192-
return sprintf('phpmyfaq-config-backup.%s.zip', date(format: 'Y-m-d'));
1199+
if ($this->backupFilename === null) {
1200+
$randomHash = bin2hex(random_bytes(4)); // 8-character hex string
1201+
$this->backupFilename = sprintf('phpmyfaq-config-backup.%s.%s.zip', date(format: 'Y-m-d'), $randomHash);
1202+
}
1203+
return $this->backupFilename;
11931204
}
11941205
}

tests/phpMyFAQ/Setup/UpdateTest.php

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use phpMyFAQ\System;
99
use PHPUnit\Framework\TestCase;
1010
use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations;
11+
use Random\RandomException;
1112

1213
#[AllowMockObjectsWithoutExpectations]
1314
class UpdateTest extends TestCase
@@ -29,19 +30,40 @@ protected function setUp(): void
2930

3031
/**
3132
* @throws Exception
33+
* @throws RandomException
3234
*/
3335
public function testCreateConfigBackup(): void
3436
{
3537
$this->update->setVersion('4.0.0');
3638
$configPath = PMF_TEST_DIR . '/content/core/config';
3739

40+
// Clean up any existing backup files before test
41+
$existingFiles = glob($configPath . '/phpmyfaq-config-backup.*.zip');
42+
foreach ($existingFiles as $file) {
43+
if (file_exists($file)) {
44+
unlink($file);
45+
}
46+
}
47+
3848
$this->update->createConfigBackup($configPath);
3949

40-
$this->assertFileExists(
41-
PMF_TEST_DIR . '/content/core/config/phpmyfaq-config-backup.' . date(format: 'Y-m-d') . '.zip'
50+
// Find a backup file with a pattern: phpmyfaq-config-backup.YYYY-MM-DD.XXXXXXXX.zip
51+
$pattern = PMF_TEST_DIR . '/content/core/config/phpmyfaq-config-backup.' . date(format: 'Y-m-d') . '.*.zip';
52+
$files = glob($pattern);
53+
54+
$this->assertNotEmpty($files, 'Backup file should exist with random hash');
55+
$this->assertCount(1, $files, 'Exactly one backup file should exist');
56+
57+
// Verify filename format: date.hash.zip where hash is 8 hex characters
58+
$filename = basename($files[0]);
59+
$this->assertMatchesRegularExpression(
60+
'/^phpmyfaq-config-backup\.\d{4}-\d{2}-\d{2}\.[0-9a-f]{8}\.zip$/',
61+
$filename,
62+
'Backup filename should contain 8-character hexadecimal hash'
4263
);
4364

44-
unlink(PMF_TEST_DIR . '/content/core/config/phpmyfaq-config-backup.' . date(format: 'Y-m-d') . '.zip');
65+
// Cleanup
66+
unlink($files[0]);
4567
}
4668

4769
public function testIsConfigTableNotAvailable(): void

0 commit comments

Comments
 (0)