From 241abfe71951e27b301b134ca5a954ba6f40d12e Mon Sep 17 00:00:00 2001 From: Joe Cooper Date: Sun, 10 May 2026 01:19:01 -0500 Subject: [PATCH] Add trusted_proxies config --- miniserv.pl | 23 ++++++++++++++++++++--- webmin/change_access.cgi | 10 ++++++++++ webmin/edit_access.cgi | 4 ++++ webmin/help/access_tproxies.html | 25 +++++++++++++++++++++++++ webmin/lang/en | 2 ++ 5 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 webmin/help/access_tproxies.html diff --git a/miniserv.pl b/miniserv.pl index 08dbdf033..fd60f2ed3 100755 --- a/miniserv.pl +++ b/miniserv.pl @@ -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) { diff --git a/webmin/change_access.cgi b/webmin/change_access.cgi index b9eac3f19..0ce50ea60 100755 --- a/webmin/change_access.cgi +++ b/webmin/change_access.cgi @@ -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(); diff --git a/webmin/edit_access.cgi b/webmin/edit_access.cgi index c81880cc8..1fe33471d 100755 --- a/webmin/edit_access.cgi +++ b/webmin/edit_access.cgi @@ -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'}, diff --git a/webmin/help/access_tproxies.html b/webmin/help/access_tproxies.html new file mode 100644 index 000000000..afe6562cf --- /dev/null +++ b/webmin/help/access_tproxies.html @@ -0,0 +1,25 @@ +
Trusted proxy addresses
+ +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 X-Forwarded-For, X-Real-IP, X-SSL-Client-DN +and X-SSL-Client-Verify. + +

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

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

+Example: +

+10.0.0.5
+192.168.1.0/24
+2001:DB8::/32
+
+

diff --git a/webmin/lang/en b/webmin/lang/en index 810635d16..9f29358d8 100644 --- a/webmin/lang/en +++ b/webmin/lang/en @@ -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.