Fixes from the phase-1 code review
- one locking protocol per location: backup steps and restores hold a shared lock, prune and untrash an exclusive one and refuse while any run (backup, restore, export) exists; starting a backup checks and creates the run under the exclusive lock, closing the race with prune - snapshot entries are validated when read (relative path without '.', '..', NUL, backslash or control characters); raw restore also checks that the resolved directory stays below the target; the restore API and service accept canonical paths only - recovery kit confirmation requires a recent password confirmation and the confirmation code printed in the current kit file (new code per kit version), proving the kit was actually downloaded - prune decides on the authenticated index files on the location and refreshes the local index cache from them - restore job marks the run failed on any error; restore runs without progress for 6 hours are failed so they cannot block cleanup forever - tests for safe paths and a malicious snapshot path
This commit is contained in:
@@ -120,4 +120,30 @@ class RepositoryTest extends TestCase {
|
||||
$this->expectException(CryptoException::class);
|
||||
$repo->getObject('db/2');
|
||||
}
|
||||
|
||||
public function testSafePaths(): void {
|
||||
foreach (['data/alice/files/a.txt', 'config/config.php', 'a b/c-d_e.f'] as $ok) {
|
||||
$this->assertTrue(Repository::isSafePath($ok), $ok);
|
||||
}
|
||||
foreach (['', '/etc/passwd', '../x', 'a/../b', 'a/./b', 'a//b', 'a/', "a\0b", 'a\\b', "a\nb"] as $bad) {
|
||||
$this->assertFalse(Repository::isSafePath($bad), json_encode($bad));
|
||||
}
|
||||
}
|
||||
|
||||
public function testMaliciousSnapshotPathIsRejectedOnRestore(): void {
|
||||
$repo = Repository::init($this->backend, 'pw');
|
||||
$stats = [];
|
||||
$blob = $repo->storeData('evil', $stats);
|
||||
$repo->flushPacks();
|
||||
$repo->writeSnapshot('bad', ['time' => gmdate('c')], [json_encode(['p' => '../../escaped.txt', 's' => 4, 'm' => 0, 'b' => [$blob]])]);
|
||||
$target = $this->tempDir() . '/inner';
|
||||
mkdir($target);
|
||||
try {
|
||||
$repo->restore('bad', $target);
|
||||
$this->fail('restore of a ../ path must fail');
|
||||
} catch (RepositoryException) {
|
||||
}
|
||||
$this->assertFileDoesNotExist(dirname($target, 2) . '/escaped.txt');
|
||||
$this->assertFileDoesNotExist(dirname($target) . '/escaped.txt');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user