1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
|
<?php
declare(strict_types=1);
require(__DIR__ . '/../constants.php');
// Supported types with their associated content type
const SUPPORTED_TYPES = [
'css' => 'text/css; charset=UTF-8',
'js' => 'application/javascript; charset=UTF-8',
'png' => 'image/png',
'jpeg' => 'image/jpeg',
'jpg' => 'image/jpeg',
'gif' => 'image/gif',
'svg' => 'image/svg+xml',
];
function get_absolute_filename(string $file_name): string {
$core_extension = realpath(CORE_EXTENSIONS_PATH . '/' . $file_name);
if (false !== $core_extension) {
return $core_extension;
}
$extension = realpath(EXTENSIONS_PATH . '/' . $file_name);
if (false !== $extension) {
return $extension;
}
$third_party_extension = realpath(THIRDPARTY_EXTENSIONS_PATH . '/' . $file_name);
if (false !== $third_party_extension) {
return $third_party_extension;
}
$user = realpath(USERS_PATH . '/' . $file_name);
if (false !== $user) {
return $user;
}
return '';
}
function is_valid_path_extension(string $path, string $extensionPath, bool $isStatic = true): bool {
// It must be under the extension path.
$real_ext_path = realpath($extensionPath);
if ($real_ext_path == false) {
return false;
}
//Windows compatibility
$real_ext_path = str_replace('\\', '/', $real_ext_path);
$path = str_replace('\\', '/', $path);
$in_ext_path = (str_starts_with($path, $real_ext_path));
if (!$in_ext_path) {
return false;
}
// User files do not need further validations
if (!$isStatic) {
return true;
}
// Static files to serve must be under a `ext_dir/static/` directory.
$path_relative_to_ext = substr($path, strlen($real_ext_path) + 1);
[, $static, $file] = sscanf($path_relative_to_ext, '%[^/]/%[^/]/%s') ?? [null, null, null];
if (null === $file || 'static' !== $static) {
return false;
}
return true;
}
/**
* Check if a file can be served by ext.php. A valid file is under a
* CORE_EXTENSIONS_PATH/extension_name/static/ or THIRDPARTY_EXTENSIONS_PATH/extension_name/static/ directory.
*
* You should sanitize path by using the realpath() function.
*
* @param string $path the path to the file we want to serve.
* @return bool true if it can be served, false otherwise.
*/
function is_valid_path(string $path): bool {
return is_valid_path_extension($path, CORE_EXTENSIONS_PATH) || is_valid_path_extension($path, THIRDPARTY_EXTENSIONS_PATH)
|| is_valid_path_extension($path, USERS_PATH, false);
}
function sendBadRequestResponse(?string $message = null): never {
header('HTTP/1.1 400 Bad Request');
die($message);
}
function sendNotFoundResponse(): never {
header('HTTP/1.1 404 Not Found');
die();
}
if (!isset($_GET['f'], $_GET['t']) || !is_string($_GET['f']) || !is_string($_GET['t'])) {
sendBadRequestResponse('Query string is incomplete.');
}
$file_name = urldecode($_GET['f']);
$file_type = $_GET['t'];
if (empty(SUPPORTED_TYPES[$file_type]) ||
empty(SUPPORTED_TYPES[pathinfo($file_name, PATHINFO_EXTENSION)])) {
sendBadRequestResponse('File type is not supported.');
}
// Forbid absolute paths and path traversal
if (str_contains($file_name, '..') || str_starts_with($file_name, '/') || str_starts_with($file_name, '\\')) {
sendBadRequestResponse('File is not supported.');
}
$absolute_filename = get_absolute_filename($file_name);
if (!is_valid_path($absolute_filename)) {
sendBadRequestResponse('File is not supported.');
}
$content_type = SUPPORTED_TYPES[$file_type];
header("Content-Type: {$content_type}");
header("Content-Disposition: inline; filename='{$file_name}'");
header('Referrer-Policy: same-origin');
$mtime = @filemtime($absolute_filename);
if ($mtime === false) {
sendNotFoundResponse();
}
require(LIB_PATH . '/http-conditional.php');
if (!httpConditional($mtime, 604800, 2)) {
readfile($absolute_filename);
}
|