Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
22 / 22
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
SafeStoragePathResolver
100.00% covered (success)
100.00%
21 / 21
100.00% covered (success)
100.00%
3 / 3
14
100.00% covered (success)
100.00%
1 / 1
 resolve
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
7
 resolveWritable
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
6
 isPathInsideBase
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5/** @license For full copyright and license information, please see the LICENSE.md file. */
6
7namespace App\Core\Security\Path;
8
9defined('AMMONLY_APP') || exit('Direct script access is forbidden.');
10
11/**
12 * Enterprise Safe Storage Path Resolver.
13 *
14 * Enforces strict directory sandboxing to eliminate Directory / Path Traversal (LFI)
15 * vulnerabilities during file read, write, and deletion operations.
16 *
17 * @package App\Core\Security\Path
18 */
19final readonly class SafeStoragePathResolver
20{
21    /**
22     * Resolves an absolute path for reading an existing file within a sandboxed base directory.
23     *
24     * @param string $baseDir      The allowed root directory (must exist).
25     * @param string $relativePath Relative file path to verify and resolve.
26     * @return string|null Absolute canonical path if safe and exists, null otherwise.
27     */
28    public static function resolve(string $baseDir, string $relativePath): ?string
29    {
30        $realBase = realpath($baseDir);
31        if (str_contains($relativePath, "\0") || trim($relativePath) === '' || $realBase === false) {
32            return null;
33        }
34
35        $normalizedRel = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $relativePath);
36        $fullPath = $realBase . DIRECTORY_SEPARATOR . ltrim($normalizedRel, DIRECTORY_SEPARATOR);
37
38        $realTarget = realpath($fullPath);
39        if ($realTarget === false || !is_file($realTarget)) {
40            return null;
41        }
42
43        return self::isPathInsideBase($realBase, $realTarget) ? $realTarget : null;
44    }
45
46    /**
47     * Resolves an absolute path for creating or deleting a file within a sandboxed base directory.
48     *
49     * @param string $baseDir      The allowed root directory (must exist).
50     * @param string $relativePath Relative file path to verify.
51     * @return string|null Absolute canonical path if safe, null otherwise.
52     */
53    public static function resolveWritable(string $baseDir, string $relativePath): ?string
54    {
55        $realBase = realpath($baseDir);
56        if (str_contains($relativePath, "\0") || trim($relativePath) === '' || $realBase === false) {
57            return null;
58        }
59
60        $normalizedRel = str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $relativePath);
61        $fullPath = $realBase . DIRECTORY_SEPARATOR . ltrim($normalizedRel, DIRECTORY_SEPARATOR);
62
63        $realTargetDir = realpath(dirname($fullPath));
64        if ($realTargetDir === false) {
65            return null;
66        }
67
68        $candidatePath = $realTargetDir . DIRECTORY_SEPARATOR . basename($fullPath);
69        return self::isPathInsideBase($realBase, $candidatePath) ? $candidatePath : null;
70    }
71
72    /**
73     * Verifies that the candidate path resides strictly within the base directory.
74     *
75     * @param string $realBase      Canonical base directory.
76     * @param string $candidatePath Canonical or normalized candidate path.
77     * @return bool True if inside base directory, false otherwise.
78     */
79    private static function isPathInsideBase(string $realBase, string $candidatePath): bool
80    {
81        $baseWithSep = rtrim($realBase, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
82        return str_starts_with($candidatePath, $baseWithSep);
83    }
84}