PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.6.4
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.6.4
1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 1.3.19 trunk 1.2.0 All 47 releases
fluent-cart / app / Services / FileSystem / StoragePath.php

StoragePath.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.6.4, at app/Services/FileSystem/StoragePath.php

112 lines 3.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Services\FileSystem;
4
5 /**
6 * Resolves a user supplied file path against a storage directory.
7 *
8 * `file_path` reaches the storage drivers from request input on several
9 * surfaces, and the drivers compose it onto their directory by plain string
10 * concatenation. Neither sanitize_text_field() nor wp_normalize_path() resolves
11 * a `..` segment, so the resolution has to happen here — once, so the path a
12 * write boundary stores and the path a read boundary consumes are reduced the
13 * same way.
14 *
15 * A leading separator is deliberately treated as relative rather than refused:
16 * `"{$dir}/" . "/etc/passwd"` always resolved inside the storage directory, so
17 * rejecting it would break stored paths that were never an escape.
18 */
19 class StoragePath
20 {
21 /**
22 * Reduce a path to the segments that stay inside a storage directory.
23 *
24 * @param mixed $filePath
25 * @return string The contained relative path, or '' when it escapes.
26 */
27 public static function relative($filePath): string
28 {
29 if (!is_string($filePath) && !is_numeric($filePath)) {
30 return '';
31 }
32
33 $segments = [];
34
35 foreach (explode('/', str_replace('\\', '/', (string)$filePath)) as $segment) {
36 if ($segment === '' || $segment === '.') {
37 continue;
38 }
39
40 if ($segment === '..') {
41 if (!$segments) {
42 // Walks above the storage directory.
43 return '';
44 }
45
46 array_pop($segments);
47 continue;
48 }
49
50 $segments[] = $segment;
51 }
52
53 if (!$segments) {
54 return '';
55 }
56
57 return implode('/', $segments);
58 }
59
60 /**
61 * Whether a path can be stored and later resolved inside a storage directory.
62 *
63 * @param mixed $filePath
64 * @return bool
65 */
66 public static function isSafe($filePath): bool
67 {
68 return static::relative($filePath) !== '';
69 }
70
71 /**
72 * Absolute path for $filePath inside $basePath.
73 *
74 * The file does not have to exist, so the result can address a destination
75 * that is about to be written. When it does exist its real path is checked
76 * too, because a symlink inside the directory can still point out of it.
77 *
78 * @param mixed $basePath
79 * @param mixed $filePath
80 * @return string The absolute path, or '' when it escapes $basePath.
81 */
82 public static function contain($basePath, $filePath): string
83 {
84 if (!is_string($basePath) || $basePath === '') {
85 return '';
86 }
87
88 $relativePath = static::relative($filePath);
89
90 if ($relativePath === '') {
91 return '';
92 }
93
94 $realBasePath = realpath($basePath);
95 $basePath = rtrim(str_replace('\\', '/', $realBasePath ? $realBasePath : $basePath), '/');
96
97 if ($basePath === '') {
98 return '';
99 }
100
101 $fullPath = $basePath . '/' . $relativePath;
102 $realFullPath = realpath($fullPath);
103
104 if ($realFullPath && strpos(str_replace('\\', '/', $realFullPath), $basePath . '/') !== 0) {
105 // Resolved through a symlink that leaves the storage directory.
106 return '';
107 }
108
109 return $fullPath;
110 }
111 }
112