diff --git a/CHANGELOG.md b/CHANGELOG.md index d4d7af770..28b885c25 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,8 @@ ## Changelog +#### 2.671 (October, 2026) +* Fix two-factor authentication validation to use the Webmin configuration and data directories passed by the caller +* Fix HTML escaping of module, script and session fields on the Webmin Actions Log details page + #### 2.670 (September 20, 2026) * Add new Hardware Information module for inspecting system, firmware, security, PCI, USB, storage, network, processor, sensor, driver, and kernel module details * Add options to send Webmin and Usermin errors to the systemd journal [forum.virtualmin.com/t/136562](https://forum.virtualmin.com/t/miniserv-webserver-log-growing-too-big-should-be-rotated/136562) @@ -20,6 +24,13 @@ * Fix ACL operations bypassing configured allowed paths in File Manager * Fix arbitrary file reads in Software Packages module * Fix TLS client certificate verification +* Fix Apache Webserver module to enforce the allowed directory ACL when manually editing `.htaccess` files +* Fix Custom Commands module to properly quote parameter values passed to commands and to reject parameters with multiple values +* Fix DNSSEC NSEC3 zone signing in BIND DNS Server module to use zero additional hash iterations, as recommended by RFC 9276 +* Fix System Logs module to create a new log file only after the log entry passes validation +* Fix file chooser to lift the root directory restriction only when the chroot is at or under the allowed root +* Fix password changes from the login page using a configured password change command to fail unless the command prompts for the old password +* Fix File Manager search results to only include files under the allowed directories * Update Backup Configuration module's destination selector to use the new select-based UI * Update the Authentic theme to the latest version with various improvements: - Add support for HTML signatures in Usermin diff --git a/lang/en b/lang/en index 1308cade4..5b059789c 100644 --- a/lang/en +++ b/lang/en @@ -314,7 +314,7 @@ password_enew2=Your new passwords do not match password_epam=PAM error : $1 password_emodpam=The Authen::PAM perl module needed to do password changes is not installed! password_enewpass=New password is not allowed : $1 -password_enotold=Password change commit did not prompt for old password! +password_enotold=Password change command did not prompt for old password! ui_mandatory=This field is mandatory ui_checkmandatory=Nothing was selected diff --git a/systemd/systemd-lib.pl b/systemd/systemd-lib.pl index fd9b3db2d..2ca27a756 100644 --- a/systemd/systemd-lib.pl +++ b/systemd/systemd-lib.pl @@ -585,10 +585,11 @@ my ($name) = @_; return (0, $text{'systemd_ename'}) if (!valid_unit_name($name)); my $out = backquote_logged( "systemctl start ".quotemeta($name)." 2>&1 /dev/null"); } -return (!$?, $out); +return (!$rv, $out); } =head2 stop_unit(name) @@ -661,6 +662,33 @@ my $out = backquote_logged( return (!$?, $out); } +=head2 get_unit_state(name) + +Returns a fresh hash of LoadState, UnitFileState, ActiveState, SubState and +MainPID, followed by an error string. Missing units have LoadState not-found; +command or validation failures return undef and an error. No inventory cache +is used, so callers can verify a change immediately. + +=cut +sub get_unit_state +{ +my ($name) = @_; +return (undef, $text{'systemd_ename'}) if (!valid_unit_name($name)); +my $out = backquote_logged("systemctl show --no-pager ". + "--property=LoadState,UnitFileState,ActiveState,SubState,MainPID ". + quotemeta($name)." 2>&1 {SubState}, 'listening', 'socket readiness is exposed'); + like($commands[-1], qr/--property=LoadState,UnitFileState,ActiveState,SubState,MainPID/, 'only requested properties are read'); + $reply = "LoadState=loaded\nUnitFileState=enabled\nActiveState=inactive\n"; + ($state, $error) = get_unit_state('demo.socket'); + is($state->{ActiveState}, 'inactive', 'state is not cached'); + $reply = "LoadState=not-found\nActiveState=inactive\n"; $status = 1024; + ($state, $error) = get_unit_state('missing.service'); + is($state->{LoadState}, 'not-found', 'missing unit is a valid discovery result'); + $reply = 'Failed to connect to bus'; + ($state, $error) = get_unit_state('demo.socket'); + ok(!defined($state), 'bus failure returns no state'); + like($error, qr/connect to bus/, 'bus error is preserved'); + ($state, $error) = get_unit_state('bad;unit.service'); + is($error, 'bad unit name', 'invalid state lookup is rejected'); +} + { my @cmds; my $reloaded = 0; diff --git a/t/web-lib-funcs-miniserv-config.t b/t/web-lib-funcs-miniserv-config.t new file mode 100644 index 000000000..5c9765334 --- /dev/null +++ b/t/web-lib-funcs-miniserv-config.t @@ -0,0 +1,49 @@ +#!/usr/bin/perl +# Concurrent WebSocket updates must not reuse a same-second config snapshot. +use strict; +use warnings; +use Test::More; +use File::Temp qw(tempdir); +use File::Basename qw(dirname); +use File::Spec; + +require File::Spec->rel2abs(dirname(__FILE__).'/../web-lib-funcs.pl'); +my $directory = tempdir(CLEANUP => 1); +my $file = "$directory/miniserv.conf"; +{ + no warnings qw(redefine once); + *main::get_miniserv_config_file = sub { return $file; }; +} + +# replace_config(contents) simulates another process writing within one second. +sub replace_config +{ +my ($contents) = @_; +open(my $fh, '>', $file) or die $!; +print {$fh} $contents; +close($fh) or die $!; +utime(1700000000, 1700000000, $file) or die $!; +} + +replace_config("port=10000\nwebsockets_/test/ws-555=first\n"); +my %first; +ok(main::get_miniserv_config(\%first), 'read initial configuration'); +is($first{'websockets_/test/ws-555'}, 'first', 'first route is present'); + +# Allocation must retain a route added since the caller's initial config read. +replace_config("port=10000\nwebsockets_/test/ws-555=first\n". + "websockets_/test/ws-556=second\n"); +my %added; +main::get_miniserv_config(\%added); +is($added{'websockets_/test/ws-556'}, 'second', + 'same-second route addition is visible'); + +# Cleanup must not restore a route another backend has already removed. +replace_config("port=10000\nwebsockets_/test/ws-556=second\n"); +my %removed; +main::get_miniserv_config(\%removed); +ok(!exists($removed{'websockets_/test/ws-555'}), + 'same-second route removal is visible'); +is($removed{'websockets_/test/ws-556'}, 'second', + 'unrelated route survives cleanup'); +done_testing(); diff --git a/web-lib-funcs.pl b/web-lib-funcs.pl index e32e3399a..eb63e8e00 100755 --- a/web-lib-funcs.pl +++ b/web-lib-funcs.pl @@ -3083,7 +3083,9 @@ hash reference. =cut sub get_miniserv_config { -return &read_file_cached_with_stat(&get_miniserv_config_file(), $_[0]); +# WebSocket backends can change routes several times within one second. +# An mtime-based cache can lose another process's update even under a file lock. +return &read_file(&get_miniserv_config_file(), $_[0]); } =head2 put_miniserv_config(&hash)