From d202eca8f81d6a207aba8e2537394b988b6118f8 Mon Sep 17 00:00:00 2001 From: Joe Cooper Date: Mon, 11 May 2026 21:46:10 -0500 Subject: [PATCH 01/11] Probably resolve proxied keep-alive requests retain auth state --- miniserv.pl | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/miniserv.pl b/miniserv.pl index cb4d8a285..ac52dd7bc 100755 --- a/miniserv.pl +++ b/miniserv.pl @@ -1317,6 +1317,48 @@ $reqline = $request_uri = $page = undef; $authuser = undef; $validated = undef; +# Reset all per-request state. Keep-alive lets one child handle many +# requests on one TCP connection; behind a proxy that pools backend +# connections, those requests can be from different clients. Anything +# left set from the prior request would leak across that boundary - +# most dangerously $baseauthuser, which feeds BASE_REMOTE_USER and the +# Webmin ACL layer. +$baseauthuser = undef; +$authpass = undef; +$session_id = undef; +$already_session_id = undef; +$miniserv_internal = undef; +$querystring = undef; +$queryargs = undef; +$pathinfo = undef; +$peername = undef; +$uinfo = undef; +$scriptname = undef; +$cgi_pwd = undef; +$full = undef; +$realroot = undef; +$foundroot = undef; +$is_directory = undef; +$nph_script = undef; +$logout = undef; +$failed_user = undef; +$failed_save = undef; +$twofactor_msg = undef; +$timed_out = undef; +$error_handler_recurse = undef; +%cgiheader = (); +@cgiheader = (); +$doneheaders = undef; +$headers = undef; +@stfull = (); +# Restore $host/$port to socket defaults; the Host: header overwrite +# below should not persist if the next request omits Host. +local $host = $host; +local $port = $port; +# Scope per-request mutation of %config so it cannot leak to later +# requests on this connection (see $config{'session'} = 0 below). +local $config{'session'} = $config{'session'}; + # check address against access list if (@deny && &ip_match($acptip, $localip, @deny) || @allow && !&ip_match($acptip, $localip, @allow)) { From 29952dce1e7655465a571f7378246eae87dd92b5 Mon Sep 17 00:00:00 2001 From: Joe Cooper Date: Mon, 11 May 2026 21:57:34 -0500 Subject: [PATCH 02/11] Also reset already_authuser --- miniserv.pl | 1 + 1 file changed, 1 insertion(+) diff --git a/miniserv.pl b/miniserv.pl index ac52dd7bc..ed38171ba 100755 --- a/miniserv.pl +++ b/miniserv.pl @@ -1327,6 +1327,7 @@ $baseauthuser = undef; $authpass = undef; $session_id = undef; $already_session_id = undef; +$already_authuser = undef; $miniserv_internal = undef; $querystring = undef; $queryargs = undef; From 084f7b7314c43b8ec528b3018034977216cac5e3 Mon Sep 17 00:00:00 2001 From: Ilia Ross <4426533+iliaross@users.noreply.github.com> Date: Tue, 12 May 2026 13:00:03 +0200 Subject: [PATCH 03/11] Revert "perlcritic fixes" --- nftables/acl_security.pl | 5 ++--- nftables/apply-boot.pl | 2 +- nftables/nftables-lib.pl | 6 +++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/nftables/acl_security.pl b/nftables/acl_security.pl index b592349f4..3e6271792 100644 --- a/nftables/acl_security.pl +++ b/nftables/acl_security.pl @@ -3,7 +3,7 @@ use warnings; no warnings 'redefine'; no warnings 'uninitialized'; -require './nftables-lib.pl'; ## no critic (Modules::RequireBarewordIncludes) +require 'nftables-lib.pl'; our (%in, %text); # acl_security_form(&options) @@ -86,6 +86,5 @@ if (!$err) { ); } } -my @sorted = sort { $a->[1] cmp $b->[1] } @opts; -return @sorted; +return sort { $a->[1] cmp $b->[1] } @opts; } diff --git a/nftables/apply-boot.pl b/nftables/apply-boot.pl index 5f57fbeeb..6742fac99 100755 --- a/nftables/apply-boot.pl +++ b/nftables/apply-boot.pl @@ -11,7 +11,7 @@ $no_acl_check++; if ($0 =~ /^(.*\/)[^\/]+$/) { chdir($1); } -require './nftables-lib.pl'; ## no critic (Modules::RequireBarewordIncludes) +require './nftables-lib.pl'; if ($module_name ne 'nftables') { print STDERR "Command must be run with full path\n"; exit(5); diff --git a/nftables/nftables-lib.pl b/nftables/nftables-lib.pl index b57fa79fe..472d11e17 100644 --- a/nftables/nftables-lib.pl +++ b/nftables/nftables-lib.pl @@ -153,9 +153,9 @@ return has_command($cmd); sub nft_version_text { my $cmd = get_nft_command(); -return if (!$cmd); +return undef if (!$cmd); my $out = backquote_command(quotemeta($cmd)." --version 2>&1"); -return if ($? || !$out); +return undef if ($? || !$out); $out =~ s/\r?\n.*$//s; $out =~ s/^\s+|\s+$//g; if ($out =~ /^nftables\s+v?(\S+)(?:\s+(.*))?$/i) { @@ -170,7 +170,7 @@ return $out; # Returns an error message if nftables is not installed, undef if all is OK sub check_nftables { -return if (get_nft_command()); +return undef if (get_nft_command()); return text('index_ecommand', "nft"); } From d46c8f20d5063eff5603df699450f1961fecb8b1 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Tue, 12 May 2026 17:51:37 +0200 Subject: [PATCH 04/11] Fix escapes --- nis/aix-lib.pl | 4 ++-- nis/open-linux-lib.pl | 11 ++++++++--- nis/solaris-lib.pl | 2 +- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/nis/aix-lib.pl b/nis/aix-lib.pl index 0ac2999a1..2186e4e66 100755 --- a/nis/aix-lib.pl +++ b/nis/aix-lib.pl @@ -191,8 +191,8 @@ if ($in{'domain_def'}) { } else { local $old = `domainname`; chop($old); - &system_logged("chypdom -B \"$in{'domain'}\""); - &system_logged("domainname \"$in{'domain'}\" >/dev/null 2>&1"); + &system_logged("chypdom -B ".quotemeta($in{'domain'})); + &system_logged("domainname ".quotemeta($in{'domain'})." >/dev/null 2>&1"); if ($in{'boot'}) { # Create the domain directory mkdir("/var/yp/$in{'domain'}", 0755); diff --git a/nis/open-linux-lib.pl b/nis/open-linux-lib.pl index 6d0bd92e2..b08a1ba97 100755 --- a/nis/open-linux-lib.pl +++ b/nis/open-linux-lib.pl @@ -143,6 +143,11 @@ for($n=0; defined($in{"old_$n"}); $n++) { &error(&text('server_edomain', $in{"domain_$n"})); local $domain = $in{"domain_def_$n"} ? undef : $in{"domain_$n"}; local $old = $in{"old_$n"}; + if ($old) { + $old =~ /^[A-Za-z0-9\.\-]+$/ && $old !~ /^\./ && + -r "$nis_config_dir/$old/.nisupdate.conf" || + &error(&text('server_edomain', $old)); + } if (!$old && !$domain) { # No domain before, and none chosen next; @@ -150,12 +155,12 @@ for($n=0; defined($in{"old_$n"}); $n++) { elsif (!$old && $domain) { # New domain added mkdir("$nis_config_dir/$domain", 0755); - &system_logged("cp nisupdate.conf ". - "$nis_config_dir/$domain/.nisupdate.conf"); + ©_source_dest("nisupdate.conf", + "$nis_config_dir/$domain/.nisupdate.conf"); } elsif ($old && !$domain) { # Domain taken away - &system_logged("rm -rf $nis_config_dir/$old"); + &unlink_logged("$nis_config_dir/$old"); next; } elsif ($old ne $domain) { diff --git a/nis/solaris-lib.pl b/nis/solaris-lib.pl index b2db6d1d6..f9eb609eb 100755 --- a/nis/solaris-lib.pl +++ b/nis/solaris-lib.pl @@ -193,7 +193,7 @@ else { &open_tempfile(DOM, ">/etc/defaultdomain"); &print_tempfile(DOM, "$in{'domain'}\n"); &close_tempfile(DOM); - &system_logged("domainname \"$in{'domain'}\" >/dev/null 2>&1"); + &system_logged("domainname ".quotemeta($in{'domain'})." >/dev/null 2>&1"); if ($in{'boot'}) { # Create the domain directory mkdir("/var/yp/$in{'domain'}", 0755); From 0863d6ba7a58694c94341dec822ec93e3037c19f Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Tue, 12 May 2026 18:01:16 +0200 Subject: [PATCH 05/11] Revert #2700 reverted but only fix exact bug This reverts commit 0d3e3d9473e11d57e7b80b499e037b64b36f816f, reversing changes made to 236c5cf48933dadd9373e7a514aaf727eb7e9407. --- nftables/acl_security.pl | 3 ++- nftables/apply-boot.pl | 2 +- nftables/nftables-lib.pl | 6 +++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/nftables/acl_security.pl b/nftables/acl_security.pl index 3e6271792..0bd789048 100644 --- a/nftables/acl_security.pl +++ b/nftables/acl_security.pl @@ -86,5 +86,6 @@ if (!$err) { ); } } -return sort { $a->[1] cmp $b->[1] } @opts; +my @sorted = sort { $a->[1] cmp $b->[1] } @opts; +return @sorted; } diff --git a/nftables/apply-boot.pl b/nftables/apply-boot.pl index 6742fac99..5f57fbeeb 100755 --- a/nftables/apply-boot.pl +++ b/nftables/apply-boot.pl @@ -11,7 +11,7 @@ $no_acl_check++; if ($0 =~ /^(.*\/)[^\/]+$/) { chdir($1); } -require './nftables-lib.pl'; +require './nftables-lib.pl'; ## no critic (Modules::RequireBarewordIncludes) if ($module_name ne 'nftables') { print STDERR "Command must be run with full path\n"; exit(5); diff --git a/nftables/nftables-lib.pl b/nftables/nftables-lib.pl index 472d11e17..b57fa79fe 100644 --- a/nftables/nftables-lib.pl +++ b/nftables/nftables-lib.pl @@ -153,9 +153,9 @@ return has_command($cmd); sub nft_version_text { my $cmd = get_nft_command(); -return undef if (!$cmd); +return if (!$cmd); my $out = backquote_command(quotemeta($cmd)." --version 2>&1"); -return undef if ($? || !$out); +return if ($? || !$out); $out =~ s/\r?\n.*$//s; $out =~ s/^\s+|\s+$//g; if ($out =~ /^nftables\s+v?(\S+)(?:\s+(.*))?$/i) { @@ -170,7 +170,7 @@ return $out; # Returns an error message if nftables is not installed, undef if all is OK sub check_nftables { -return undef if (get_nft_command()); +return if (get_nft_command()); return text('index_ecommand', "nft"); } From 2b8091537caf796c31da68e899f768a049357363 Mon Sep 17 00:00:00 2001 From: Joe Cooper Date: Tue, 12 May 2026 16:31:24 -0500 Subject: [PATCH 06/11] Ignore ugly require in acl_security.pl --- nftables/acl_security.pl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nftables/acl_security.pl b/nftables/acl_security.pl index 0bd789048..eedad70c9 100644 --- a/nftables/acl_security.pl +++ b/nftables/acl_security.pl @@ -3,7 +3,7 @@ use warnings; no warnings 'redefine'; no warnings 'uninitialized'; -require 'nftables-lib.pl'; +require 'nftables-lib.pl'; ## no critic our (%in, %text); # acl_security_form(&options) From c6647ce76c047b76adbf76998543e66b37d1bdda Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Wed, 13 May 2026 00:46:39 +0200 Subject: [PATCH 07/11] Fix to scope SSL cert auth user to one request * Note: Declare the SSL certificate lookup user as lexical inside `handle_request`, so a previously matched client certificate user cannot survive into later keep-alive requests handled by the same miniserv child. Enlightened by: https://github.com/webmin/webmin/pull/2699 --- miniserv.pl | 1 + 1 file changed, 1 insertion(+) diff --git a/miniserv.pl b/miniserv.pl index 1d4a6f735..b3ae6421a 100755 --- a/miniserv.pl +++ b/miniserv.pl @@ -1743,6 +1743,7 @@ 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) { + my $u; if ($use_ssl && $verified_client) { $peername = Net::SSLeay::X509_NAME_oneline( Net::SSLeay::X509_get_subject_name( From 911aa64a36b50fb26d6b69e5a3f0e20d6659e263 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Wed, 13 May 2026 01:06:57 +0200 Subject: [PATCH 08/11] Fix systemd multiline ExecStart handling * Note: Generate separate ExecStart= entries for newline-separated systemd start commands and set Type=oneshot when required. https://github.com/webmin/webmin/issues/2697 --- init/init-lib.pl | 80 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 67 insertions(+), 13 deletions(-) diff --git a/init/init-lib.pl b/init/init-lib.pl index 5fffba394..8ee2d94a3 100644 --- a/init/init-lib.pl +++ b/init/init-lib.pl @@ -2363,6 +2363,51 @@ my $out = &backquote_logged( return (!$?, $out); } +=head2 split_systemd_exec_commands(command) + +Splits a multi-line systemd command field into individual command lines. + +=cut + +sub split_systemd_exec_commands +{ +my ($cmd) = @_; +return ( ) if (!defined($cmd)); +$cmd =~ s/\r//g; +my @rv; +foreach my $l (split(/\n/, $cmd)) { + $l =~ s/^\s+//; + $l =~ s/\s+$//; + push(@rv, $l) if ($l =~ /\S/); + } +return @rv; +} + +=head2 systemd_shell_exec_command(shell, command) + +Returns a systemd command line to run some command via a shell. + +=cut + +sub systemd_shell_exec_command +{ +my ($sh, $cmd) = @_; +$cmd =~ s/'/'\\''/g; +return "$sh -c '$cmd'"; +} + +=head2 format_systemd_exec_command(shell, command) + +Returns a systemd command line, using a shell if redirection is needed. + +=cut + +sub format_systemd_exec_command +{ +my ($sh, $cmd) = @_; +return $cmd =~ /<|>/ ? &systemd_shell_exec_command($sh, $cmd) : $cmd; +} + =head2 create_systemd_service(name, description, start-script, stop-script, restart-script, [forks], [pidfile]) @@ -2372,20 +2417,22 @@ Create a new systemd service with the given details. sub create_systemd_service { my ($name, $desc, $start, $stop, $restart, $forks, $pidfile, $exits, $opts) = @_; -$start =~ s/\r?\n/ ; /g; -$stop =~ s/\r?\n/ ; /g; -$restart =~ s/\r?\n/ ; /g; my $sh = &has_command("sh") || "sh"; my $kill = &has_command("kill") || "kill"; -if ($start =~ /<|>/) { - $start = "$sh -c '$start'"; +my @starts = &split_systemd_exec_commands($start); +my @stops = &split_systemd_exec_commands($stop); +my @restarts = &split_systemd_exec_commands($restart); +my $start_type = ref($opts) ? $opts->{'type'} : undef; +$start_type ||= $forks ? 'forking' : $exits ? 'oneshot' : undef; +my $multi_start_oneshot = @starts > 1 && !$start_type; +if (@starts > 1 && $start_type && $start_type ne 'oneshot') { + @starts = (&systemd_shell_exec_command($sh, join("; ", @starts))); } -if ($restart =~ /<|>/) { - $restart = "$sh -c '$restart'"; - } -if ($stop =~ /<|>/) { - $stop = "$sh -c '$stop'"; +else { + @starts = map { &format_systemd_exec_command($sh, $_) } @starts; } +@stops = map { &format_systemd_exec_command($sh, $_) } @stops; +@restarts = map { &format_systemd_exec_command($sh, $_) } @restarts; my $cfile = &get_systemd_root($name)."/".$name; &open_lock_tempfile(CFILE, ">$cfile"); &print_tempfile(CFILE, "[Unit]\n"); @@ -2401,9 +2448,16 @@ if (ref($opts)) { } &print_tempfile(CFILE, "\n"); &print_tempfile(CFILE, "[Service]\n"); -&print_tempfile(CFILE, "ExecStart=$start\n"); -&print_tempfile(CFILE, "ExecStop=$stop\n") if ($stop); -&print_tempfile(CFILE, "ExecReload=$restart\n") if ($restart); +&print_tempfile(CFILE, "Type=oneshot\n") if ($multi_start_oneshot); +foreach my $start (@starts) { + &print_tempfile(CFILE, "ExecStart=$start\n"); + } +foreach my $stop (@stops) { + &print_tempfile(CFILE, "ExecStop=$stop\n"); + } +foreach my $restart (@restarts) { + &print_tempfile(CFILE, "ExecReload=$restart\n"); + } &print_tempfile(CFILE, "Type=forking\n") if ($forks); &print_tempfile(CFILE, "Type=oneshot\n", "RemainAfterExit=yes\n") if ($exits); From 413087ae84d22bc7f05b7dc37bd882e1ac8a2ac4 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Wed, 13 May 2026 02:20:42 +0200 Subject: [PATCH 09/11] Fix MariaDB create user auth plugin syntax * Note: Use MariaDB-compatible IDENTIFIED VIA ... USING PASSWORD(...) syntax when creating users with an explicit authentication plugin, while preserving default password creation and MySQL behavior. https://forum.virtualmin.com/t/mariadb-syntax-change-on-rocky-10/137187 --- mysql/mysql-lib.pl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mysql/mysql-lib.pl b/mysql/mysql-lib.pl index 94034d946..a961ae4aa 100755 --- a/mysql/mysql-lib.pl +++ b/mysql/mysql-lib.pl @@ -2006,11 +2006,12 @@ my $other_field_values = $sc->{'other_field_values'}; my ($ver, $variant) = &get_remote_mysql_variant(); my $plugin = $sc->{'plugin'} || &get_mysql_plugin(); -$plugin = $plugin ? "with $plugin" : ""; if ($variant eq "mariadb" && &compare_version_numbers($ver, "10.4") >= 0) { - my $sql = "create user '$user'\@'$host' identified $plugin by ". - "'".&escapestr($pass)."'"; + my $auth = $plugin ? + &get_plugin_sql($ver, $variant, $pass, $plugin) : + "identified by '".&escapestr($pass)."'"; + my $sql = "create user '$user'\@'$host' $auth"; &execute_sql_logged($master_db, $sql); &execute_sql_logged($master_db, 'flush privileges'); @@ -2036,6 +2037,7 @@ else { &execute_sql_logged($master_db, 'flush privileges'); if ($variant eq "mysql" && &compare_version_numbers($ver, "5.7.6") >= 0) { + $plugin = $plugin ? "with $plugin" : ""; &execute_sql_logged($master_db, "alter user '$user'\@'$host' identified $plugin by ". "'".&escapestr($pass)."'"); From 0db0cf77f9769c72401587c1165ccfe2d9ee827a Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Wed, 13 May 2026 23:12:42 +0200 Subject: [PATCH 10/11] Fix to disregard silly new line option --- ui-lib.pl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui-lib.pl b/ui-lib.pl index f557848a7..52cceb5b7 100755 --- a/ui-lib.pl +++ b/ui-lib.pl @@ -3471,13 +3471,13 @@ return &theme_ui_brh() if (defined(&theme_ui_brh)); return "
\n"; } -# ui_tag_start(tag, [attrs], [no-new-line]) +# ui_tag_start(tag, [attrs]) # Function to create an opening HTML tag with optional attributes. # Attributes are passed as a hash reference and its values are quote escaped. sub ui_tag_start { return theme_ui_tag_start(@_) if (defined(&theme_ui_tag_start)); -my ($tag, $attrs, $nnl) = @_; +my ($tag, $attrs) = @_; # Ensure every tag gets a proper marker class $attrs ||= {}; @@ -3505,7 +3505,7 @@ if ($attrs && ref($attrs) eq 'HASH') { } # Close the opening tag -$rv .= $nnl ? ">" : ">\n"; +$rv .= ">"; # Handle special case for tag $rv = "\n$rv" if ($tag eq 'html'); @@ -3539,7 +3539,7 @@ sub ui_tag { return theme_ui_tag(@_) if (defined(&theme_ui_tag)); my ($tag, $content, $attrs) = @_; -my $rv = ui_tag_start($tag, $attrs, !defined($content)); +my $rv = ui_tag_start($tag, $attrs); $rv .= ui_tag_content($content) if (defined($content)); my %void_tags = map { $_ => 1 } qw( From d3671897114b335608f64c0bfb9c0c4cb64d698d Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Thu, 14 May 2026 00:38:34 +0200 Subject: [PATCH 11/11] Fix to reset remaining per-request keep-alive state too https://github.com/webmin/webmin/pull/2699#issuecomment-4435490798 --- miniserv.pl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/miniserv.pl b/miniserv.pl index 70b374e4d..3d0800744 100755 --- a/miniserv.pl +++ b/miniserv.pl @@ -1360,6 +1360,10 @@ $error_handler_recurse = undef; $doneheaders = undef; $headers = undef; @stfull = (); +# Reset logout-triggered auth suppression and any partial POST body +# count before handling the next request on this connection. +$deny_authentication = undef; +$clen_read = 0; # Restore $host/$port to socket defaults; the Host: header overwrite # below should not persist if the next request omits Host. local $host = $host;