Skip to content

Commit d0597c9

Browse files
committed
update.php fixes
Restrict update.php '#file' writes to the expected configuration roots (/boot/config, /etc/wireguard) so a write cannot land outside config storage via an absolute path or '..'. The target is resolved with realpath() and range-checked with a shared in_safe_path() helper, which is promoted into Wrappers.php; FileUpload.php now uses that shared copy. Refs: OS-489
1 parent de7bf2f commit d0597c9

3 files changed

Lines changed: 30 additions & 5 deletions

File tree

emhttp/plugins/dynamix/include/FileUpload.php

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,7 @@
2121
$safeexts = ['.png'];
2222
$result = false;
2323

24-
function in_safe_path(string $path, string $base): bool {
25-
$path = rtrim($path, '/').'/';
26-
$base = rtrim($base, '/').'/';
27-
return strpos($path, $base) === 0;
28-
}
24+
// in_safe_path() is provided by the shared Wrappers.php (pulled in via Helpers.php)
2925

3026
function remove_tree(string $dir): bool {
3127
if (!is_dir($dir)) return false;

emhttp/plugins/dynamix/include/Wrappers.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ function file_put_contents_atomic($filename,$data) {
3939
return strlen($data);
4040
}
4141

42+
// Returns true when $path is contained within $base. Both arguments should be
43+
// canonicalized by the caller (e.g. via realpath) so that '..' segments and
44+
// symlinks cannot be used to escape $base.
45+
function in_safe_path(string $path, string $base): bool {
46+
$path = rtrim($path, '/').'/';
47+
$base = rtrim($base, '/').'/';
48+
return strpos($path, $base) === 0;
49+
}
50+
4251
// custom parse_ini_file/string functions to deal with '#' comment lines and remove html/php tags
4352
function my_parse_ini_string($text, $sections=false, $scanner=INI_SCANNER_NORMAL) {
4453
return parse_ini_string(strip_tags(html_entity_decode(preg_replace('/^#.*$/m','',$text))),$sections,$scanner);

emhttp/update.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,26 @@ function write_log($string) {
6262
$raw_file = isset($_POST['#raw_file']) ? ($_POST['#raw_file'] === 'true') : false;
6363
// prepend with boot (flash) if path is relative
6464
if ($file && $file[0]!='/') $file = "/boot/config/plugins/$file";
65+
// Confine '#file' to the allowed config roots so it can only write inside
66+
// its expected locations. Resolve the nearest existing ancestor with
67+
// realpath() (collapsing '..' and symlinks) since the target file/dir may
68+
// not exist yet, then range-check it with the shared in_safe_path() helper.
69+
$allowed_roots = ['/boot/config', '/etc/wireguard'];
70+
// walk up from the target's directory to the nearest directory that exists,
71+
// so confinement works regardless of how deep the not-yet-created target is
72+
$resolved_dir = false;
73+
for ($probe = dirname($file); ; $probe = dirname($probe)) {
74+
if (($real = realpath($probe)) !== false) { $resolved_dir = $real; break; }
75+
if (dirname($probe) === $probe) break; // reached '/' with nothing existing
76+
}
77+
$file_allowed = false;
78+
if (strpos($file, '..') === false && $resolved_dir !== false)
79+
foreach ($allowed_roots as $root)
80+
if (($base = realpath($root)) && in_safe_path($resolved_dir, $base)) { $file_allowed = true; break; }
81+
if (!$file_allowed) {
82+
syslog(LOG_WARNING, "update.php: refused '#file' write outside allowed paths: ".$_POST['#file']);
83+
exit;
84+
}
6585
$section = $_POST['#section'] ?? false;
6686
$cleanup = isset($_POST['#cleanup']);
6787

0 commit comments

Comments
 (0)