From 80da8d1915fa93be1c5d4d9476ce3d66681a98a9 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Thu, 4 Jun 2026 01:44:32 +0200 Subject: [PATCH 1/7] Fix Webmin temp directory setup in Advanced Options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This fixes saving a custom Webmin temp directory from Webmin Configuration → Advanced Options. Previously, setting a path like /var/webmin/tmp failed if the directory did not already exist. Users had to create it manually, and it was easy to end up with a bad parent directory such as /var/webmin with 0700, which made the saved temp path unusable. This change makes Webmin handle the safe parts automatically: - Creates missing temp directories and parents as 0755 - Validates existing parent directories are traversable by group/other - Requires the final Webmin temp directory to be root-owned with mode 0755 - Allows shared temp dirs like /var/tmp when root-owned and 1777 - Shows a clear error when existing permissions must be fixed manually --- web-lib-funcs.pl | 16 +++-- webmin/change_advanced.cgi | 119 ++++++++++++++++++++++++++++++++++++- webmin/lang/en | 5 ++ 3 files changed, 131 insertions(+), 9 deletions(-) diff --git a/web-lib-funcs.pl b/web-lib-funcs.pl index ef25fa6de..57f1006b8 100755 --- a/web-lib-funcs.pl +++ b/web-lib-funcs.pl @@ -538,9 +538,12 @@ else { my $mkdirerr; while($tries++ < 10) { my @st = lstat($tmp_dir); - last if ($st[4] == $< && (-d _) && - ($st[2] & 0777) == 0755); if (@st) { + my $mode = $st[2] & 07777; + # Accept Webmin-private dirs and standard shared + last if ($st[4] == $< && (-d _) && + ((($mode & 0777) == 0755) || + $mode == 01777)); unlink($tmp_dir) || rmdir($tmp_dir) || system("/bin/rm -rf ". quotemeta($tmp_dir)); @@ -559,14 +562,15 @@ else { $tmp_dir.$mkdirerr); } } - # If running as root, check parent dir (usually /tmp) to make sure it's - # world-writable and owned by root + # If running as root, check parent dir (usually /tmp) to make sure it + # is searchable by group and others. my $tmp_parent = $tmp_dir; $tmp_parent =~ s/\/[^\/]+$//; if ($tmp_parent eq "/tmp") { my @st = stat($tmp_parent); - if (($st[2] & 0555) != 0555) { - &error("Base temp directory $tmp_parent is not world readable and listable"); + if (($st[2] & 0011) != 0011) { + &error("Base temp directory $tmp_parent must be ". + "group and other executable"); } } } diff --git a/webmin/change_advanced.cgi b/webmin/change_advanced.cgi index e9bf765b4..1c8cf8877 100755 --- a/webmin/change_advanced.cgi +++ b/webmin/change_advanced.cgi @@ -11,9 +11,10 @@ if ($in{'tempdir_def'}) { delete($gconfig{'tempdir'}); } else { - -d $in{'tempdir'} || &error($text{'advanced_etemp'}); &allowed_temp_dir($in{'tempdir'}) || &error(&text('advanced_etempallowed', $in{'tempdir'})); + $in{'tempdir'} = &setup_advanced_temp_dir( + $in{'tempdir'}, $text{'advanced_etemp'}); $gconfig{'tempdir'} = $in{'tempdir'}; } @@ -33,9 +34,10 @@ for($i=0; defined($tmod = $in{'tmod_'.$i}); $i++) { next if (!$tmod); $tdir = $in{'tdir_'.$i}; %minfo = &get_module_info($tmod); - -d $tdir || &error(&text('advanced_etdir', $minfo{'desc'})); &allowed_temp_dir($tdir) || - &error(&text('advanced_etempallowed', $in{'tempdir'})); + &error(&text('advanced_etempallowed', $tdir)); + $tdir = &setup_advanced_temp_dir( + $tdir, &text('advanced_etdir', $minfo{'desc'})); push(@tdirs, [ $tmod, $tdir ]); } &save_tempdirs(\%gconfig, \@tdirs); @@ -141,3 +143,114 @@ sub allowed_temp_dir my ($t) = @_; return $t eq "/" || $t =~ /^\/[^\/]+\/?$/ ? 0 : 1; } + +# Create a configured Webmin temp directory if needed, and validate existing +# path components without changing permissions on directories the user made. +sub setup_advanced_temp_dir +{ +my ($dir, $missing_error) = @_; +$dir =~ /\S/ || &error($missing_error); +$dir =~ s/\/+$// if ($dir ne "/"); +$dir =~ /\S/ || &error($missing_error); +if (&advanced_temp_dir_is_windows($dir)) { + if (!-d $dir) { + &make_dir($dir, &advanced_temp_dir_perms(), 1) || + &error(&text('advanced_etempmkdir', $dir, "$!")); + -d $dir || + &error(&text('advanced_etempmkdir', $dir, "$!")); + } + return $dir; + } +if ($dir =~ /^\//) { + my $sdir = &simplify_path($dir); + defined($sdir) || &error($missing_error); + $dir = $sdir; + } +&allowed_temp_dir($dir) || + &error(&text('advanced_etempallowed', $dir)); + +# Walk the path so existing parents are checked, while missing parents and +# the final temp directory are created with Webmin's expected mode. +my $path = $dir =~ /^\// ? "/" : ""; +foreach my $part (split(/\/+/, $dir)) { + next if ($part eq ""); + $path = $path eq "/" ? "/$part" : + $path eq "" ? $part : "$path/$part"; + my $final = $path eq $dir; + if (-e $path || -l $path) { + -d $path || + &error(&text('advanced_etempparent', $path)); + if ($final) { + &advanced_temp_dir_perms_ok($path) || + &error(&text('advanced_etempperms', $path, + &advanced_temp_dir_allowed_perms_text())); + } + else { + &advanced_temp_parent_dir_perms_ok($path) || + &error(&text('advanced_etempparentperms', + $path)); + } + next; + } + &make_dir($path, &advanced_temp_dir_perms()) || + &error(&text('advanced_etempmkdir', $path, "$!")); + &advanced_temp_dir_perms_ok($path) || + &error(&text('advanced_etempchmod', $path, + &advanced_temp_dir_perms_text(), "$!")); + } +&advanced_temp_dir_perms_ok($dir) || + &error(&text('advanced_etempperms', $dir, + &advanced_temp_dir_allowed_perms_text())); +return $dir; +} + +# Permissions used for newly created Webmin temp directories. +sub advanced_temp_dir_perms +{ +return 0755; +} + +# Octal text form used in error messages. +sub advanced_temp_dir_perms_text +{ +return sprintf("%04o", &advanced_temp_dir_perms()); +} + +# Existing final temp dirs may be Webmin-private, or standard shared temp dirs. +sub advanced_temp_dir_allowed_perms_text +{ +return "".&advanced_temp_dir_perms_text()." or 1777"; +} + +# Check the final configured temp directory. Webmin creates 0755 directories, +# but also permits standard shared temp directories like /var/tmp. +sub advanced_temp_dir_perms_ok +{ +my ($dir) = @_; +my @st = lstat($dir); +return 0 if (!@st || !-d _); +return 0 if ($st[4] != $<); +my $mode = $st[2] & 07777; +return 1 if (($mode & 0777) == &advanced_temp_dir_perms()); +return 1 if ($mode == 01777); +return 0; +} + +# Existing parents only need to be searchable by group and others. The final +# temp directory itself is checked more strictly above. +sub advanced_temp_parent_dir_perms_ok +{ +my ($dir) = @_; +my @st = stat($dir); +return 0 if (!@st || !-d _); +my $mode = $st[2] & 07777; +return 0 if (($mode & 0011) != 0011); +return 1; +} + +# Windows temp directories are only checked for existence and created if needed. +sub advanced_temp_dir_is_windows +{ +my ($dir) = @_; +return $gconfig{'os_type'} eq 'windows' || $dir =~ /^[a-z]:/i; +} diff --git a/webmin/lang/en b/webmin/lang/en index 9f29358d8..3ed7c3cda 100644 --- a/webmin/lang/en +++ b/webmin/lang/en @@ -942,6 +942,11 @@ advanced_err=Failed to save advanced options advanced_etemp=Missing or non-existant temporary files directory advanced_etdir=Missing or non-existant temporary files directory for $1 advanced_etempallowed=Temporary files directory $1 is a system directory +advanced_etempmkdir=Failed to create temporary files directory $1 : $2 +advanced_etempparent=Cannot create temporary files directory because $1 exists and is not a directory +advanced_etempparentperms=Parent directory $1 must be group and other executable. +advanced_etempchmod=Failed to set permissions on temporary files directory $1 to $2 : $3 +advanced_etempperms=Temporary files directory $1 already exists with incorrect ownership or permissions, and must be owned by root with mode $2 before saving. advanced_pass_desc=Make password available to Usermin programs? advanced_pass_help=Does not work when session authentication is enabled advanced_tempmods=Per-module temporary directories From e434d0b1389edc6a1e9ad507c6475ad073c03acc Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Fri, 5 Jun 2026 22:06:49 +0200 Subject: [PATCH 2/7] Fix to use a variable for default perms --- webmin/change_advanced.cgi | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/webmin/change_advanced.cgi b/webmin/change_advanced.cgi index 1c8cf8877..95ec8cbe2 100755 --- a/webmin/change_advanced.cgi +++ b/webmin/change_advanced.cgi @@ -6,6 +6,9 @@ require './webmin-lib.pl'; &error_setup($text{'advanced_err'}); &get_miniserv_config(\%miniserv); +# Permissions used for newly created Webmin temp directories. +my $advanced_temp_dir_perms = 0755; + # Save global temp dir setting if ($in{'tempdir_def'}) { delete($gconfig{'tempdir'}); @@ -154,7 +157,7 @@ $dir =~ s/\/+$// if ($dir ne "/"); $dir =~ /\S/ || &error($missing_error); if (&advanced_temp_dir_is_windows($dir)) { if (!-d $dir) { - &make_dir($dir, &advanced_temp_dir_perms(), 1) || + &make_dir($dir, $advanced_temp_dir_perms, 1) || &error(&text('advanced_etempmkdir', $dir, "$!")); -d $dir || &error(&text('advanced_etempmkdir', $dir, "$!")); @@ -192,7 +195,7 @@ foreach my $part (split(/\/+/, $dir)) { } next; } - &make_dir($path, &advanced_temp_dir_perms()) || + &make_dir($path, $advanced_temp_dir_perms) || &error(&text('advanced_etempmkdir', $path, "$!")); &advanced_temp_dir_perms_ok($path) || &error(&text('advanced_etempchmod', $path, @@ -204,16 +207,10 @@ foreach my $part (split(/\/+/, $dir)) { return $dir; } -# Permissions used for newly created Webmin temp directories. -sub advanced_temp_dir_perms -{ -return 0755; -} - # Octal text form used in error messages. sub advanced_temp_dir_perms_text { -return sprintf("%04o", &advanced_temp_dir_perms()); +return sprintf("%04o", $advanced_temp_dir_perms); } # Existing final temp dirs may be Webmin-private, or standard shared temp dirs. @@ -231,7 +228,7 @@ my @st = lstat($dir); return 0 if (!@st || !-d _); return 0 if ($st[4] != $<); my $mode = $st[2] & 07777; -return 1 if (($mode & 0777) == &advanced_temp_dir_perms()); +return 1 if (($mode & 0777) == $advanced_temp_dir_perms); return 1 if ($mode == 01777); return 0; } From 501bddabc85e341a8c8b31b9440588fa6043e72b Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Fri, 5 Jun 2026 22:13:48 +0200 Subject: [PATCH 3/7] Fix to make temp dir perms message translatable --- webmin/change_advanced.cgi | 26 ++++++++++---------------- webmin/lang/en | 2 +- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/webmin/change_advanced.cgi b/webmin/change_advanced.cgi index 95ec8cbe2..3cafd9e40 100755 --- a/webmin/change_advanced.cgi +++ b/webmin/change_advanced.cgi @@ -8,6 +8,10 @@ require './webmin-lib.pl'; # Permissions used for newly created Webmin temp directories. my $advanced_temp_dir_perms = 0755; +my $advanced_temp_dir_perms_text = sprintf("%04o", $advanced_temp_dir_perms); +my $advanced_temp_dir_shared_perms = 01777; +my $advanced_temp_dir_shared_perms_text = + sprintf("%04o", $advanced_temp_dir_shared_perms); # Save global temp dir setting if ($in{'tempdir_def'}) { @@ -186,7 +190,8 @@ foreach my $part (split(/\/+/, $dir)) { if ($final) { &advanced_temp_dir_perms_ok($path) || &error(&text('advanced_etempperms', $path, - &advanced_temp_dir_allowed_perms_text())); + $advanced_temp_dir_perms_text, + $advanced_temp_dir_shared_perms_text)); } else { &advanced_temp_parent_dir_perms_ok($path) || @@ -199,26 +204,15 @@ foreach my $part (split(/\/+/, $dir)) { &error(&text('advanced_etempmkdir', $path, "$!")); &advanced_temp_dir_perms_ok($path) || &error(&text('advanced_etempchmod', $path, - &advanced_temp_dir_perms_text(), "$!")); + $advanced_temp_dir_perms_text, "$!")); } &advanced_temp_dir_perms_ok($dir) || &error(&text('advanced_etempperms', $dir, - &advanced_temp_dir_allowed_perms_text())); + $advanced_temp_dir_perms_text, + $advanced_temp_dir_shared_perms_text)); return $dir; } -# Octal text form used in error messages. -sub advanced_temp_dir_perms_text -{ -return sprintf("%04o", $advanced_temp_dir_perms); -} - -# Existing final temp dirs may be Webmin-private, or standard shared temp dirs. -sub advanced_temp_dir_allowed_perms_text -{ -return "".&advanced_temp_dir_perms_text()." or 1777"; -} - # Check the final configured temp directory. Webmin creates 0755 directories, # but also permits standard shared temp directories like /var/tmp. sub advanced_temp_dir_perms_ok @@ -229,7 +223,7 @@ return 0 if (!@st || !-d _); return 0 if ($st[4] != $<); my $mode = $st[2] & 07777; return 1 if (($mode & 0777) == $advanced_temp_dir_perms); -return 1 if ($mode == 01777); +return 1 if ($mode == $advanced_temp_dir_shared_perms); return 0; } diff --git a/webmin/lang/en b/webmin/lang/en index 3ed7c3cda..cd63a7036 100644 --- a/webmin/lang/en +++ b/webmin/lang/en @@ -946,7 +946,7 @@ advanced_etempmkdir=Failed to create temporary files directory $1 : $2 advanced_etempparent=Cannot create temporary files directory because $1 exists and is not a directory advanced_etempparentperms=Parent directory $1 must be group and other executable. advanced_etempchmod=Failed to set permissions on temporary files directory $1 to $2 : $3 -advanced_etempperms=Temporary files directory $1 already exists with incorrect ownership or permissions, and must be owned by root with mode $2 before saving. +advanced_etempperms=Temporary files directory $1 already exists with incorrect ownership or permissions, and must be owned by root with mode $2 or $3 before saving. advanced_pass_desc=Make password available to Usermin programs? advanced_pass_help=Does not work when session authentication is enabled advanced_tempmods=Per-module temporary directories From 9577737aebc78da16c32426d0d048579a8950640 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Sat, 6 Jun 2026 22:40:52 +0200 Subject: [PATCH 4/7] Fix to harden Webmin temp directory validation https://github.com/webmin/webmin/pull/2749#discussion_r3368028469 --- web-lib-funcs.pl | 3 ++- webmin/change_advanced.cgi | 31 ++++++++++++++----------------- webmin/lang/en | 2 +- 3 files changed, 17 insertions(+), 19 deletions(-) diff --git a/web-lib-funcs.pl b/web-lib-funcs.pl index 57f1006b8..e4231d22f 100755 --- a/web-lib-funcs.pl +++ b/web-lib-funcs.pl @@ -533,7 +533,8 @@ if ($gconfig{'os_type'} eq 'windows' || $tmp_dir =~ /^[a-z]:/i) { } else { # On Unix systems, need to make sure temp dir is valid - if ($tmp_dir ne "/tmp") { + if ($tmp_dir ne "/dev/shm" && $tmp_dir ne "/tmp" && + $tmp_dir ne "/var/tmp" && $tmp_dir ne "/usr/tmp") { my $tries = 0; my $mkdirerr; while($tries++ < 10) { diff --git a/webmin/change_advanced.cgi b/webmin/change_advanced.cgi index 3cafd9e40..ab3297529 100755 --- a/webmin/change_advanced.cgi +++ b/webmin/change_advanced.cgi @@ -9,9 +9,8 @@ require './webmin-lib.pl'; # Permissions used for newly created Webmin temp directories. my $advanced_temp_dir_perms = 0755; my $advanced_temp_dir_perms_text = sprintf("%04o", $advanced_temp_dir_perms); -my $advanced_temp_dir_shared_perms = 01777; -my $advanced_temp_dir_shared_perms_text = - sprintf("%04o", $advanced_temp_dir_shared_perms); +my %advanced_system_temp_dirs = map { $_ => 1 } + ( "/dev/shm", "/tmp", "/var/tmp", "/usr/tmp" ); # Save global temp dir setting if ($in{'tempdir_def'}) { @@ -148,7 +147,10 @@ else { sub allowed_temp_dir { my ($t) = @_; -return $t eq "/" || $t =~ /^\/[^\/]+\/?$/ ? 0 : 1; +my $dir = $t; +$dir =~ s/\/+$// if ($dir ne "/"); +return $dir eq "/" || $dir =~ /^\/[^\/]+$/ || + $advanced_system_temp_dirs{$dir} ? 0 : 1; } # Create a configured Webmin temp directory if needed, and validate existing @@ -184,14 +186,13 @@ foreach my $part (split(/\/+/, $dir)) { $path = $path eq "/" ? "/$part" : $path eq "" ? $part : "$path/$part"; my $final = $path eq $dir; - if (-e $path || -l $path) { - -d $path || - &error(&text('advanced_etempparent', $path)); + my @st = lstat($path); + if (@st) { + -d _ || &error(&text('advanced_etempparent', $path)); if ($final) { &advanced_temp_dir_perms_ok($path) || &error(&text('advanced_etempperms', $path, - $advanced_temp_dir_perms_text, - $advanced_temp_dir_shared_perms_text)); + $advanced_temp_dir_perms_text)); } else { &advanced_temp_parent_dir_perms_ok($path) || @@ -208,13 +209,11 @@ foreach my $part (split(/\/+/, $dir)) { } &advanced_temp_dir_perms_ok($dir) || &error(&text('advanced_etempperms', $dir, - $advanced_temp_dir_perms_text, - $advanced_temp_dir_shared_perms_text)); + $advanced_temp_dir_perms_text)); return $dir; } -# Check the final configured temp directory. Webmin creates 0755 directories, -# but also permits standard shared temp directories like /var/tmp. +# Check the final configured temp directory. It must be Webmin-private. sub advanced_temp_dir_perms_ok { my ($dir) = @_; @@ -222,9 +221,7 @@ my @st = lstat($dir); return 0 if (!@st || !-d _); return 0 if ($st[4] != $<); my $mode = $st[2] & 07777; -return 1 if (($mode & 0777) == $advanced_temp_dir_perms); -return 1 if ($mode == $advanced_temp_dir_shared_perms); -return 0; +return $mode == $advanced_temp_dir_perms; } # Existing parents only need to be searchable by group and others. The final @@ -232,7 +229,7 @@ return 0; sub advanced_temp_parent_dir_perms_ok { my ($dir) = @_; -my @st = stat($dir); +my @st = lstat($dir); return 0 if (!@st || !-d _); my $mode = $st[2] & 07777; return 0 if (($mode & 0011) != 0011); diff --git a/webmin/lang/en b/webmin/lang/en index cd63a7036..bec303568 100644 --- a/webmin/lang/en +++ b/webmin/lang/en @@ -946,7 +946,7 @@ advanced_etempmkdir=Failed to create temporary files directory $1 : $2 advanced_etempparent=Cannot create temporary files directory because $1 exists and is not a directory advanced_etempparentperms=Parent directory $1 must be group and other executable. advanced_etempchmod=Failed to set permissions on temporary files directory $1 to $2 : $3 -advanced_etempperms=Temporary files directory $1 already exists with incorrect ownership or permissions, and must be owned by root with mode $2 or $3 before saving. +advanced_etempperms=Temporary files directory $1 already exists with incorrect ownership or permissions, and must be owned by root with mode $2 before saving. advanced_pass_desc=Make password available to Usermin programs? advanced_pass_help=Does not work when session authentication is enabled advanced_tempmods=Per-module temporary directories From d788bbe9c27ad8e1e9726e79cffd4256070169b6 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Sat, 6 Jun 2026 22:49:37 +0200 Subject: [PATCH 5/7] Fix to add a placeholder to tempdir field --- webmin/edit_advanced.cgi | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/webmin/edit_advanced.cgi b/webmin/edit_advanced.cgi index 0037976df..c35ba2553 100755 --- a/webmin/edit_advanced.cgi +++ b/webmin/edit_advanced.cgi @@ -11,7 +11,9 @@ print &ui_table_start($text{'advanced_header'}, undef, 2); # Global temp directory print &ui_table_row($text{'advanced_temp'}, &ui_opt_textbox("tempdir", $gconfig{'tempdir'}, 30, - &text('advanced_tempdef', &default_webmin_temp_dir())). + &text('advanced_tempdef', &default_webmin_temp_dir()), + undef, undef, undef, undef, + "placeholder='/var/tmp/.webmin'"). "
". &ui_checkbox("tempdirdelete", 1, $text{'advanced_tdd'}, $gconfig{'tempdirdelete'})); From 0d4c65ec04a33cee601f6fffa1e79192f3c16cd0 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Mon, 8 Jun 2026 18:53:57 +0200 Subject: [PATCH 6/7] Fix to create custom temp dirs after validation --- web-lib-funcs.pl | 6 +- webmin/change_advanced.cgi | 145 +++++++++++++++++++++++++------------ 2 files changed, 100 insertions(+), 51 deletions(-) diff --git a/web-lib-funcs.pl b/web-lib-funcs.pl index e4231d22f..a6844ec5e 100755 --- a/web-lib-funcs.pl +++ b/web-lib-funcs.pl @@ -541,10 +541,10 @@ else { my @st = lstat($tmp_dir); if (@st) { my $mode = $st[2] & 07777; - # Accept Webmin-private dirs and standard shared + # Accept only Webmin-private dirs here. Shared + # system temp roots are skipped above. last if ($st[4] == $< && (-d _) && - ((($mode & 0777) == 0755) || - $mode == 01777)); + $mode == 0755); unlink($tmp_dir) || rmdir($tmp_dir) || system("/bin/rm -rf ". quotemeta($tmp_dir)); diff --git a/webmin/change_advanced.cgi b/webmin/change_advanced.cgi index ab3297529..e10e2b0fa 100755 --- a/webmin/change_advanced.cgi +++ b/webmin/change_advanced.cgi @@ -11,16 +11,16 @@ my $advanced_temp_dir_perms = 0755; my $advanced_temp_dir_perms_text = sprintf("%04o", $advanced_temp_dir_perms); my %advanced_system_temp_dirs = map { $_ => 1 } ( "/dev/shm", "/tmp", "/var/tmp", "/usr/tmp" ); +my @advanced_temp_dirs_to_create; # Save global temp dir setting if ($in{'tempdir_def'}) { delete($gconfig{'tempdir'}); } else { - &allowed_temp_dir($in{'tempdir'}) || - &error(&text('advanced_etempallowed', $in{'tempdir'})); - $in{'tempdir'} = &setup_advanced_temp_dir( - $in{'tempdir'}, $text{'advanced_etemp'}); + $in{'tempdir'} = &validate_advanced_temp_dir( + $in{'tempdir'}, $text{'advanced_etemp'}, + \@advanced_temp_dirs_to_create); $gconfig{'tempdir'} = $in{'tempdir'}; } @@ -40,10 +40,9 @@ for($i=0; defined($tmod = $in{'tmod_'.$i}); $i++) { next if (!$tmod); $tdir = $in{'tdir_'.$i}; %minfo = &get_module_info($tmod); - &allowed_temp_dir($tdir) || - &error(&text('advanced_etempallowed', $tdir)); - $tdir = &setup_advanced_temp_dir( - $tdir, &text('advanced_etdir', $minfo{'desc'})); + $tdir = &validate_advanced_temp_dir( + $tdir, &text('advanced_etdir', $minfo{'desc'}), + \@advanced_temp_dirs_to_create); push(@tdirs, [ $tmod, $tdir ]); } &save_tempdirs(\%gconfig, \@tdirs); @@ -84,15 +83,6 @@ foreach my $l (split(/\r?\n/, $in{'headers'})) { } $gconfig{'extra_headers'} = join("\t", @hl); -# Sort config file's keys alphabetically -if (defined($in{'sortconfigs'})) { - $gconfig{'sortconfigs'} = $in{'sortconfigs'}; - } - -&lock_file("$config_directory/config"); -&write_file("$config_directory/config", \%gconfig); -&unlock_file("$config_directory/config"); - if (defined($in{'preload'})) { # Save preload option, forcing new mode if ($in{'preload'}) { @@ -136,6 +126,17 @@ else { $miniserv{'bufsize_binary'} = $in{'bufsize_binary'}; } +# Sort config file's keys alphabetically +if (defined($in{'sortconfigs'})) { + $gconfig{'sortconfigs'} = $in{'sortconfigs'}; + } + +&setup_advanced_temp_dirs(\@advanced_temp_dirs_to_create); + +&lock_file("$config_directory/config"); +&write_file("$config_directory/config", \%gconfig); +&unlock_file("$config_directory/config"); + &lock_file($ENV{'MINISERV_CONFIG'}); &put_miniserv_config(\%miniserv); &unlock_file($ENV{'MINISERV_CONFIG'}); @@ -153,20 +154,21 @@ return $dir eq "/" || $dir =~ /^\/[^\/]+$/ || $advanced_system_temp_dirs{$dir} ? 0 : 1; } -# Create a configured Webmin temp directory if needed, and validate existing -# path components without changing permissions on directories the user made. -sub setup_advanced_temp_dir +# Validate a configured Webmin temp directory without creating or changing it. +# Missing components are queued and created after all form validation passes. +sub validate_advanced_temp_dir { -my ($dir, $missing_error) = @_; +my ($dir, $missing_error, $create_dirs) = @_; $dir =~ /\S/ || &error($missing_error); $dir =~ s/\/+$// if ($dir ne "/"); $dir =~ /\S/ || &error($missing_error); if (&advanced_temp_dir_is_windows($dir)) { - if (!-d $dir) { - &make_dir($dir, $advanced_temp_dir_perms, 1) || - &error(&text('advanced_etempmkdir', $dir, "$!")); + if (-e $dir || -l $dir) { -d $dir || - &error(&text('advanced_etempmkdir', $dir, "$!")); + &error(&text('advanced_etempparent', $dir)); + } + else { + push(@$create_dirs, $dir); } return $dir; } @@ -178,8 +180,8 @@ if ($dir =~ /^\//) { &allowed_temp_dir($dir) || &error(&text('advanced_etempallowed', $dir)); -# Walk the path so existing parents are checked, while missing parents and -# the final temp directory are created with Webmin's expected mode. +# Walk the path so existing components are checked, while missing components +# can be created after all form validation has passed. my $path = $dir =~ /^\// ? "/" : ""; foreach my $part (split(/\/+/, $dir)) { next if ($part eq ""); @@ -187,32 +189,79 @@ foreach my $part (split(/\/+/, $dir)) { $path eq "" ? $part : "$path/$part"; my $final = $path eq $dir; my @st = lstat($path); - if (@st) { - -d _ || &error(&text('advanced_etempparent', $path)); - if ($final) { - &advanced_temp_dir_perms_ok($path) || - &error(&text('advanced_etempperms', $path, - $advanced_temp_dir_perms_text)); - } - else { - &advanced_temp_parent_dir_perms_ok($path) || - &error(&text('advanced_etempparentperms', - $path)); - } + if (!@st) { + push(@$create_dirs, $path); next; } - &make_dir($path, $advanced_temp_dir_perms) || - &error(&text('advanced_etempmkdir', $path, "$!")); - &advanced_temp_dir_perms_ok($path) || - &error(&text('advanced_etempchmod', $path, - $advanced_temp_dir_perms_text, "$!")); + -d _ || &error(&text('advanced_etempparent', $path)); + if ($final) { + &advanced_temp_dir_perms_ok($path) || + &error(&text('advanced_etempperms', $path, + $advanced_temp_dir_perms_text)); + } + else { + &advanced_temp_parent_dir_perms_ok($path) || + &error(&text('advanced_etempparentperms', + $path)); + } } -&advanced_temp_dir_perms_ok($dir) || - &error(&text('advanced_etempperms', $dir, - $advanced_temp_dir_perms_text)); return $dir; } +# Create missing temp directory components after all form validation passes. +sub setup_advanced_temp_dirs +{ +my ($dirs) = @_; +my %done; +my @created; +foreach my $dir (@$dirs) { + next if ($done{$dir}++); + if (&advanced_temp_dir_is_windows($dir)) { + if (!-d $dir) { + &make_dir($dir, $advanced_temp_dir_perms, 1) || + &advanced_temp_dirs_error( + \@created, + &text('advanced_etempmkdir', + $dir, "$!")); + push(@created, $dir); + } + -d $dir || + &advanced_temp_dirs_error( + \@created, + &text('advanced_etempmkdir', $dir, "$!")); + next; + } + if (-e $dir || -l $dir) { + &advanced_temp_dir_perms_ok($dir) || + &advanced_temp_dirs_error( + \@created, + &text('advanced_etempperms', $dir, + $advanced_temp_dir_perms_text)); + next; + } + &make_dir($dir, $advanced_temp_dir_perms) || + &advanced_temp_dirs_error( + \@created, + &text('advanced_etempmkdir', $dir, "$!")); + push(@created, $dir); + &advanced_temp_dir_perms_ok($dir) || + &advanced_temp_dirs_error( + \@created, + &text('advanced_etempchmod', $dir, + $advanced_temp_dir_perms_text, "$!")); + } +} + +# Roll back only directories created by this save attempt, and only if empty. +sub advanced_temp_dirs_error +{ +my ($created, $msg) = @_; +foreach my $dir (reverse(@$created)) { + rmdir($dir); + } +&error($msg); +} + # Check the final configured temp directory. It must be Webmin-private. sub advanced_temp_dir_perms_ok { @@ -236,7 +285,7 @@ return 0 if (($mode & 0011) != 0011); return 1; } -# Windows temp directories are only checked for existence and created if needed. +# Windows temp directories are only checked when they already exist. sub advanced_temp_dir_is_windows { my ($dir) = @_; From ccd2b1394271b3e4a6da458f6d8b8947ce44ae20 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Thu, 18 Jun 2026 20:48:47 +0200 Subject: [PATCH 7/7] Fix to enforce private basename for Webmin temp dirs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⓘ Adds hidden `tempdirname` support and normalizes custom temp paths so Webmin always uses a private final directory like `.webmin`, while keeping the existing permission checks. --- WebminCore.pm | 2 +- t/web-lib-funcs-paths.t | 30 ++++++++++++++++++++++++++++ web-lib-funcs.pl | 41 +++++++++++++++++++++++++++++++++++++- webmin/change_advanced.cgi | 17 ++++------------ webmin/edit_advanced.cgi | 3 ++- webmin/lang/en | 1 - 6 files changed, 77 insertions(+), 17 deletions(-) diff --git a/WebminCore.pm b/WebminCore.pm index 9968848c9..15dbda271 100644 --- a/WebminCore.pm +++ b/WebminCore.pm @@ -23,7 +23,7 @@ $main::export_to_caller = 1; # Add functions in web-lib-funcs.pl # Generated with : # grep -h "^sub " web-lib-funcs.pl ui-lib.pl | sed -e 's/sub //' | xargs echo -@EXPORT = qw(read_file read_file_cached read_file_cached_with_stat write_file html_escape html_unescape html_strip quote_escape quote_literal_escape quote_javascript default_webmin_temp_dir tempname_dir tempname_dir_sys tempname transname transname_timestamped trunc indexof indexoflc sysprint check_ipaddress check_ip6address is_non_public_ipaddress generate_icon urlize un_urlize include copydata ReadParseMime ReadParse read_fully read_parse_mime_callback read_parse_mime_javascript PrintHeader header get_html_title get_html_framed_title get_html_status_line popup_header footer popup_footer load_module_preferences load_theme_library redirect kill_byname kill_byname_logged find_byname error error_stderr popup_error register_error_handler call_error_handlers error_setup wait_for fast_wait_for has_command make_date make_date_relative file_chooser_button popup_window_button popup_window_link read_acl acl_filename acl_check get_miniserv_config_file get_miniserv_config put_miniserv_config restart_miniserv reload_miniserv check_os_support http_download complete_http_download http_post ftp_download ftp_upload no_proxy open_socket download_timeout ftp_command to_ipaddress to_ip6address to_hostname icons_table replace_meta replace_file_line read_file_lines flush_file_lines unflush_file_lines unix_user_input unix_group_input hlink user_chooser_button group_chooser_button foreign_check foreign_exists foreign_available foreign_require foreign_call foreign_config foreign_installed foreign_defined get_system_hostname get_webmin_version get_webmin_version_release get_webmin_full_version get_module_acl get_group_module_acl save_module_acl save_group_module_acl init_config load_language_auto load_language text_subs text encode_base64 decode_base64 encode_base32 decode_base32 get_module_info get_all_module_infos list_themes get_theme_info list_locales list_languages safe_language read_env_file write_env_file lock_file unlock_file test_lock unlock_all_files can_lock_file webmin_log additional_log var_dump webmin_debug_log system_logged backquote_logged backquote_with_timeout backquote_command kill_logged rename_logged rename_file symlink_logged symlink_file link_file make_dir make_dir_recursive set_ownership_permissions unlink_logged unlink_file copy_permissions_source_dest copy_source_dest move_source_dest remote_session_name verify_session_id remote_foreign_require remote_foreign_call remote_foreign_check remote_foreign_config remote_eval remote_write remote_read remote_finished remote_error_setup remote_rpc_call remote_multi_callback remote_multi_callback_error serialise_variable unserialise_variable other_groups date_chooser_button help_file read_help_file seed_random disk_usage_kb recursive_disk_usage help_search_link can_use_http_ssl make_http_connection validate_ssl_connection read_http_connection write_http_connection close_http_connection read_http_headers get_http_auth_reason clean_environment reset_environment clean_language progress_callback switch_to_remote_user switch_to_unix_user eval_as_unix_user create_user_config_dirs create_missing_homedir filter_javascript resolve_links simplify_path same_file flush_webmin_caches list_usermods available_usermods get_available_module_infos get_visible_module_infos get_visible_modules_categories is_under_directory parse_http_url check_clicks_function load_entities_map entities_to_ascii get_product_name get_charset get_display_hostname save_module_config save_user_module_config nice_size get_perl_path get_goto_module select_all_link select_invert_link select_rows_link check_pid_file get_mod_lib module_root_directory list_mime_types guess_mime_type open_tempfile close_tempfile print_tempfile is_selinux_enabled get_clear_file_attributes reset_file_attributes cleanup_tempnames open_lock_tempfile END month_to_number number_to_month supports_ipv6 execute_command execute_command_logged open_readfile open_execute_command translate_filename translate_command register_filename_callback register_command_callback capture_function_output capture_function_output_tempfile modules_chooser_button substitute_template substitute_pattern running_in_zone running_in_vserver running_in_xen running_in_openvz list_categories is_readonly_mode command_as_user list_osdn_mirrors convert_osdn_url get_current_dir supports_users supports_symlinks quote_path get_windows_root read_file_contents write_file_contents read_file_contents_limit unix_crypt split_quoted_string write_to_http_cache check_in_http_cache clear_http_cache supports_javascript get_module_name get_module_variable clear_time_locale reset_time_locale callers_package web_libs_package get_userdb_string connect_userdb disconnect_userdb split_userdb_string uniquelc list_combined_webmin_menu list_modules_webmin_menu module_to_menu_item list_combined_system_info shell_is_bash compare_version_numbers convert_to_json convert_from_json print_json get_referer_relative get_webmin_email_url get_webmin_browser_url trim ui_link ui_help ui_img ui_link_button ui_table_start ui_table_end ui_table_row ui_table_hr ui_table_span ui_columns_start ui_columns_row ui_columns_header ui_checked_columns_row ui_radio_columns_row ui_columns_end ui_columns_table ui_form_columns_table ui_form_elements_wrapper ui_form_start ui_form_end ui_form_end_side_by_side ui_form_grouped_buttons ui_textbox ui_filebox ui_bytesbox ui_upload ui_password ui_hidden ui_select ui_multi_select ui_multi_select_javascript ui_radio ui_yesno_radio ui_radio_row ui_checkbox ui_oneradio ui_textarea ui_user_textbox ui_users_textbox ui_group_textbox ui_groups_textbox ui_opt_textbox ui_submit ui_reset ui_button ui_date_input ui_buttons_start ui_buttons_end ui_buttons_row ui_buttons_hr ui_post_header ui_pre_footer ui_print_header ui_print_unbuffered_header ui_print_footer ui_config_link ui_print_endpage ui_subheading ui_links_row ui_hidden_javascript ui_hidden_start ui_hidden_end ui_hidden_table_row_start ui_hidden_table_row_end ui_hidden_table_start ui_hidden_table_end ui_tabs_start ui_tabs_end ui_tabs_start_tab ui_tabs_start_tabletab ui_tabs_end_tab ui_tabs_end_tabletab ui_max_text_width ui_radio_selector ui_radio_selector_javascript ui_switch_theme_javascript ui_grid_table ui_radio_table ui_up_down_arrows ui_hr ui_nav_link ui_confirmation_form ui_text_color ui_alert_box js_disable_inputs ui_page_flipper js_checkbox_disable js_redirect ui_webmin_link ui_line_break_double ui_page_refresh ui_details ui_div_row ui_space ui_newline ui_text_wrap ui_element_inline ui_paginations ui_hide_outside_of_viewport ui_read_file_contents_limit ui_note ui_brh ui_tag_start ui_tag_content ui_tag_end ui_tag ui_alert ui_button_icon ui_link_icon ui_icon ui_br ui_p ui_div ui_text_mask get_python_cmd get_buffer_size get_buffer_size_binary get_webprefix get_sub_ref_name setvar getvar delvar print_call_stack webmin_user_can_rpc webmin_user_login_mode webmin_user_is_admin webmin_user_is get_current_theme_info_cached miniserv_using_default_cert is_int float is_float parse_accepted_language get_default_system_locale get_http_redirect get_http_cookie create_wrapper get_lock_links_dir allocate_miniserv_websocket get_miniserv_websocket_url remove_miniserv_websocket cleanup_miniserv_websockets get_miniserv_websockets_modules get_webmin_base_url encrypt_phrase decrypt_phrase is_encrypt_phrase); +@EXPORT = qw(read_file read_file_cached read_file_cached_with_stat write_file html_escape html_unescape html_strip quote_escape quote_literal_escape quote_javascript default_webmin_temp_dir tempname_dir tempname_dir_sys webmin_temp_dir_name webmin_temp_dir_path tempname transname transname_timestamped trunc indexof indexoflc sysprint check_ipaddress check_ip6address is_non_public_ipaddress generate_icon urlize un_urlize include copydata ReadParseMime ReadParse read_fully read_parse_mime_callback read_parse_mime_javascript PrintHeader header get_html_title get_html_framed_title get_html_status_line popup_header footer popup_footer load_module_preferences load_theme_library redirect kill_byname kill_byname_logged find_byname error error_stderr popup_error register_error_handler call_error_handlers error_setup wait_for fast_wait_for has_command make_date make_date_relative file_chooser_button popup_window_button popup_window_link read_acl acl_filename acl_check get_miniserv_config_file get_miniserv_config put_miniserv_config restart_miniserv reload_miniserv check_os_support http_download complete_http_download http_post ftp_download ftp_upload no_proxy open_socket download_timeout ftp_command to_ipaddress to_ip6address to_hostname icons_table replace_meta replace_file_line read_file_lines flush_file_lines unflush_file_lines unix_user_input unix_group_input hlink user_chooser_button group_chooser_button foreign_check foreign_exists foreign_available foreign_require foreign_call foreign_config foreign_installed foreign_defined get_system_hostname get_webmin_version get_webmin_version_release get_webmin_full_version get_module_acl get_group_module_acl save_module_acl save_group_module_acl init_config load_language_auto load_language text_subs text encode_base64 decode_base64 encode_base32 decode_base32 get_module_info get_all_module_infos list_themes get_theme_info list_locales list_languages safe_language read_env_file write_env_file lock_file unlock_file test_lock unlock_all_files can_lock_file webmin_log additional_log var_dump webmin_debug_log system_logged backquote_logged backquote_with_timeout backquote_command kill_logged rename_logged rename_file symlink_logged symlink_file link_file make_dir make_dir_recursive set_ownership_permissions unlink_logged unlink_file copy_permissions_source_dest copy_source_dest move_source_dest remote_session_name verify_session_id remote_foreign_require remote_foreign_call remote_foreign_check remote_foreign_config remote_eval remote_write remote_read remote_finished remote_error_setup remote_rpc_call remote_multi_callback remote_multi_callback_error serialise_variable unserialise_variable other_groups date_chooser_button help_file read_help_file seed_random disk_usage_kb recursive_disk_usage help_search_link can_use_http_ssl make_http_connection validate_ssl_connection read_http_connection write_http_connection close_http_connection read_http_headers get_http_auth_reason clean_environment reset_environment clean_language progress_callback switch_to_remote_user switch_to_unix_user eval_as_unix_user create_user_config_dirs create_missing_homedir filter_javascript resolve_links simplify_path same_file flush_webmin_caches list_usermods available_usermods get_available_module_infos get_visible_module_infos get_visible_modules_categories is_under_directory parse_http_url check_clicks_function load_entities_map entities_to_ascii get_product_name get_charset get_display_hostname save_module_config save_user_module_config nice_size get_perl_path get_goto_module select_all_link select_invert_link select_rows_link check_pid_file get_mod_lib module_root_directory list_mime_types guess_mime_type open_tempfile close_tempfile print_tempfile is_selinux_enabled get_clear_file_attributes reset_file_attributes cleanup_tempnames open_lock_tempfile END month_to_number number_to_month supports_ipv6 execute_command execute_command_logged open_readfile open_execute_command translate_filename translate_command register_filename_callback register_command_callback capture_function_output capture_function_output_tempfile modules_chooser_button substitute_template substitute_pattern running_in_zone running_in_vserver running_in_xen running_in_openvz list_categories is_readonly_mode command_as_user list_osdn_mirrors convert_osdn_url get_current_dir supports_users supports_symlinks quote_path get_windows_root read_file_contents write_file_contents read_file_contents_limit unix_crypt split_quoted_string write_to_http_cache check_in_http_cache clear_http_cache supports_javascript get_module_name get_module_variable clear_time_locale reset_time_locale callers_package web_libs_package get_userdb_string connect_userdb disconnect_userdb split_userdb_string uniquelc list_combined_webmin_menu list_modules_webmin_menu module_to_menu_item list_combined_system_info shell_is_bash compare_version_numbers convert_to_json convert_from_json print_json get_referer_relative get_webmin_email_url get_webmin_browser_url trim ui_link ui_help ui_img ui_link_button ui_table_start ui_table_end ui_table_row ui_table_hr ui_table_span ui_columns_start ui_columns_row ui_columns_header ui_checked_columns_row ui_radio_columns_row ui_columns_end ui_columns_table ui_form_columns_table ui_form_elements_wrapper ui_form_start ui_form_end ui_form_end_side_by_side ui_form_grouped_buttons ui_textbox ui_filebox ui_bytesbox ui_upload ui_password ui_hidden ui_select ui_multi_select ui_multi_select_javascript ui_radio ui_yesno_radio ui_radio_row ui_checkbox ui_oneradio ui_textarea ui_user_textbox ui_users_textbox ui_group_textbox ui_groups_textbox ui_opt_textbox ui_submit ui_reset ui_button ui_date_input ui_buttons_start ui_buttons_end ui_buttons_row ui_buttons_hr ui_post_header ui_pre_footer ui_print_header ui_print_unbuffered_header ui_print_footer ui_config_link ui_print_endpage ui_subheading ui_links_row ui_hidden_javascript ui_hidden_start ui_hidden_end ui_hidden_table_row_start ui_hidden_table_row_end ui_hidden_table_start ui_hidden_table_end ui_tabs_start ui_tabs_end ui_tabs_start_tab ui_tabs_start_tabletab ui_tabs_end_tab ui_tabs_end_tabletab ui_max_text_width ui_radio_selector ui_radio_selector_javascript ui_switch_theme_javascript ui_grid_table ui_radio_table ui_up_down_arrows ui_hr ui_nav_link ui_confirmation_form ui_text_color ui_alert_box js_disable_inputs ui_page_flipper js_checkbox_disable js_redirect ui_webmin_link ui_line_break_double ui_page_refresh ui_details ui_div_row ui_space ui_newline ui_text_wrap ui_element_inline ui_paginations ui_hide_outside_of_viewport ui_read_file_contents_limit ui_note ui_brh ui_tag_start ui_tag_content ui_tag_end ui_tag ui_alert ui_button_icon ui_link_icon ui_icon ui_br ui_p ui_div ui_text_mask get_python_cmd get_buffer_size get_buffer_size_binary get_webprefix get_sub_ref_name setvar getvar delvar print_call_stack webmin_user_can_rpc webmin_user_login_mode webmin_user_is_admin webmin_user_is get_current_theme_info_cached miniserv_using_default_cert is_int float is_float parse_accepted_language get_default_system_locale get_http_redirect get_http_cookie create_wrapper get_lock_links_dir allocate_miniserv_websocket get_miniserv_websocket_url remove_miniserv_websocket cleanup_miniserv_websockets get_miniserv_websockets_modules get_webmin_base_url encrypt_phrase decrypt_phrase is_encrypt_phrase); # Add global variables in web-lib.pl push(@EXPORT, qw(&unique)); diff --git a/t/web-lib-funcs-paths.t b/t/web-lib-funcs-paths.t index a2d9b6837..18e1b5fdc 100644 --- a/t/web-lib-funcs-paths.t +++ b/t/web-lib-funcs-paths.t @@ -47,6 +47,36 @@ subtest 'simplify_path' => sub { is(main::simplify_path('foo'), '/foo', 'relative input is promoted to absolute'); }; +# webmin_temp_dir_name / webmin_temp_dir_path — hidden final temp dir name. +subtest 'webmin temp dir path' => sub { + no warnings 'once'; + local %main::gconfig = ('os_type' => 'linux'); + + is(main::webmin_temp_dir_name(), '.webmin', + 'default final temp dir name'); + is(main::webmin_temp_dir_path('/var/tmp'), '/var/tmp/.webmin', + 'default name appended to base path'); + is(main::webmin_temp_dir_path('/var/tmp/.webmin'), '/var/tmp/.webmin', + 'default name is not appended twice'); + + local %main::gconfig = ( + 'os_type' => 'linux', + 'tempdirname' => 'webmin-private', + ); + is(main::webmin_temp_dir_name(), 'webmin-private', + 'hidden tempdirname config overrides default name'); + is(main::webmin_temp_dir_path('/tmp/path1/path2/path3'), + '/tmp/path1/path2/path3/webmin-private', + 'custom name becomes the final path component'); + + local %main::gconfig = ( + 'os_type' => 'linux', + 'tempdirname' => '../bad', + ); + is(main::webmin_temp_dir_name(), '.webmin', + 'invalid hidden name falls back to default'); +}; + # parse_http_url — absolute and base-relative URL parsing. # # Contract on success: returns (host, port, page, ssl, [user], [pass]). diff --git a/web-lib-funcs.pl b/web-lib-funcs.pl index a6844ec5e..81c563752 100755 --- a/web-lib-funcs.pl +++ b/web-lib-funcs.pl @@ -466,6 +466,45 @@ my $keys = ($modk && $gconfig{$modk}) ? "$modk or tempdir_sys" : "tempdir_sys"; "directory in $config_directory/config and try again."); } +=head2 webmin_temp_dir_name() + +Returns the final directory name used for Webmin-private temp directories. +This defaults to .webmin, and can be changed with the hidden tempdirname +configuration option. + +=cut +sub webmin_temp_dir_name +{ +my $name = $gconfig{'tempdirname'} || ".webmin"; +$name =~ s/^\s+//; +$name =~ s/\s+$//; +return $name =~ /^[^\/\\]+$/ && $name ne "." && $name ne ".." ? + $name : ".webmin"; +} + +=head2 webmin_temp_dir_path(path) + +Returns a temporary directory path ending in the configured Webmin-private +directory name. + +=cut +sub webmin_temp_dir_path +{ +my ($dir) = @_; +my $name = &webmin_temp_dir_name(); +return $dir if (!defined($dir) || $dir eq ""); +if ($gconfig{'os_type'} eq 'windows' || $dir =~ /^[a-z]:/i) { + my $slash = $dir =~ /\// && $dir !~ /\\/ ? "/" : "\\"; + $dir =~ s/[\/\\]+$// if ($dir !~ /^[a-z]:[\/\\]?$/i); + return $dir if ($dir =~ /[\/\\]\Q$name\E$/); + return $dir =~ /^[a-z]:[\/\\]?$/i ? "$dir$name" : + "$dir$slash$name"; + } +$dir =~ s/\/+$// if ($dir ne "/"); +return $dir if ($dir =~ /(^|\/)\Q$name\E$/); +return $dir eq "/" ? "/$name" : "$dir/$name"; +} + =head2 default_webmin_temp_dir() Returns the built-in Webmin temporary directory path used when no tempdir @@ -474,7 +513,7 @@ configuration or environment override is set. =cut sub default_webmin_temp_dir { -return -d "c:/temp" ? "c:/temp" : "/tmp/.webmin"; +return -d "c:/temp" ? "c:/temp" : "/tmp/".&webmin_temp_dir_name(); } =head2 tempname_dir() diff --git a/webmin/change_advanced.cgi b/webmin/change_advanced.cgi index e10e2b0fa..a414a7fb0 100755 --- a/webmin/change_advanced.cgi +++ b/webmin/change_advanced.cgi @@ -9,8 +9,6 @@ require './webmin-lib.pl'; # Permissions used for newly created Webmin temp directories. my $advanced_temp_dir_perms = 0755; my $advanced_temp_dir_perms_text = sprintf("%04o", $advanced_temp_dir_perms); -my %advanced_system_temp_dirs = map { $_ => 1 } - ( "/dev/shm", "/tmp", "/var/tmp", "/usr/tmp" ); my @advanced_temp_dirs_to_create; # Save global temp dir setting @@ -145,15 +143,6 @@ if (defined($in{'sortconfigs'})) { &webmin_log("advanced"); -sub allowed_temp_dir -{ -my ($t) = @_; -my $dir = $t; -$dir =~ s/\/+$// if ($dir ne "/"); -return $dir eq "/" || $dir =~ /^\/[^\/]+$/ || - $advanced_system_temp_dirs{$dir} ? 0 : 1; -} - # Validate a configured Webmin temp directory without creating or changing it. # Missing components are queued and created after all form validation passes. sub validate_advanced_temp_dir @@ -163,6 +152,7 @@ $dir =~ /\S/ || &error($missing_error); $dir =~ s/\/+$// if ($dir ne "/"); $dir =~ /\S/ || &error($missing_error); if (&advanced_temp_dir_is_windows($dir)) { + $dir = &webmin_temp_dir_path($dir); if (-e $dir || -l $dir) { -d $dir || &error(&text('advanced_etempparent', $dir)); @@ -177,8 +167,9 @@ if ($dir =~ /^\//) { defined($sdir) || &error($missing_error); $dir = $sdir; } -&allowed_temp_dir($dir) || - &error(&text('advanced_etempallowed', $dir)); +# Treat the entered directory as a base path. The final Webmin-private +# component is always the hidden tempdirname setting, or .webmin by default. +$dir = &webmin_temp_dir_path($dir); # Walk the path so existing components are checked, while missing components # can be created after all form validation has passed. diff --git a/webmin/edit_advanced.cgi b/webmin/edit_advanced.cgi index c35ba2553..c02ecfc84 100755 --- a/webmin/edit_advanced.cgi +++ b/webmin/edit_advanced.cgi @@ -9,11 +9,12 @@ print &ui_form_start("change_advanced.cgi", "post"); print &ui_table_start($text{'advanced_header'}, undef, 2); # Global temp directory +my $tempdir_placeholder = &webmin_temp_dir_path("/var/tmp"); print &ui_table_row($text{'advanced_temp'}, &ui_opt_textbox("tempdir", $gconfig{'tempdir'}, 30, &text('advanced_tempdef', &default_webmin_temp_dir()), undef, undef, undef, undef, - "placeholder='/var/tmp/.webmin'"). + "placeholder='"."e_escape($tempdir_placeholder)."'"). "
". &ui_checkbox("tempdirdelete", 1, $text{'advanced_tdd'}, $gconfig{'tempdirdelete'})); diff --git a/webmin/lang/en b/webmin/lang/en index bec303568..4b61ab910 100644 --- a/webmin/lang/en +++ b/webmin/lang/en @@ -941,7 +941,6 @@ advanced_eprecache=Missing list of shell patterns to pre-cache advanced_err=Failed to save advanced options advanced_etemp=Missing or non-existant temporary files directory advanced_etdir=Missing or non-existant temporary files directory for $1 -advanced_etempallowed=Temporary files directory $1 is a system directory advanced_etempmkdir=Failed to create temporary files directory $1 : $2 advanced_etempparent=Cannot create temporary files directory because $1 exists and is not a directory advanced_etempparentperms=Parent directory $1 must be group and other executable.