From 326abc074496ce8268b1a86d9fc208bbf59b5870 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 23 Oct 2025 13:27:29 +0700 Subject: [PATCH] Detect SPF all mechanism --- api/openapi.yaml | 5 ++ pkg/analyzer/dns.go | 60 ++++++++++--------- .../lib/components/SpfRecordsDisplay.svelte | 27 +++++++++ 3 files changed, 65 insertions(+), 27 deletions(-) diff --git a/api/openapi.yaml b/api/openapi.yaml index 23cf1b6..8dd1376 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -867,6 +867,11 @@ components: type: boolean description: Whether the SPF record is valid example: true + all_qualifier: + type: string + enum: ["+", "-", "~", "?"] + description: "Qualifier for the 'all' mechanism: + (pass), - (fail), ~ (softfail), ? (neutral)" + example: "~" error: type: string description: Error message if validation failed diff --git a/pkg/analyzer/dns.go b/pkg/analyzer/dns.go index 11a6e17..54b0d2f 100644 --- a/pkg/analyzer/dns.go +++ b/pkg/analyzer/dns.go @@ -245,28 +245,34 @@ func (d *DNSAnalyzer) resolveSPFRecords(domain string, visited map[string]bool, // Basic validation valid := d.validateSPF(spfRecord) - // Check for strict -all mechanism + // Extract the "all" mechanism qualifier + var allQualifier *api.SPFRecordAllQualifier var errMsg *string + if !valid { errMsg = api.PtrTo("SPF record appears malformed") - } else if !d.hasSPFStrictFail(spfRecord) { - // Check what mechanism is used - if strings.HasSuffix(spfRecord, " ~all") { - errMsg = api.PtrTo("SPF uses ~all (softfail) instead of -all (hardfail). This weakens email authentication and may reduce deliverability.") - } else if strings.HasSuffix(spfRecord, " +all") || strings.HasSuffix(spfRecord, " ?all") { - errMsg = api.PtrTo("SPF uses permissive 'all' mechanism. This severely weakens email authentication. Use -all for strict policy.") + } else { + // Extract qualifier from the "all" mechanism + if strings.HasSuffix(spfRecord, " -all") { + allQualifier = api.PtrTo(api.SPFRecordAllQualifier("-")) + } else if strings.HasSuffix(spfRecord, " ~all") { + allQualifier = api.PtrTo(api.SPFRecordAllQualifier("~")) + } else if strings.HasSuffix(spfRecord, " +all") { + allQualifier = api.PtrTo(api.SPFRecordAllQualifier("+")) + } else if strings.HasSuffix(spfRecord, " ?all") { + allQualifier = api.PtrTo(api.SPFRecordAllQualifier("?")) } else if strings.HasSuffix(spfRecord, " all") { - errMsg = api.PtrTo("SPF uses neutral 'all' mechanism. Use -all for strict policy to improve deliverability.") - } else { - errMsg = api.PtrTo("SPF record should end with -all for strict policy to improve deliverability and prevent spoofing.") + // Implicit + qualifier (default) + allQualifier = api.PtrTo(api.SPFRecordAllQualifier("+")) } } results = append(results, api.SPFRecord{ - Domain: &domain, - Record: &spfRecord, - Valid: valid, - Error: errMsg, + Domain: &domain, + Record: &spfRecord, + Valid: valid, + AllQualifier: allQualifier, + Error: errMsg, }) // Extract and resolve include: directives @@ -694,23 +700,23 @@ func (d *DNSAnalyzer) CalculateDNSScore(results *api.DNSResults) (int, string) { mainSPF := (*results.SpfRecords)[0] if mainSPF.Valid { // Full points for valid SPF - score += 20 + score += 15 - // Check for strict -all mechanism - if mainSPF.Record != nil && !d.hasSPFStrictFail(*mainSPF.Record) { - // Deduct points for weak SPF policy - if strings.HasSuffix(*mainSPF.Record, " ~all") { + // Deduct points based on the all mechanism qualifier + if mainSPF.AllQualifier != nil { + switch *mainSPF.AllQualifier { + case "-": + // Strict fail - no deduction, this is the recommended policy + score += 5 + case "~": // Softfail - moderate penalty - score -= 5 - } else if strings.HasSuffix(*mainSPF.Record, " +all") || - strings.HasSuffix(*mainSPF.Record, " ?all") || - strings.HasSuffix(*mainSPF.Record, " all") { + case "+", "?": // Pass/neutral - severe penalty - score -= 10 - } else { - // No 'all' mechanism at all - severe penalty - score -= 10 + score -= 5 } + } else { + // No 'all' mechanism qualifier extracted - severe penalty + score -= 5 } } else if mainSPF.Record != nil { // Partial credit if SPF record exists but has issues diff --git a/web/src/lib/components/SpfRecordsDisplay.svelte b/web/src/lib/components/SpfRecordsDisplay.svelte index 172e9f7..e1086f7 100644 --- a/web/src/lib/components/SpfRecordsDisplay.svelte +++ b/web/src/lib/components/SpfRecordsDisplay.svelte @@ -50,6 +50,33 @@ Invalid {/if} + {#if spf.all_qualifier} +
+ All Mechanism Policy: + {#if spf.all_qualifier === '-'} + Strict (-all) + {:else if spf.all_qualifier === '~'} + Softfail (~all) + {:else if spf.all_qualifier === '+'} + Pass (+all) + {:else if spf.all_qualifier === '?'} + Neutral (?all) + {/if} + {#if index === 0} +
+ {#if spf.all_qualifier === '-'} + All unauthorized servers will be rejected. This is the recommended strict policy. + {:else if spf.all_qualifier === '~'} + Unauthorized servers will softfail. Consider using -all for stricter policy, though this rarely affects legitimate email deliverability. + {:else if spf.all_qualifier === '+'} + All servers are allowed to send email. This severely weakens email authentication. Use -all for strict policy. + {:else if spf.all_qualifier === '?'} + No statement about unauthorized servers. Use -all for strict policy to prevent spoofing. + {/if} +
+ {/if} +
+ {/if} {#if spf.record}
Record: