Compare commits

..

11 Commits
2.640 ... 2.641

Author SHA1 Message Date
Jamie Cameron
ce1ab74c6f Merge pull request #2694 from swelljoe/login-redirect-password
Remove login_redirect
2026-05-10 20:13:52 -07:00
Joe Cooper
77d817357d Remove login_redirect 2026-05-10 20:52:09 -05:00
Jamie Cameron
350908ed56 changelog update 2026-05-10 17:56:11 -07:00
Jamie Cameron
94ac2ff7d7 New version bump 2026-05-10 17:46:56 -07:00
Jamie Cameron
a24e5371b7 Merge pull request #2691 from swelljoe/add-trusted-proxies
Add trusted_proxies config
2026-05-10 17:37:42 -07:00
Ilia Ross
0810083588 Fix skip hwclock when unavailable #2693
Some checks failed
webmin.dev: webmin/webmin / build (push) Has been cancelled
2026-05-10 19:48:18 +02:00
Ilia Ross
2ec6cadbbd Fix not to print newly created module config dirs to stdout 2026-05-10 19:36:28 +02:00
Joe Cooper
241abfe719 Add trusted_proxies config 2026-05-10 01:19:01 -05:00
Ilia Ross
f78413549b Fix Perl warning
Some checks failed
webmin.dev: webmin/webmin / build (push) Has been cancelled
2026-05-10 01:51:40 +02:00
Ilia Ross
b53cce9084 Add logging option to execute command sub
Some checks failed
webmin.dev: webmin/webmin / build (push) Has been cancelled
299b10a0e8 (r184647207)
2026-05-08 12:31:16 +02:00
Ilia Ross
a92b52f502 Fix text variable typo
https://github.com/webmin/webmin/issues/2686
2026-05-08 11:48:13 +02:00
16 changed files with 179 additions and 91 deletions

View File

@@ -1,5 +1,10 @@
## Changelog
#### 2.641 (May 10, 2026)
* Fixed a bug when editing monitors in the System and Server Status module
* Fix Fail2Ban default jail options
* Added support for trusted proxy IP addresses
#### 2.640 (May 4, 2026)
* Add new nftables module with profiles, saved tables, and chains/sets management
* Add new Nginx module with look and feel matching the Apache module

File diff suppressed because one or more lines are too long

View File

@@ -528,7 +528,7 @@ if [ "$post_config" = "1" ]; then
# Copy config file to /etc/webmin or /etc/usermin
os_type=`grep "^os_type=" /etc/$product/config | sed -e 's/os_type=//g'`
os_version=`grep "^os_version=" /etc/$product/config | sed -e 's/os_version=//g'`
/usr/bin/perl /usr/share/$product/copyconfig.pl \$os_type \$os_version /usr/share/$product /etc/$product $mod
/usr/bin/perl /usr/share/$product/copyconfig.pl \$os_type \$os_version /usr/share/$product /etc/$product $mod >/dev/null
# Update the ACL for the root user, or the first user in the ACL
grep "^root:" /etc/$product/webmin.acl >/dev/null

View File

@@ -492,7 +492,7 @@ if [ "$post_config" = "1" ]; then
os_version=`grep "^os_version=" /etc/$prog/config | sed -e 's/os_version=//g'`
real_os_type=`grep "^real_os_type=" /etc/$prog/config | sed -e 's/real_os_type=//g'`
real_os_version=`grep "^real_os_version=" /etc/$prog/config | sed -e 's/real_os_version=//g'`
/usr/bin/perl /usr/libexec/$prog/copyconfig.pl "\$os_type/\$real_os_type" "\$os_version/\$real_os_version" /usr/libexec/$prog /etc/$prog $mod
/usr/bin/perl /usr/libexec/$prog/copyconfig.pl "\$os_type/\$real_os_type" "\$os_version/\$real_os_version" /usr/libexec/$prog /etc/$prog $mod >/dev/null
# Update the ACL for the root user, or the first user in the ACL
grep "^root:" /etc/$prog/webmin.acl >/dev/null

View File

@@ -1469,6 +1469,19 @@ if ($headerhost) {
$headerhost = undef if (!&check_ipaddress($headerhost) &&
!&check_ip6address($headerhost));
}
# If trusted_proxies is configured, header-supplied client IP and SSL
# client info are only honored when the direct TCP peer is in that list.
# Otherwise drop them so an attacker reaching miniserv directly cannot
# spoof X-Forwarded-For or X-SSL-Client-* to bypass auth.
if ($config{'trust_real_ip'} && $config{'trusted_proxies'} ne '' &&
!&ip_match($acptip, $localip,
split(/\s+/, $config{'trusted_proxies'}))) {
print DEBUG "handle_request: peer $acptip not in trusted_proxies; ".
"ignoring forwarding and SSL client headers\n";
$headerhost = undef;
delete $header{'x-ssl-client-dn'};
delete $header{'x-ssl-client-verify'};
}
if ($config{'trust_real_ip'}) {
$acpthost = $headerhost || $acpthost;
if (&check_ipaddress($headerhost) || &check_ip6address($headerhost)) {
@@ -1723,7 +1736,7 @@ if ($header{'user-agent'} =~ /webmin/i ||
my $trust_ssl = $config{'trust_real_ip'} && !$config{'no_trust_ssl'};
if ($use_ssl && $verified_client ||
$trust_ssl && $header{'x-ssl-client-dn'} &&
$header{'x-ssl-client-verify'} =~ /^success/i) {
$header{'x-ssl-client-verify'} =~ /^success$/i) {
if ($use_ssl && $verified_client) {
$peername = Net::SSLeay::X509_NAME_oneline(
Net::SSLeay::X509_get_subject_name(
@@ -1731,8 +1744,12 @@ if ($use_ssl && $verified_client ||
$ssl_con)));
$u = &find_user_by_cert($peername);
}
if ($trust_ssl && !$u && $header{'x-ssl-client-dn'}) {
# Use proxied client cert
if ($trust_ssl && !$u && $header{'x-ssl-client-dn'} &&
!($use_ssl && $verified_client)) {
# Use proxied client cert (only when this connection
# is not itself a verified mTLS client; otherwise the
# header could be set by a real-cert client that didn't
# match a user, to authenticate as someone else).
$u = &find_user_by_cert($header{'x-ssl-client-dn'});
}
if ($u) {
@@ -4424,51 +4441,36 @@ if ($ok && (!$expired ||
&run_login_script($authuser, $sid,
$loghost, $localip);
# Check for a redirect URL for the user
local $rurl = &login_redirect($authuser, $pass, $host);
print DEBUG "handle_login: redirect URL rurl=$rurl\n";
if ($rurl) {
# Got one .. go to it
&write_data("HTTP/1.0 302 Moved Temporarily\r\n");
&write_data("Date: $datestr\r\n");
&write_data("Server: @{[&server_info()]}\r\n");
&write_data("Location: $rurl\r\n");
&write_keep_alive(0);
&write_data("\r\n");
&log_request($loghost, $authuser, $reqline, 302, 0);
# Set cookie and redirect to originally requested page
&write_data("HTTP/1.0 302 Moved Temporarily\r\n");
&write_data("Date: $datestr\r\n");
&write_data("Server: @{[&server_info()]}\r\n");
local $sec = $ssl ? "; secure" : "";
if (!$config{'no_httponly'}) {
$sec .= "; httpOnly";
}
if (!$config{'no_samesite'}) {
$sec .= "; SameSite=Lax";
}
if ($in{'page'} !~ /^\/[A-Za-z0-9\/\.\-\_:]+$/) {
# Make redirect URL safe
$in{'page'} = "/";
}
local $cpath = $config{'cookiepath'};
if ($in{'save'}) {
&write_data("Set-Cookie: $sidname=$sid; path=$cpath; ".
"expires=\"Thu, 31-Dec-2037 00:00:00\"$sec\r\n");
}
else {
# Set cookie and redirect to originally requested page
&write_data("HTTP/1.0 302 Moved Temporarily\r\n");
&write_data("Date: $datestr\r\n");
&write_data("Server: @{[&server_info()]}\r\n");
local $sec = $ssl ? "; secure" : "";
if (!$config{'no_httponly'}) {
$sec .= "; httpOnly";
}
if (!$config{'no_samesite'}) {
$sec .= "; SameSite=Lax";
}
if ($in{'page'} !~ /^\/[A-Za-z0-9\/\.\-\_:]+$/) {
# Make redirect URL safe
$in{'page'} = "/";
}
local $cpath = $config{'cookiepath'};
if ($in{'save'}) {
&write_data("Set-Cookie: $sidname=$sid; path=$cpath; ".
"expires=\"Thu, 31-Dec-2037 00:00:00\"$sec\r\n");
}
else {
&write_data("Set-Cookie: $sidname=$sid; path=$cpath".
"$sec\r\n");
}
&write_data("Location: $prot://$hostport$in{'page'}\r\n");
&write_keep_alive(0);
&write_data("\r\n");
&log_request($loghost, $authuser, $reqline, 302, 0);
syslog("info", "%s", "Successful login as $authuser from $loghost") if ($use_syslog);
&write_login_utmp($authuser, $acpthost);
&write_data("Set-Cookie: $sidname=$sid; path=$cpath".
"$sec\r\n");
}
&write_data("Location: $prot://$hostport$in{'page'}\r\n");
&write_keep_alive(0);
&write_data("\r\n");
&log_request($loghost, $authuser, $reqline, 302, 0);
syslog("info", "%s", "Successful login as $authuser from $loghost") if ($use_syslog);
&write_login_utmp($authuser, $acpthost);
return 0;
}
elsif ($ok && $expired &&
@@ -4980,20 +4982,6 @@ foreach my $c (keys %config) {
}
}
# login_redirect(username, password, host)
# Calls the login redirect script (if configured), which may output a URL to
# re-direct a user to after logging in.
sub login_redirect
{
return undef if (!$config{'login_redirect'});
local $quser = quotemeta($_[0]);
local $qpass = quotemeta($_[1]);
local $qhost = quotemeta($_[2]);
local $url = `$config{'login_redirect'} $quser $qpass $qhost`;
chop($url);
return $url;
}
# reload_config_file()
# Re-read %config, and call post-config actions
sub reload_config_file

View File

@@ -239,8 +239,8 @@ print &ui_table_row($text{'mon_ontimeout'},
# When to run commands?
print &ui_table_row($text{'mon_cmdmode'},
&ui_radio("cmdmode", $serv->{'cmdmode'} || 0,
[ [ 0, text{'mon_cmdmode0'} ],
[ 1, text{'mon_cmdmode1'} ] ]));
[ [ 0, $text{'mon_cmdmode0'} ],
[ 1, $text{'mon_cmdmode1'} ] ]));
print &ui_table_row(" ", "<font size=-1>$text{'mon_oninfo'}</font>",
undef, \@tds);

View File

@@ -456,9 +456,9 @@ if (!$config{'collect_notemp'} &&
$a->[0] =~ /^Temperature$/i ||
$a->[0] =~ /^Current\s+Drive\s+Temperature$/i ||
$a->[0] =~ /^Airflow\s+Temperature\s+Cel/i) &&
$a->[1] > 0) {
defined($a->[1]) && $a->[1] =~ /^\s*(\d+)/ && $1 > 0) {
push(@rv, { 'device' => $d->{'device'},
'temp' => int($a->[1]),
'temp' => int($1),
'errors' => $st->{'errors'},
'failed' => !$st->{'check'} });
last;

View File

@@ -1,8 +1,5 @@
#!/usr/local/bin/perl
local $format;
local $out;
require "./time-lib.pl";
use Time::Local;
@@ -17,17 +14,15 @@ $mode = "time";
if ($in{'action'} eq $text{'action_sync'}) {
# Set system time to hardware time
&error( $text{ 'acl_nosys' } ) if( $access{ 'sysdate' } );
local $flags = &get_hwclock_flags();
$out = &backquote_logged("hwclock $flags --hctosys");
&error( &text( 'error_sync', $out ) ) if( $out ne "" );
$err = &set_system_time_to_hardware_time();
&error( &text( 'error_sync', &html_escape($err) ) ) if ($err);
&webmin_log("sync");
} elsif ($in{'action'} eq $text{'action_sync_s'}) {
# Set hardware time to system time
&error( $text{ 'acl_nohw' } ) if( $access{ 'hwdate' } && $access{'sysdate'} );
local $flags = &get_hwclock_flags();
$out = &backquote_logged("hwclock $flags --systohc");
&error( &text( 'error_sync', $out ) ) if( $out ne "" );
$err = &set_hardware_time_to_system_time();
&error( &text( 'error_sync', &html_escape($err) ) ) if ($err);
&webmin_log("sync_s");
} elsif($in{'action'} eq $text{'action_apply'} || $in{'mode'} eq 'sysdate' ) {
@@ -111,4 +106,3 @@ if ($in{'action'} eq $text{'action_sync'}) {
}
&redirect("index.cgi?mode=$mode");

View File

@@ -104,7 +104,7 @@ if ($support_hwtime) {
print &tabletime(&hlink($text{'hw_title'}, "hardware_time"),
0, %hw_date);
print &ui_submit($text{'action_save'}, "action");
if (support_hwtime()) {
if ($support_hwtime) {
print &ui_submit($text{'action_sync_s'}, "action");
}
print &ui_form_end();
@@ -153,7 +153,8 @@ if ($access{'timezone'} && &has_timezone()) {
print &ui_tabs_end_tab();
}
if ( ( !$access{ 'sysdate' } && &has_command( "date" ) || !$access{ 'hwdate' } && &has_command( "hwclock" ) ) && $access{'ntp'} )
if (((!$access{'sysdate'} && &has_command("date")) ||
(!$access{'hwdate'} && $support_hwtime)) && $access{'ntp'})
{
# Show time server input
print &ui_tabs_start_tab("mode", "sync");

View File

@@ -102,9 +102,10 @@ elsif ($?) {
if ($hwtoo) {
# Set hardware clock time to match system time (which is now correct)
my $flags = &get_hwclock_flags();
my $out = &backquote_logged("hwclock $flags --systohc");
return $? ? $out : undef;
if (&support_hwtime()) {
my $err = &set_hardware_time_to_system_time();
return $err if ($err);
}
}
return undef;
@@ -163,16 +164,34 @@ else {
}
}
# hwclock_command()
# Returns the path to the hwclock command, if available
sub hwclock_command
{
return &has_command("hwclock");
}
# hwclock_missing_error()
sub hwclock_missing_error
{
return &text('error_cnf', "hwclock");
}
# get_hardware_time()
# Returns the current hardware time, in localtime format. On failure returns
# an empty array, and sets the global $get_hardware_time_error
sub get_hardware_time
{
my $hwclock = &hwclock_command();
$get_hardware_time_error = undef;
if (!$hwclock) {
$get_hardware_time_error = &hwclock_missing_error();
return ( );
}
my $flags = &get_hwclock_flags();
$flags ||= "";
$get_hardware_time_error = undef;
&clean_language();
my $out = &backquote_command("hwclock $flags 2>/dev/null");
my $out = &backquote_command("$hwclock $flags 2>/dev/null");
&reset_environment();
if ($out =~ /^(\S+)\s+(\S+)\s+(\d+)\s+(\d+):(\d+):(\d+)\s+(\d+)\s+/) {
return ($6, $5, $4, $3, &month_to_number($2), $7-1900, &weekday_to_number($1));
@@ -186,7 +205,7 @@ elsif ($out =~ /^(\d+)\-(\d+)\-(\d+)\s+(\d+):(\d+):(\d+)/) {
}
else {
$get_hardware_time_error = &text('index_ehwclock',
"<tt>".&html_escape("hwclock $flags")."</tt>",
"<tt>".&html_escape("$hwclock $flags")."</tt>",
"<pre>".&html_escape($out)."</pre>");
return ( );
}
@@ -203,15 +222,41 @@ return localtime(time());
sub set_hardware_time
{
my ($second, $minute, $hour, $date, $month, $year) = @_;
my $hwclock = &hwclock_command();
return &hwclock_missing_error() if (!$hwclock);
$month++;
$year += 1900;
my $format = "--set --date=".
quotemeta("$year-$month-$date $hour:$minute:$second");
my $flags = &get_hwclock_flags();
my $out = &backquote_logged("hwclock $flags $format 2>&1");
my $out = &backquote_logged("$hwclock $flags $format 2>&1");
return $? ? $out : undef;
}
# set_hardware_time_to_system_time()
# Sets the hardware time to the current system time
sub set_hardware_time_to_system_time
{
my $hwclock = &hwclock_command();
return &hwclock_missing_error() if (!$hwclock);
my $flags = &get_hwclock_flags();
$flags ||= "";
my $out = &backquote_logged("$hwclock $flags --systohc 2>&1");
return $? ? $out : undef;
}
# set_system_time_to_hardware_time()
# Sets the system time to the current hardware time
sub set_system_time_to_hardware_time
{
my $hwclock = &hwclock_command();
return &hwclock_missing_error() if (!$hwclock);
my $flags = &get_hwclock_flags();
$flags ||= "";
my $out = &backquote_logged("$hwclock $flags --hctosys 2>&1");
return $? || $out ne "" ? $out : undef;
}
# set_system_time(secs, mins, hours, day, month, year)
sub set_system_time
{
@@ -278,8 +323,9 @@ return defined($_[0]) ? ucfirst($weekday_names[$_[0]]) : undef;
# Returns 1 if this system supports setting the hardware clock.
sub support_hwtime
{
return &has_command("hwclock") &&
&execute_command("hwclock") == 0 &&
my $hwclock = &hwclock_command();
return $hwclock &&
&execute_command($hwclock) == 0 &&
!&running_in_xen() && !&running_in_vserver() &&
!&running_in_openvz() && !&running_in_zone();
}
@@ -307,4 +353,3 @@ if ($modconf_info) {
}
1;

View File

@@ -1 +1 @@
2.640
2.641

View File

@@ -11559,7 +11559,7 @@ my %access = &get_module_acl($u, $m, 1);
return $access{'rbac'} ? 1 : 0;
}
=head2 execute_command(command, stdin, stdout, stderr, translate-files?, safe?)
=head2 execute_command(command, stdin, stdout, stderr, translate-files?, safe?, logged?)
Runs some command, possibly feeding it input and capturing output to the
give files or scalar references. The parameters are :
@@ -11576,16 +11576,19 @@ give files or scalar references. The parameters are :
=item safe - Set to 1 if this command is safe and does not modify the state of the system.
=item logged - Set to 1 if the command should be logged.
=cut
sub execute_command
{
my ($cmd, $stdin, $stdout, $stderr, $trans, $safe) = @_;
my ($cmd, $stdin, $stdout, $stderr, $trans, $safe, $logged) = @_;
if (&is_readonly_mode() && !$safe) {
print STDERR "Vetoing command $_[0]\n";
$? = 0;
return 0;
}
$cmd = &translate_command($cmd);
&additional_log('exec', undef, $cmd) if ($logged);
# Use ` operator where possible
&webmin_debug_log('CMD', "cmd=$cmd") if ($gconfig{'debug_what_cmd'});
@@ -11681,6 +11684,17 @@ waitpid($pid, 0);
return $?;
}
=head2 execute_command_logged(command, stdin, stdout, stderr, translate-files?, safe?)
Like execute_command, but also logs the command run.
=cut
sub execute_command_logged
{
my ($cmd, $stdin, $stdout, $stderr, $trans, $safe) = @_;
return &execute_command($cmd, $stdin, $stdout, $stderr, $trans, $safe, 1);
}
=head2 open_readfile(handle, file)
Opens some file for reading. Returns 1 on success, 0 on failure. Pretty much

View File

@@ -30,6 +30,15 @@ if (!$@ && $in{'libwrap'}) {
}
}
@tprox = split(/\s+/, $in{'trusted_proxies'});
foreach $h (@tprox) {
$err = &valid_allow($h);
&error($err) if ($err);
}
if ($in{'trust'} == 2 && !@tprox) {
&error($text{'access_etproxies'});
}
&lock_file($ENV{'MINISERV_CONFIG'});
&get_miniserv_config(\%miniserv);
delete($miniserv{"allow"});
@@ -52,6 +61,7 @@ else {
$miniserv{'trust_real_ip'} = 0;
$miniserv{'no_trust_ssl'} = 1;
}
$miniserv{'trusted_proxies'} = join(' ', @tprox);
&put_miniserv_config(\%miniserv);
&unlock_file($ENV{'MINISERV_CONFIG'});
&show_restart_page();

View File

@@ -38,6 +38,10 @@ print &ui_table_row(&hlink($text{'access_trust_lvl'}, "access_trust_lvl"),
[ 1, $text{'access_trust_lvl1'} ],
[ 2, $text{'access_trust_lvl2'} ] ]));
@tprox = split(/\s+/, $miniserv{'trusted_proxies'});
print &ui_table_row(&hlink($text{'access_tproxies'}, "access_tproxies"),
&ui_textarea("trusted_proxies", join("\n", @tprox), 4, 30));
eval "use Authen::Libwrap qw(hosts_ctl STRING_UNKNOWN)";
if (!$@) {
print &ui_table_row($text{'access_libwrap'},

View File

@@ -0,0 +1,25 @@
<header>Trusted proxy addresses</header>
A list of IP addresses or networks (one per line, in the same format as the
allowed addresses field above) that are permitted to provide proxy headers
such as <tt>X-Forwarded-For</tt>, <tt>X-Real-IP</tt>, <tt>X-SSL-Client-DN</tt>
and <tt>X-SSL-Client-Verify</tt>.
<p>
When this list is set, headers received from any other peer are ignored,
preventing a client that can reach Webmin directly from spoofing its source
IP or impersonating a user via a fake SSL client certificate header.
<p>
This field is required when the trust level is set to trust both the remote
IP and SSL certificate provided by proxies. It is strongly recommended
whenever any proxy header trust is enabled.
<p>
Example:
<pre>
10.0.0.5
192.168.1.0/24
2001:DB8::/32
</pre>
<p></p>

View File

@@ -35,6 +35,8 @@ access_trust_lvl=Trust level for proxy headers
access_trust_lvl0=No, do not trust any headers from the proxy
access_trust_lvl1=Yes, trust the remote IP address provided by proxies
access_trust_lvl2=Yes, trust both the remote IP and SSL cert provided by proxies
access_tproxies=Trusted proxy addresses
access_etproxies=At least one trusted proxy address must be entered when trusting the remote IP and SSL cert from proxies
bind_title=Ports and Addresses
bind_desc2=This form can be used to change the port number that Webmin listens on, or have it listen on only a single IP address on your system. You can also configure it to accept connections on multiple ports, or to listen on several IP addresses. Note - your web browser may prompt you to log in again after changing the port or binding address.