proxy-auth: remove check_proxy / proxy_ip settings

The "Check Proxy" option inspected the forwarded client IP returned
by Http::GetClientIp(true), which resolves to HTTP_X_FORWARDED_FOR
first and only falls back to REMOTE_ADDR. In any typical reverse-proxy
deployment that header carries the end-user's IP, not the proxy's, so
the option never actually validated that the request came from the
trusted proxy. It only constrained the set of end-user IPs allowed
to use SSO, which is not a meaningful guarantee on its own.

Drop the option, the paired proxy_ip subnet field, and the
ip_in_range helper. Document that access must be gated at the
network layer (firewall / docker network / bound interface) and
that the upstream proxy must strip any client-supplied value of
the configured remote-user header.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Philipp Mundhenk 2026-05-17 10:44:36 +02:00
parent 68588287ef
commit 863f719da3
2 changed files with 16 additions and 85 deletions

View file

@ -19,10 +19,12 @@ The following steps are require in SnappyMail:
- Master User is dependent on Dovecot config (see below) - Master User is dependent on Dovecot config (see below)
- Master User Password is dependent on Dovecot config (see below) - Master User Password is dependent on Dovecot config (see below)
- Header Name is dependent on authentication solution. This is the header containing the name of currently logged in user. In case of Authelia, this is "Remote-User". - Header Name is dependent on authentication solution. This is the header containing the name of currently logged in user. In case of Authelia, this is "Remote-User".
- Check Proxy: Since this plugin partially bypasses authentication, it is important to only allow this access from well-defined hosts. It is highly recommended to activate this option!
- When checking for reverse proxy, it is required to set the IP filter to either an IP address or a subnet.
- Automatic Login: Automatically logs in the user of user header is present (see below) - Automatic Login: Automatically logs in the user of user header is present (see below)
> **Security note**
>
> This plugin trusts the configured request header as proof of identity. Anyone who can reach the SnappyMail container directly and set that header can log in as any user. You **must** ensure that SnappyMail is only reachable through your reverse proxy / SSO chain (e.g. via the docker network, a firewall, or by binding SnappyMail to a non-public interface) and that the upstream proxy strips any client-supplied value of the header before forwarding. The plugin itself does no source-IP validation — earlier versions had a `check_proxy` option, but it inspected the forwarded client IP (the end user's IP) and therefore did not actually verify that the request came from the proxy. It has been removed; gate access at the network layer instead.
This concludes the setup of SnappyMail. This concludes the setup of SnappyMail.
### Dovecot ### Dovecot

View file

@ -6,7 +6,7 @@ class ProxyAuthPlugin extends \RainLoop\Plugins\AbstractPlugin
NAME = 'Proxy Auth', NAME = 'Proxy Auth',
AUTHOR = 'Philipp', AUTHOR = 'Philipp',
URL = 'https://www.mundhenk.org/', URL = 'https://www.mundhenk.org/',
VERSION = '0.6', VERSION = '0.7',
RELEASE = '2026-05-17', RELEASE = '2026-05-17',
REQUIRED = '2.36.1', REQUIRED = '2.36.1',
CATEGORY = 'Login', CATEGORY = 'Login',
@ -21,26 +21,6 @@ class ProxyAuthPlugin extends \RainLoop\Plugins\AbstractPlugin
$this->addHook('login.credentials', 'MapEmailAddress'); $this->addHook('login.credentials', 'MapEmailAddress');
} }
/* by https://gist.github.com/tott/7684443 */
/**
* Check if a given ip is in a network
* @param string $ip IP to check in IPV4 format eg. 127.0.0.1
* @param string $range IP/CIDR netmask eg. 127.0.0.0/24, also 127.0.0.1 is accepted and /32 assumed
* @return boolean true if the ip is in this range / false if not.
*/
private function ip_in_range( $ip, $range ) {
if ( strpos( $range, '/' ) == false ) {
$range .= '/32';
}
// $range is in IP/CIDR format eg 127.0.0.1/24
list( $range, $netmask ) = explode( '/', $range, 2 );
$range_decimal = ip2long( $range );
$ip_decimal = ip2long( $ip );
$wildcard_decimal = pow( 2, ( 32 - $netmask ) ) - 1;
$netmask_decimal = ~ $wildcard_decimal;
return ( ( $ip_decimal & $netmask_decimal ) == ( $range_decimal & $netmask_decimal ) );
}
public function MapEmailAddress(string &$sEmail, string &$sImapUser, string &$sPassword, string &$sSmtpUser) public function MapEmailAddress(string &$sEmail, string &$sImapUser, string &$sPassword, string &$sSmtpUser)
{ {
$oActions = \RainLoop\Api::Actions(); $oActions = \RainLoop\Api::Actions();
@ -80,59 +60,19 @@ class ProxyAuthPlugin extends \RainLoop\Plugins\AbstractPlugin
$sMsg = "Remote User: " . $sRemoteUser; $sMsg = "Remote User: " . $sRemoteUser;
$oLogger->Write($sMsg, $sLevel, $sPrefix); $oLogger->Write($sMsg, $sLevel, $sPrefix);
$sProxyIP = $this->Config()->getDecrypted('plugin', 'proxy_ip', ''); /* create master user login from remote user header and settings */
$sMsg = "ProxyIP: " . $sProxyIP; $sEmail = $sRemoteUser . $sMasterSeparator . $sMasterUser;
$oLogger->Write($sMsg, $sLevel, $sPrefix); $sPassword = new \SnappyMail\SensitiveString(\trim($this->Config()->getDecrypted('plugin', 'master_password', '')));
$sProxyCheck = (bool) $this->Config()->Get('plugin', 'check_proxy', false); try
$sClientIPs = $this->Manager()->Actions()->Http()->GetClientIP(true); {
static::$login = true;
/* make sure that remote user is only set by authorized proxy to avoid security risks */ $oAccount = $oActions->LoginProcess($sEmail, $sPassword);
if ($sProxyCheck) {
$sProxyRequest = false;
$sMsg = "checking client IPs: " . $sClientIPs;
$oLogger->Write($sMsg, $sLevel, $sPrefix);
$sClientIPs = explode(", ", $sClientIPs);
if (is_array($sClientIPs)) {
foreach ($sClientIPs as &$sIP) {
$sMsg = "checking client IP: " . $sIP;
$oLogger->Write($sMsg, $sLevel, $sPrefix);
if ($this->ip_in_range($sIP, $sProxyIP)) {
$sProxyRequest = true;
}
}
} else {
$sMsg = "checking client IP: " . $sClientIPs;
$oLogger->Write($sMsg, $sLevel, $sPrefix);
if ($this->ip_in_range($sClientIPs, $sProxyIP)) {
$sProxyRequest = true;
}
}
} else {
$sProxyRequest = true;
} }
catch (\Throwable $oException)
if ($sProxyRequest) { {
/* create master user login from remote user header and settings */ $oLogger = $oActions->Logger();
$sEmail = $sRemoteUser . $sMasterSeparator . $sMasterUser; $oLogger && $oLogger->WriteException($oException);
$sPassword = new \SnappyMail\SensitiveString(\trim($this->Config()->getDecrypted('plugin', 'master_password', '')));
try
{
static::$login = true;
$oAccount = $oActions->LoginProcess($sEmail, $sPassword);
}
catch (\Throwable $oException)
{
$oLogger = $oActions->Logger();
$oLogger && $oLogger->WriteException($oException);
}
\MailSo\Base\Http::Location('./');
return true;
} }
\MailSo\Base\Http::Location('./'); \MailSo\Base\Http::Location('./');
@ -188,17 +128,6 @@ class ProxyAuthPlugin extends \RainLoop\Plugins\AbstractPlugin
->SetDescription('Name of header containing username') ->SetDescription('Name of header containing username')
->SetDefaultValue('Remote-User') ->SetDefaultValue('Remote-User')
->SetEncrypted(), ->SetEncrypted(),
\RainLoop\Plugins\Property::NewInstance('check_proxy')
->SetLabel('Check Proxy')
->SetType(\RainLoop\Enumerations\PluginPropertyType::BOOL)
->SetDescription('Activates check if proxy is connecting')
->SetDefaultValue(true),
\RainLoop\Plugins\Property::NewInstance('proxy_ip')
->SetLabel('Proxy IPNet')
->SetType(\RainLoop\Enumerations\PluginPropertyType::STRING_TEXT)
->SetDescription('IP or Subnet of proxy, auth header will only be accepted from this address')
->SetDefaultValue('10.1.0.0/24')
->SetEncrypted(),
\RainLoop\Plugins\Property::NewInstance('auto_login') \RainLoop\Plugins\Property::NewInstance('auto_login')
->SetAllowedInJs(true) ->SetAllowedInJs(true)
->SetLabel('Activate automatic login') ->SetLabel('Activate automatic login')