Protect dkim in MIME Authentication-Results header

Like: Authentication-Results: ORIGINATING;
6acc7b28b7
This commit is contained in:
the-djmaze 2026-11-01 15:32:17 +01:00
parent 929ce7bfd7
commit af7848cbeb
3 changed files with 35 additions and 2 deletions

View file

@ -291,6 +291,7 @@ class Message implements \JsonSerializable
}
$aAuth = $oHeaders->AuthStatuses();
$aAuth = $oHeaders->AuthStatuses(\RainLoop\Api::Config()->Get('security', 'trusted_authserv_ids', ''));
$oMessage->SPF = $aAuth['spf'];
$oMessage->DKIM = $aAuth['dkim'];
$oMessage->DMARC = $aAuth['dmarc'];

View file

@ -180,7 +180,7 @@ class HeaderCollection extends \MailSo\Base\Collection
* spf=pass (ORIGINATING: domain of "snappymail@domain.tld" designates 0.0.0.0 as permitted sender) smtp.mailfrom="snappymail@domain.tld";
* dmarc=fail reason="SPF not aligned (relaxed), DKIM not aligned (relaxed)" header.from=domain.tld (policy=none)
*/
public function AuthStatuses(): array
public function AuthStatuses(string $sTrustedIds = ''): array
{
$aResult = [
'dkim' => [],
@ -188,6 +188,27 @@ class HeaderCollection extends \MailSo\Base\Collection
'spf' => []
];
$aHeaders = $this->ValuesByName(Enumerations\Header::AUTHENTICATION_RESULTS);
/**
* A sender can add this header itself, so a result is only worth reading when the
* server that wrote it handled the message. $sTrustedIds names those servers by
* authserv-id; with none given, the authserv-id of the topmost header is taken as
* the local one, which keeps the separate headers an opendkim and an opendmarc
* each add. See RFC 8601 section 7.1.
*/
$aTrusted = \array_filter(\preg_split('/[\\s,;]+/', \strtolower(\trim($sTrustedIds))));
$bConfigured = !!\count($aTrusted);
if (\count($aHeaders)) {
// The authserv-id is the first field, before the first ";". A version number
// may follow it, and a comment may sit anywhere.
$fAuthservId = function (string $sValue) : string {
$sId = \preg_replace('/\\([^)]*\\)/', ' ', \explode(';', $sValue, 2)[0]);
return \strtolower(\trim(\preg_replace('/\\s+\\d+$/', '', \trim($sId)), " \t\""));
};
$aTrusted = $aTrusted ?: array($fAuthservId($aHeaders[0]));
$aHeaders = \array_filter($aHeaders, fn($sValue) => \in_array($fAuthservId($sValue), $aTrusted, true));
}
if (\count($aHeaders)) {
$aHeaders = \implode(';', $aHeaders);
$aHeaders = \preg_replace('/[\\r\\n\\t\\s]+/', ' ', $aHeaders);
@ -204,8 +225,10 @@ class HeaderCollection extends \MailSo\Base\Collection
}
}
}
if (!\count($aResult['dkim'])) {
if (!\count($aResult['dkim']) && !$bConfigured) {
// X-DKIM-Authentication-Results: signer="hostinger.com" status="pass"
// This one carries no authserv-id, so there is nothing to tie it to: only the
// topmost is read, and an install that names its trusted ids skips it.
$aHeaders = $this->ValuesByName(Enumerations\Header::X_DKIM_AUTHENTICATION_RESULTS);
foreach ($aHeaders as $sHeaderValue) {
$aStatus = array();
@ -216,6 +239,7 @@ class HeaderCollection extends \MailSo\Base\Collection
) {
$aResult['dkim'][] = array($aStatus[1], \trim($aHeader[1]), $sHeaderValue);
}
break;
}
}

View file

@ -226,6 +226,14 @@ Warning: only enable when server does not do this, else double compression error
'openpgp' => array(true),
'auto_verify_signatures' => array(false),
'trusted_authserv_ids' => array('', 'Which servers\' DKIM, SPF and DMARC results to believe,
by authserv-id (the first field of an Authentication-Results header), separated by
spaces or commas. A sender can add that header itself, so a result not tied to a
server that handled the message says nothing.
Empty: the authserv-id of the topmost header is taken as the local one. That covers
the usual case, but a server which adds no results of its own leaves the sender\'s
header as the topmost, so name your own id here if these results are shown.'),
'allow_admin_panel' => array(true, 'Access settings'),
'admin_login' => array('admin', 'Login and password for web admin panel'),
'admin_password' => array(''),