Issues (32)

Security Analysis    no request data  

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

src/Vault.php (1 issue)

Labels
Severity

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Storeman;
4
5
use Psr\Log\LoggerAwareInterface;
6
use Psr\Log\LoggerAwareTrait;
7
use Psr\Log\NullLogger;
8
use Storeman\Config\VaultConfiguration;
9
use Storeman\ConflictHandler\ConflictHandlerInterface;
10
use Storeman\Hash\HashContainer;
11
use Storeman\Index\Index;
12
use Storeman\Index\IndexObject;
13
use Storeman\Operation\WriteSynchronizationOperation;
14
use Storeman\StorageAdapter\StorageAdapterInterface;
15
use Storeman\IndexMerger\IndexMergerInterface;
16
use Storeman\LockAdapter\LockAdapterInterface;
17
use Storeman\OperationListBuilder\OperationListBuilderInterface;
18
use Storeman\SynchronizationProgressListener\DummySynchronizationProgressListener;
19
use Storeman\SynchronizationProgressListener\SynchronizationProgressListenerInterface;
20
use Storeman\VaultLayout\VaultLayoutInterface;
21
22
/**
23
 * Represents a vault in a set of vaults associated to a storeman (archive) instance.
24
 */
25
class Vault implements LoggerAwareInterface
26
{
27
    use LoggerAwareTrait;
28
29
30
    public const LOCK_SYNC = 'sync';
31
32
33
    /**
34
     * @var Storeman
35
     */
36
    protected $storeman;
37
38
    /**
39
     * @var VaultConfiguration
40
     */
41
    protected $vaultConfiguration;
42
43
    /**
44
     * @var VaultLayoutInterface
45
     */
46
    protected $vaultLayout;
47
48
    /**
49
     * @var StorageAdapterInterface
50
     */
51
    protected $storageAdapter;
52
53
    /**
54
     * @var LockAdapterInterface
55
     */
56
    protected $lockAdapter;
57
58
    /**
59
     * @var IndexMergerInterface
60
     */
61
    protected $indexMerger;
62
63
    /**
64
     * @var ConflictHandlerInterface
65
     */
66
    protected $conflictHandler;
67
68
    /**
69
     * @var OperationListBuilderInterface
70
     */
71
    protected $operationListBuilder;
72
73
    /**
74
     * @var Index
75
     */
76
    protected $lastLocalIndex;
77
78
    public function __construct(Storeman $storeman, VaultConfiguration $vaultConfiguration)
79
    {
80
        $this->storeman = $storeman;
81
        $this->vaultConfiguration = $vaultConfiguration;
82
        $this->logger = new NullLogger();
83
    }
84
85
    public function getStoreman(): Storeman
86
    {
87
        return $this->storeman;
88
    }
89
90
    public function getVaultConfiguration(): VaultConfiguration
91
    {
92
        return $this->vaultConfiguration;
93
    }
94
95
    public function getVaultLayout(): VaultLayoutInterface
96
    {
97
        return $this->vaultLayout ?: ($this->vaultLayout = $this->getContainer()->get('vaultLayout'));
98
    }
99
100
    public function getStorageAdapter(): StorageAdapterInterface
101
    {
102
        return $this->storageAdapter ?: ($this->storageAdapter = $this->getContainer()->get('storageAdapter'));
103
    }
104
105
    public function getLockAdapter(): LockAdapterInterface
106
    {
107
        return $this->lockAdapter ?: ($this->lockAdapter = $this->getContainer()->get('lockAdapter'));
108
    }
109
110
    public function getIndexMerger(): IndexMergerInterface
111
    {
112
        return $this->indexMerger ?: ($this->indexMerger = $this->getContainer()->get('indexMerger'));
113
    }
114
115
    public function getConflictHandler(): ConflictHandlerInterface
116
    {
117
        return $this->conflictHandler ?: ($this->conflictHandler = $this->getContainer()->get('conflictHandler'));
118
    }
119
120
    public function getOperationListBuilder(): OperationListBuilderInterface
121
    {
122
        return $this->operationListBuilder ?: ($this->operationListBuilder = $this->getContainer()->get('operationListBuilder'));
123
    }
124
125
    /**
126
     * Reads and returns the index representing the local state on the last synchronization.
127
     *
128
     * @return Index
129
     * @throws Exception
130
     */
131
    public function getLastLocalIndex(): ?Index
132
    {
133
        if ($this->lastLocalIndex === null)
134
        {
135
            $index = null;
136
            $path = $this->getLastLocalIndexFilePath();
137
138
            if (is_file($path))
139
            {
140
                $this->logger->info("Reading in last local index from {$path}...");
141
142
                $stream = fopen($path, 'rb');
143
144
                $index = new Index();
145
                while (($row = fgetcsv($stream)) !== false)
146
                {
147
                    $index->addObject($this->createIndexObjectFromScalarArray($row));
148
                }
149
150
                fclose($stream);
151
152
                $this->logger->info("Read {$index->count()} records for last local index");
153
            }
154
            else
155
            {
156
                $this->logger->info("No last local index exists");
157
            }
158
159
            $this->lastLocalIndex = $index;
160
        }
161
162
        return $this->lastLocalIndex;
163
    }
164
165
    /**
166
     * Reads and returns the current remote index.
167
     *
168
     * @param int $revision Revision to load. Defaults to the last revision.
169
     *
170
     * @return Index
171
     */
172
    public function getRemoteIndex(int $revision = null): ?Index
173
    {
174
        $this->logger->info(sprintf("Loading %s remote index...", $revision ? "r{$revision}" : 'latest'));
175
176
        $synchronization = $revision ?
177
            $this->getVaultLayout()->getSynchronization($revision) :
178
            $this->getVaultLayout()->getLastSynchronization();
179
180
        return $synchronization ? $synchronization->getIndex() : null;
181
    }
182
183
    /**
184
     * Computes and returns the index representing the vault state after the local index has been merged with the remote index.
185
     *
186
     * @return Index
187
     */
188
    public function getMergedIndex(): Index
189
    {
190
        return $this->doBuildMergedIndex();
191
    }
192
193
    /**
194
     * Synchronizes the local with the remote state by executing all operations returned by getOperationList()
195
     *
196
     * @param int $newRevision
197
     * @param SynchronizationProgressListenerInterface $progressionListener
198
     *
199
     * @return OperationResultList
200
     * @throws Exception
201
     */
202
    public function synchronize(int $newRevision = null, SynchronizationProgressListenerInterface $progressionListener = null): OperationResultList
203
    {
204
        $progressionListener = $progressionListener ?: new DummySynchronizationProgressListener();
205
206
        $localIndex = $this->storeman->getLocalIndex();
207
        $lastLocalIndex = $this->getLastLocalIndex();
208
209
210
        if (!$this->getLockAdapter()->acquireLock(static::LOCK_SYNC))
211
        {
212
            throw new Exception('Failed to acquire lock.');
213
        }
214
215
216
        $lastSynchronization = $this->getVaultLayout()->getLastSynchronization();
217
218
        if ($lastSynchronization)
219
        {
220
            $newRevision = $newRevision ?: ($lastSynchronization->getRevision() + 1);
221
            $remoteIndex = $lastSynchronization->getIndex();
222
        }
223
        else
224
        {
225
            $newRevision = $newRevision ?: 1;
226
            $remoteIndex = null;
227
        }
228
229
        // compute merged index
230
        $mergedIndex = $this->doBuildMergedIndex($localIndex, $lastLocalIndex, $remoteIndex);
231
232
        $synchronization = new Synchronization($newRevision, new \DateTime(), $this->storeman->getConfiguration()->getIdentity(), $mergedIndex);
233
234
        $operationList = $this->getOperationListBuilder()->buildOperationList($mergedIndex, $localIndex);
235
        $operationList->add(new OperationListItem(new WriteSynchronizationOperation($synchronization)));
236
237
        $operationResultList = $this->executeOperationList($operationList, $this->storeman->getConfiguration()->getPath(), $progressionListener);
238
239
        // release lock
240
        if (!$this->getLockAdapter()->releaseLock(static::LOCK_SYNC))
241
        {
242
            throw new Exception('Failed to release lock.');
243
        }
244
245
        // save merged index locally
246
        $this->writeLastLocalIndex($mergedIndex);
247
248
        return $operationResultList;
249
    }
250
251
    /**
252
     * Restores the local state at the given revision from the vault.
253
     *
254
     * @param int $revision
255
     * @param SynchronizationProgressListenerInterface $progressionListener
256
     *
257
     * @return OperationResultList
258
     * @throws Exception
259
     */
260
    public function restore(int $revision = null, SynchronizationProgressListenerInterface $progressionListener = null): OperationResultList
261
    {
262
        return $this->doRestore($revision, $progressionListener);
263
    }
264
265
    /**
266
     * @param string $targetPath
267
     * @param int $revision
268
     * @param SynchronizationProgressListenerInterface|null $progressListener
269
     *
270
     * @return OperationResultList
271
     * @throws \Exception
272
     */
273
    public function dump(string $targetPath, int $revision = null, SynchronizationProgressListenerInterface $progressListener = null): OperationResultList
274
    {
275
        return $this->doRestore($revision, $progressListener, true, $targetPath);
276
    }
277
278
    /**
279
     * Returns a hash that is the same for any vault referencing the same physical storage location.
280
     *
281
     * @return string
282
     */
283
    public function getHash(): string
284
    {
285
        return hash('sha1', implode([
286
            get_class($this->getStorageAdapter()),
287
            $this->getStorageAdapter()->getIdentificationString($this->vaultConfiguration),
288
        ]));
289
    }
290
291
    /**
292
     * Returns an identifier usable for UI.
293
     *
294
     * @return string
295
     */
296
    public function getIdentifier(): string
297
    {
298
        return "{$this->getHash()} ({$this->vaultConfiguration->getTitle()})";
299
    }
300
301
    protected function doBuildMergedIndex(Index $localIndex = null, Index $lastLocalIndex = null, Index $remoteIndex = null): Index
302
    {
303
        $localIndex = $localIndex ?: $this->storeman->getLocalIndex();
304
        $lastLocalIndex = $lastLocalIndex ?: $this->getLastLocalIndex();
305
        $remoteIndex = $remoteIndex ?: $this->getRemoteIndex();
306
307
        if ($remoteIndex === null)
308
        {
309
            return $localIndex;
310
        }
311
312
        return $this->getIndexMerger()->merge($this->getConflictHandler(), $remoteIndex, $localIndex, $lastLocalIndex, IndexMergerInterface::INJECT_BLOBID);
0 ignored issues
show
It seems like $lastLocalIndex defined by $lastLocalIndex ?: $this->getLastLocalIndex() on line 304 can be null; however, Storeman\IndexMerger\IndexMergerInterface::merge() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
313
    }
314
315
    protected function doRestore(int $revision = null, SynchronizationProgressListenerInterface $progressionListener = null, bool $skipLastLocalIndexUpdate = false, string $targetPath = null): OperationResultList
316
    {
317
        $progressionListener = $progressionListener ?: new DummySynchronizationProgressListener();
318
        $targetPath = $targetPath ?: $this->storeman->getConfiguration()->getPath();
319
320
        $localIndex = $this->storeman->getLocalIndex($targetPath);
321
322
        if (!$this->getLockAdapter()->acquireLock(static::LOCK_SYNC))
323
        {
324
            throw new Exception('Failed to acquire lock.');
325
        }
326
327
        // fall back to last revision
328
        if ($revision === null)
329
        {
330
            $lastSynchronization = $this->getVaultLayout()->getLastSynchronization();
331
332
            if (!$lastSynchronization)
333
            {
334
                throw new Exception('No revision to restore from.');
335
            }
336
337
            $revision = $lastSynchronization->getRevision();
338
        }
339
340
        $remoteIndex = $this->getRemoteIndex($revision);
341
342
        if ($remoteIndex === null)
343
        {
344
            throw new Exception("Unknown revision: {$revision}");
345
        }
346
347
        $operationList = $this->getOperationListBuilder()->buildOperationList($remoteIndex, $localIndex);
348
349
        $operationResultList = $this->executeOperationList($operationList, $targetPath, $progressionListener);
350
351
        if (!$this->getLockAdapter()->releaseLock(static::LOCK_SYNC))
352
        {
353
            throw new Exception('Failed to release lock.');
354
        }
355
356
        if (!$skipLastLocalIndexUpdate)
357
        {
358
            $this->writeLastLocalIndex($remoteIndex);
359
        }
360
361
        return $operationResultList;
362
    }
363
364
    protected function executeOperationList(OperationList $operationList, string $basePath, SynchronizationProgressListenerInterface $progressionListener): OperationResultList
365
    {
366
        $this->logger->notice(sprintf("Executing %d operation(s)...", count($operationList)));
367
368
        $operationResultList = new OperationResultList();
369
370
        $progressionListener->start(count($operationList));
371
        foreach ($operationList as $index => $operationListItem)
372
        {
373
            /** @var OperationListItem $operationListItem */
374
375
            $this->logger->debug(sprintf("#%d {$operationListItem->getOperation()}", $index + 1));
376
377
            $success = $operationListItem->getOperation()->execute($basePath, $this->storeman->getFileReader(), $this->getVaultLayout());
378
379
            // update index ctime right after operation execution to make it as reliable as possible for usage in the next run
380
            if ($success && $indexObject = $operationListItem->getIndexObject())
381
            {
382
                $absolutePath = PathUtils::getAbsolutePath($basePath . $indexObject->getRelativePath());
383
384
                if (is_file($absolutePath))
385
                {
386
                    $indexObject->setCtime(FilesystemUtility::lstat($absolutePath)['ctime']);
387
                }
388
            }
389
390
            $operationResult = new OperationResult($operationListItem->getOperation(), $success);
391
            $operationResultList->addOperationResult($operationResult);
392
393
            $progressionListener->advance();
394
        }
395
        $progressionListener->finish();
396
397
        return $operationResultList;
398
    }
399
400
    protected function writeLastLocalIndex(Index $index): void
401
    {
402
        $this->logger->info(sprintf("Writing last local index with %d records to %s", $index->count(), $this->getLastLocalIndexFilePath()));
403
404
        // prevent outdated cache on failure
405
        $this->lastLocalIndex = null;
406
407
        $stream = fopen($this->getLastLocalIndexFilePath(), 'wb');
408
409
        foreach ($index as $object)
410
        {
411
            /** @var IndexObject $object */
412
413
            if (fputcsv($stream, $this->indexObjectToScalarArray($object)) === false)
414
            {
415
                throw new Exception("Writing to {$this->getLastLocalIndexFilePath()} failed");
416
            }
417
        }
418
419
        fclose($stream);
420
421
        // update local cache
422
        $this->lastLocalIndex = $index;
423
    }
424
425
    /**
426
     * Transforms an IndexObject instance into a scalar array suitable for fputcsv().
427
     *
428
     * @param IndexObject $indexObject
429
     * @return array
430
     */
431
    protected function indexObjectToScalarArray(IndexObject $indexObject): array
432
    {
433
        return [
434
            $indexObject->getRelativePath(),
435
            $indexObject->getType(),
436
            sprintf('%.9f', $indexObject->getMtime()),
437
            sprintf('%.9f', $indexObject->getCtime()),
438
            $indexObject->getPermissions(),
439
            $indexObject->getSize(),
440
            $indexObject->getInode(),
441
            $indexObject->getLinkTarget(),
442
            $indexObject->getBlobId(),
443
            $indexObject->getHashes() ? $indexObject->getHashes()->serialize() : null,
444
        ];
445
    }
446
447
    /**
448
     * Reconstructs an IndexObject instance from a scalar array read by fgetcsv().
449
     *
450
     * @param array $array
451
     * @return IndexObject
452
     */
453
    protected function createIndexObjectFromScalarArray(array $array): IndexObject
454
    {
455
        return new IndexObject(
456
            $array[0],
457
            (int)$array[1],
458
            (float)$array[2],
459
            (float)$array[3],
460
            (int)$array[4],
461
            ($array[5] !== '') ? (int)$array[5] : null,
462
            (int)$array[6],
463
            $array[7] ?: null,
464
            $array[8] ?: null,
465
            $array[9] ? (new HashContainer())->unserialize($array[9]) : null
466
        );
467
    }
468
469
    protected function getLastLocalIndexFilePath(): string
470
    {
471
        return $this->storeman->getMetadataDirectoryPath() . sprintf('lastLocalIndex-%s', $this->getHash());
472
    }
473
474
    /**
475
     * Returns the service container with this vault as its context.
476
     *
477
     * @return Container
478
     */
479
    protected function getContainer(): Container
480
    {
481
        return $this->storeman->getContainer($this);
482
    }
483
}
484