fix: never count a helo spf pass as verifying the sender

This commit is contained in:
Linus Rath
2026-10-29 04:41:02 +01:00
parent 67cdb4e792
commit 8adef1e13f
2 changed files with 11 additions and 6 deletions
+5 -1
View File
@@ -11,7 +11,7 @@ describe('parseAuthenticationResults', () => {
const r = parseAuthenticationResults( const r = parseAuthenticationResults(
'mx.example; spf=pass smtp.mailfrom=news.example; dkim=pass header.d=news.example header.s=s1; dmarc=pass header.from=news.example policy.dmarc=none; iprev=pass policy.iprev=1.2.3.4', 'mx.example; spf=pass smtp.mailfrom=news.example; dkim=pass header.d=news.example header.s=s1; dmarc=pass header.from=news.example policy.dmarc=none; iprev=pass policy.iprev=1.2.3.4',
); );
expect(r.spf).toEqual({ result: 'pass', domain: 'news.example' }); expect(r.spf).toEqual({ result: 'pass', identity: 'mailfrom', domain: 'news.example' });
expect(r.dkim).toEqual({ result: 'pass', domain: 'news.example', selector: 's1' }); expect(r.dkim).toEqual({ result: 'pass', domain: 'news.example', selector: 's1' });
expect(r.dmarc).toEqual({ result: 'pass', domain: 'news.example', policy: 'none' }); expect(r.dmarc).toEqual({ result: 'pass', domain: 'news.example', policy: 'none' });
expect(r.iprev).toEqual({ result: 'pass', ip: '1.2.3.4' }); expect(r.iprev).toEqual({ result: 'pass', ip: '1.2.3.4' });
@@ -88,6 +88,10 @@ describe('getSenderVerification', () => {
it('does not count a HELO pass', () => { it('does not count a HELO pass', () => {
const auth = parseAuthenticationResults('mx; spf=pass smtp.helo=bank.example; spf=none smtp.mailfrom=bank.example'); const auth = parseAuthenticationResults('mx; spf=pass smtp.helo=bank.example; spf=none smtp.mailfrom=bank.example');
expect(getSenderVerification(auth, 'a@bank.example')?.status).toBe('unverified'); expect(getSenderVerification(auth, 'a@bank.example')?.status).toBe('unverified');
// Nor a HELO pass that is the only SPF result.
const only = parseAuthenticationResults('mx; spf=pass smtp.helo=bank.example; dkim=none; dmarc=none');
expect(hasAlignedAuthenticationPass(only, 'a@bank.example')).toBe(false);
expect(getSenderVerification(only, 'a@bank.example')?.status).toBe('unverified');
}); });
}); });
+6 -5
View File
@@ -45,7 +45,7 @@ export interface DkimEntry {
} }
export interface AuthenticationResults { export interface AuthenticationResults {
spf?: { result: SpfResult; domain?: string; all?: SpfEntry[] }; spf?: { result: SpfResult; identity?: SpfEntry['identity']; domain?: string; all?: SpfEntry[] };
/** The first signature's result; `all` lists every one when there are several. */ /** The first signature's result; `all` lists every one when there are several. */
dkim?: DkimEntry & { all?: DkimEntry[] }; dkim?: DkimEntry & { all?: DkimEntry[] };
dmarc?: { result: DmarcResult; domain?: string; policy?: DmarcPolicy }; dmarc?: { result: DmarcResult; domain?: string; policy?: DmarcPolicy };
@@ -140,10 +140,10 @@ function alignedWith(fromDomain: string, candidate: string | undefined): boolean
* the visible sender. A HELO pass never does. * the visible sender. A HELO pass never does.
*/ */
function hasAlignedSpfPass(auth: AuthenticationResults, fromDomain: string): boolean { function hasAlignedSpfPass(auth: AuthenticationResults, fromDomain: string): boolean {
if (auth.spf?.all) { // With one SPF result there is no `all` list, and its identity is checked
return auth.spf.all.some((entry) => entry.identity !== 'helo' && entry.result === 'pass' && alignedWith(fromDomain, entry.domain)); // too: `spf=pass smtp.helo=<From domain>` alone used to count.
} const entries = auth.spf?.all ?? (auth.spf ? [auth.spf] : []);
return auth.spf?.result === 'pass' && alignedWith(fromDomain, auth.spf.domain); return entries.some((entry) => entry.identity === 'mailfrom' && entry.result === 'pass' && alignedWith(fromDomain, entry.domain));
} }
/** A DKIM pass for a signing domain that aligns with the From domain. */ /** A DKIM pass for a signing domain that aligns with the From domain. */
@@ -381,6 +381,7 @@ export function parseAuthenticationResults(headers: string | readonly string[]):
} }
results.spf = { results.spf = {
result: primary.result, result: primary.result,
...(primary.identity ? { identity: primary.identity } : {}),
domain: primary.domain, domain: primary.domain,
...(spfResults.length > 1 ? { all: spfResults } : {}), ...(spfResults.length > 1 ? { all: spfResults } : {}),
}; };