Fix to enforce private basename for Webmin temp dirs

ⓘ 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.
This commit is contained in:
Ilia Ross
2026-06-18 20:48:47 +02:00
parent 0d4c65ec04
commit ccd2b13942
6 changed files with 77 additions and 17 deletions

File diff suppressed because one or more lines are too long

View File

@@ -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]).

View File

@@ -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()

View File

@@ -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.

View File

@@ -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='".&quote_escape($tempdir_placeholder)."'").
"<br>".
&ui_checkbox("tempdirdelete", 1, $text{'advanced_tdd'},
$gconfig{'tempdirdelete'}));

View File

@@ -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 <tt>$1</tt> must be group and other executable.