From 955daa53c6fd7571a8550ccbf81d9a7ed3344709 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Sun, 12 Jul 2026 23:50:34 +0200 Subject: [PATCH 1/6] Fix to use physical ACL restore when supported MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⓘ Detect support for combining `-P` with `setfacl --restore` to prevent acl 2.4.0 security warnings while retaining compatibility with older releases. --- web-lib-funcs.pl | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/web-lib-funcs.pl b/web-lib-funcs.pl index 08927da2a..953a39e13 100755 --- a/web-lib-funcs.pl +++ b/web-lib-funcs.pl @@ -11390,7 +11390,8 @@ elsif (defined($main::open_tempfiles{$_[0]})) { } if ($file_acls) { # Set original ACLs - open(my $pipe, '|-', "$setfacl --restore=-"); + my $restore_command = &get_setfacl_restore_command($setfacl); + open(my $pipe, '|-', $restore_command); print($pipe $file_acls); close($pipe); } @@ -11455,6 +11456,27 @@ if (!defined($main::selinux_enabled_cache)) { return $main::selinux_enabled_cache; } +=head2 get_setfacl_restore_command(setfacl-command) + +Returns a command for restoring ACLs from standard input. Uses physical +restore when supported by setfacl, while remaining compatible with versions +older than 2.4.0 which reject -P together with --restore. + +=cut +sub get_setfacl_restore_command +{ +my ($setfacl) = @_; +state %physical_restore_cache; + +if (!exists($physical_restore_cache{$setfacl})) { + my $help = &backquote_command("$setfacl --help 2>&1 Date: Sun, 12 Jul 2026 23:50:48 +0200 Subject: [PATCH 2/6] Add regression tests previous patch --- t/web-lib-funcs-acl.t | 49 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 t/web-lib-funcs-acl.t diff --git a/t/web-lib-funcs-acl.t b/t/web-lib-funcs-acl.t new file mode 100644 index 000000000..1a8d46124 --- /dev/null +++ b/t/web-lib-funcs-acl.t @@ -0,0 +1,49 @@ +#!/usr/bin/perl +# Unit tests for ACL preservation helpers in web-lib-funcs.pl. + +use strict; +use warnings; +use Test::More; +use File::Basename qw(dirname); +use File::Spec; + +my $script = File::Spec->rel2abs( + File::Spec->catfile(dirname(__FILE__), '..', 'web-lib-funcs.pl')); +require $script; + +subtest 'physical restore is enabled when supported' => sub { + no warnings 'redefine'; + my $calls = 0; + local *main::backquote_command = sub { + $calls++; + return "setfacl 2.4.0 -- set file access control lists\n". + "Usage: setfacl [-bkndRLP] { -m|-M|-x|-X ... } file ...\n". + " setfacl [-P] --restore=file\n". + " -P, --physical physical walk, do not follow symbolic links\n". + " --restore=file restore ACLs (inverse of `getfacl -R')\n"; + }; + + is(main::get_setfacl_restore_command('/usr/bin/setfacl'), + '/usr/bin/setfacl -P --restore=-', + 'uses physical restore with setfacl 2.4.0 and later'); + is(main::get_setfacl_restore_command('/usr/bin/setfacl'), + '/usr/bin/setfacl -P --restore=-', + 'keeps using physical restore on later calls'); + is($calls, 1, 'caches the capability check'); +}; + +subtest 'older setfacl remains supported' => sub { + no warnings 'redefine'; + local *main::backquote_command = sub { + return "setfacl 2.3.2 -- set file access control lists\n". + "Usage: setfacl [-bkndRLP] { -m|-M|-x|-X ... } file ...\n". + " -P, --physical physical walk, do not follow symbolic links\n". + " --restore=file restore ACLs (inverse of `getfacl -R')\n"; + }; + + is(main::get_setfacl_restore_command('/usr/local/bin/setfacl'), + '/usr/local/bin/setfacl --restore=-', + 'does not pass incompatible -P option to older versions'); +}; + +done_testing(); From 4601f268a08b28c56131e6a5bf3709fbdbb6ccfc Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Mon, 13 Jul 2026 15:07:49 +0200 Subject: [PATCH 3/6] Fix XML-RPC handling of undefined and empty values MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⓘ Normalize undefined and empty XML-RPC values during encoding and parsing, preventing uninitialized-value warnings and uncontrolled error-log growth. https://github.com/webmin/webmin/issues/2793 --- xmlrpc-lib.pl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/xmlrpc-lib.pl b/xmlrpc-lib.pl index 90c089506..02a37963f 100644 --- a/xmlrpc-lib.pl +++ b/xmlrpc-lib.pl @@ -18,7 +18,7 @@ my ($base64) = &find_xmls("base64", $value, 1); my ($struct) = &find_xmls("struct", $value, 1); my ($array) = &find_xmls("array", $value, 1); if ($scalar) { - return $scalar->[1]->[2]; + return $scalar->[1]->[2] // ""; } elsif ($date) { # Need to decode date @@ -26,7 +26,7 @@ elsif ($date) { } elsif ($base64) { # Convert to binary - return &decode_base64($base64->[1]->[2]); + return &decode_base64($base64->[1]->[2] // ""); } elsif ($struct) { # Parse member names and values @@ -35,7 +35,7 @@ elsif ($struct) { my ($name) = &find_xmls("name", $member, 1); my ($value) = &find_xmls("value", $member, 1); my $perlv = &parse_xml_value($value); - $rv{$name->[1]->[2]} = $perlv; + $rv{$name->[1]->[2] // ""} = $perlv; } return \%rv; } @@ -51,7 +51,7 @@ elsif ($array) { } else { # Fallback - just a string directly in the value - return $value->[1]->[2]; + return $value->[1]->[2] // ""; } } @@ -60,6 +60,7 @@ else { sub encode_xml_value { my ($perlv) = @_; +$perlv = "" if (!defined($perlv)); if (ref($perlv) eq "ARRAY") { # Convert to array XML format my $xmlrv = "\n\n"; From 64f67e63d5b27c35b0258e478f0ff38c7fa3cb31 Mon Sep 17 00:00:00 2001 From: Ilia Ross Date: Mon, 13 Jul 2026 15:08:13 +0200 Subject: [PATCH 4/6] Add regression coverage for warning-free empty values https://github.com/webmin/webmin/issues/2793 --- t/xmlrpc.t | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/t/xmlrpc.t b/t/xmlrpc.t index 8ec2e3d55..c01d04b9e 100644 --- a/t/xmlrpc.t +++ b/t/xmlrpc.t @@ -110,6 +110,14 @@ subtest 'encode_xml_value type selection' => sub { # Plain strings. like(encode_xml_value('hello'), qr{^hello\s*$}, 'word -> string'); like(encode_xml_value(''), qr{^\s*$}, 'empty string -> empty '); + my @warnings; + my $undef_xml; + { + local $SIG{__WARN__} = sub { push(@warnings, @_); }; + $undef_xml = encode_xml_value(undef); + } + like($undef_xml, qr{^\s*$}, 'undef -> empty '); + is_deeply(\@warnings, [], 'undef emits no warnings'); # A value with control characters cannot live in a (the # printable-range regex fails), so it must fall through to base64. @@ -189,6 +197,19 @@ subtest 'parse_xml_value' => sub { is(parse_xml_value(value_tree('1')),1, 'boolean parsed'); is(parse_xml_value(value_tree('2.5')),'2.5', 'double parsed'); is(parse_xml_value(value_tree('hi')), 'hi', 'string parsed'); + my @warnings; + { + local $SIG{__WARN__} = sub { push(@warnings, @_); }; + is(parse_xml_value(value_tree('')), '', + 'empty string parsed'); + is(parse_xml_value(value_tree('')), '', 'implicit empty string parsed'); + is(parse_xml_value(value_tree('')), '', + 'empty base64 parsed'); + my $empty_name = parse_xml_value(value_tree( + 'x')); + is_deeply($empty_name, { '' => 'x' }, 'empty struct member name parsed'); + } + is_deeply(\@warnings, [], 'empty XML values emit no warnings'); # base64 is decoded back to its raw bytes. my $b64 = encode_base64("ab\x00cd"); From 9e7cdc801c7754fa023ec5d9ad84d14af34e73dd Mon Sep 17 00:00:00 2001 From: Jamie Cameron Date: Mon, 13 Jul 2026 14:20:14 -0700 Subject: [PATCH 5/6] Use formal params --- web-lib-funcs.pl | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/web-lib-funcs.pl b/web-lib-funcs.pl index 953a39e13..6fbdaabb5 100755 --- a/web-lib-funcs.pl +++ b/web-lib-funcs.pl @@ -10120,16 +10120,17 @@ out progress of an HTTP request. =cut sub progress_callback { +my ($mode, $arg) = @_; if (defined(&theme_progress_callback)) { # Call the theme override return &theme_progress_callback(@_); } -if ($_[0] == 2) { +if ($mode == 2) { # Got size print $progress_callback_prefix; - if ($_[1]) { - $progress_size = $_[1]; - $progress_step = int($_[1] / 10); + if ($arg) { + $progress_size = $arg; + $progress_step = int($arg / 10); print &text('progress_size2', &html_escape($progress_callback_url), &nice_size($progress_size)),"
\n"; @@ -10141,42 +10142,42 @@ if ($_[0] == 2) { } $last_progress_time = $last_progress_size = undef; } -elsif ($_[0] == 3) { +elsif ($mode == 3) { # Got data update my $sp = $progress_callback_prefix.(" " x 5); if ($progress_size) { # And we have a size to compare against - my $st = int(($_[1] * 10) / $progress_size); + my $st = int(($arg * 10) / $progress_size); my $time_now = time(); if ($st != $progress_step || $time_now - $last_progress_time > 60) { # Show progress every 10% or 60 seconds - print $sp,&text('progress_datan', &nice_size($_[1]), - int($_[1]*100/$progress_size)),"
\n"; + print $sp,&text('progress_datan', &nice_size($arg), + int($arg*100/$progress_size)),"
\n"; $last_progress_time = $time_now; } $progress_step = $st; } else { # No total size .. so only show in 1M jumps - if ($_[1] > $last_progress_size+1024*1024) { + if ($arg > $last_progress_size+1024*1024) { print $sp,&text('progress_data2n', - &nice_size($_[1])),"
\n"; - $last_progress_size = $_[1]; + &nice_size($arg)),"
\n"; + $last_progress_size = $arg; } } } -elsif ($_[0] == 4) { +elsif ($mode == 4) { # All done downloading print $progress_callback_prefix,&text('progress_done'),"
\n"; } -elsif ($_[0] == 5) { +elsif ($mode == 5) { # Got new location after redirect - $progress_callback_url = $_[1]; + $progress_callback_url = $arg; } -elsif ($_[0] == 6) { +elsif ($mode == 6) { # URL is in cache - $progress_callback_url = $_[1]; + $progress_callback_url = $arg; print &text('progress_incache', &html_escape($progress_callback_url)),"
\n"; } From 70011c11c0f5b96b887f43ec5e29cd360f39c0d6 Mon Sep 17 00:00:00 2001 From: Jamie Cameron Date: Mon, 13 Jul 2026 14:41:52 -0700 Subject: [PATCH 6/6] Keep track of the remote IP for HTTP connections, and make it available to the callback function --- web-lib-funcs.pl | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/web-lib-funcs.pl b/web-lib-funcs.pl index 6fbdaabb5..ba4a1e5d0 100755 --- a/web-lib-funcs.pl +++ b/web-lib-funcs.pl @@ -3223,6 +3223,23 @@ Downloads data from a HTTP url to a local file or string. The parameters are : =item response_headers - If set returns a hash ref of response HTTP headers. +If the callback function is defined, it will be called at each step of the download process with a mode parameter and +additional args. The mode will be one of : + +=item 1 - Completed reading headers, arg is 1 for a redirect, 0 otherwise + +=item 2 - Received content length, arg is the response size in bytes + +=item 3 - Received data, arg is the bytes returned so far + +=item 4 - Finished receiving data + +=item 5 - Following a redirect, arg is the new URL + +=item 6 - Data was found in cache, arg is the full cached URL + +=item 7 - TCP connection made, arg is the remote IP (if known) + =cut sub http_download { @@ -3291,6 +3308,7 @@ if (!ref($h)) { if ($error) { $$error = $h; return; } else { &error(&html_escape($h)); } } +&$cbfunc(7, $h->{'ip'}) if ($cbfunc); &complete_http_download($h, $dest, $error, $cbfunc, $osdn, $host, $port, $headers, $ssl, $nocache, $timeout, $response_headers); if ((!$error || !$$error) && !$nocache) { @@ -3883,6 +3901,8 @@ parameters are : =item bindip - Local IP address to bind to for outgoing connections +Returns the IP to which the connection was actually made. + =cut sub open_socket { @@ -3899,13 +3919,14 @@ my @ips = &to_ipaddress($host); push(@ips, &to_ip6address($host)); if (!@ips) { my $msg = "Failed to lookup IP address for $host"; - if ($err) { $$err = $msg; return 0; } + if ($err) { $$err = $msg; return undef; } else { &error($msg); } } # Try each of the resolved IPs my $msg; my $proto = getprotobyname("tcp"); +my $gotip; foreach my $ip (@ips) { $msg = undef; if (&check_ipaddress($ip)) { @@ -3943,11 +3964,12 @@ foreach my $ip (@ips) { next; } } + $gotip = $ip; last; # If we got this far, it worked } if ($msg) { # Last attempt failed - if ($err) { $$err = $msg; return 0; } + if ($err) { $$err = $msg; return undef; } else { &error($msg); } } @@ -3955,7 +3977,7 @@ if ($msg) { my $old = select($fh); $| = 1; select($old); -return 1; +return $gotip; } =head2 download_timeout @@ -9782,8 +9804,9 @@ if ($ssl) { if (!$connected) { # Direct connection my $error; - &open_socket($host, $port, $rv->{'fh'}, \$error, $bindip); + my $ip = &open_socket($host, $port, $rv->{'fh'}, \$error, $bindip); return $error if ($error); + $rv->{'ip'} = $ip; } Net::SSLeay::set_fd($rv->{'ssl_con'}, fileno($rv->{'fh'})); eval { @@ -9835,9 +9858,10 @@ else { if (!$connected) { # Connecting directly my $error; - &open_socket($host, $port, $rv->{'fh'}, \$error, $bindip); + my $ip = &open_socket($host, $port, $rv->{'fh'}, \$error, $bindip); return $error if ($error); my $fh = $rv->{'fh'}; + $rv->{'ip'} = $ip; my $rtxt = "$method $page HTTP/1.0\r\n".$htxt; print $fh $rtxt; }