Check SPF include
This commit is contained in:
parent
a64b866cfa
commit
f6a1ea73a2
3 changed files with 160 additions and 46 deletions
|
|
@ -788,8 +788,11 @@ components:
|
|||
items:
|
||||
$ref: '#/components/schemas/MXRecord'
|
||||
description: MX records for the domain
|
||||
spf_record:
|
||||
$ref: '#/components/schemas/SPFRecord'
|
||||
spf_records:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/SPFRecord'
|
||||
description: SPF records found (includes resolved include directives)
|
||||
dkim_records:
|
||||
type: array
|
||||
items:
|
||||
|
|
@ -835,6 +838,10 @@ components:
|
|||
required:
|
||||
- valid
|
||||
properties:
|
||||
domain:
|
||||
type: string
|
||||
description: Domain this SPF record belongs to
|
||||
example: "example.com"
|
||||
record:
|
||||
type: string
|
||||
description: SPF record content
|
||||
|
|
|
|||
|
|
@ -68,8 +68,8 @@ func (d *DNSAnalyzer) AnalyzeDNS(email *EmailMessage, authResults *api.Authentic
|
|||
// Check MX records
|
||||
results.MxRecords = d.checkMXRecords(domain)
|
||||
|
||||
// Check SPF record
|
||||
results.SpfRecord = d.checkSPFRecord(domain)
|
||||
// Check SPF records (including includes)
|
||||
results.SpfRecords = d.checkSPFRecords(domain)
|
||||
|
||||
// Check DKIM records (from authentication results)
|
||||
if authResults != nil && authResults.Dkim != nil {
|
||||
|
|
@ -142,16 +142,43 @@ func (d *DNSAnalyzer) checkMXRecords(domain string) *[]api.MXRecord {
|
|||
return &results
|
||||
}
|
||||
|
||||
// checkSPFRecord looks up and validates SPF record for a domain
|
||||
func (d *DNSAnalyzer) checkSPFRecord(domain string) *api.SPFRecord {
|
||||
// checkSPFRecords looks up and validates SPF records for a domain, including resolving include: directives
|
||||
func (d *DNSAnalyzer) checkSPFRecords(domain string) *[]api.SPFRecord {
|
||||
visited := make(map[string]bool)
|
||||
return d.resolveSPFRecords(domain, visited, 0)
|
||||
}
|
||||
|
||||
// resolveSPFRecords recursively resolves SPF records including include: directives
|
||||
func (d *DNSAnalyzer) resolveSPFRecords(domain string, visited map[string]bool, depth int) *[]api.SPFRecord {
|
||||
const maxDepth = 10 // Prevent infinite recursion
|
||||
|
||||
if depth > maxDepth {
|
||||
return &[]api.SPFRecord{
|
||||
{
|
||||
Domain: &domain,
|
||||
Valid: false,
|
||||
Error: api.PtrTo("Maximum SPF include depth exceeded"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent circular references
|
||||
if visited[domain] {
|
||||
return &[]api.SPFRecord{}
|
||||
}
|
||||
visited[domain] = true
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), d.Timeout)
|
||||
defer cancel()
|
||||
|
||||
txtRecords, err := d.resolver.LookupTXT(ctx, domain)
|
||||
if err != nil {
|
||||
return &api.SPFRecord{
|
||||
Valid: false,
|
||||
Error: api.PtrTo(fmt.Sprintf("Failed to lookup TXT records: %v", err)),
|
||||
return &[]api.SPFRecord{
|
||||
{
|
||||
Domain: &domain,
|
||||
Valid: false,
|
||||
Error: api.PtrTo(fmt.Sprintf("Failed to lookup TXT records: %v", err)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -166,33 +193,77 @@ func (d *DNSAnalyzer) checkSPFRecord(domain string) *api.SPFRecord {
|
|||
}
|
||||
|
||||
if spfCount == 0 {
|
||||
return &api.SPFRecord{
|
||||
Valid: false,
|
||||
Error: api.PtrTo("No SPF record found"),
|
||||
return &[]api.SPFRecord{
|
||||
{
|
||||
Domain: &domain,
|
||||
Valid: false,
|
||||
Error: api.PtrTo("No SPF record found"),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var results []api.SPFRecord
|
||||
|
||||
if spfCount > 1 {
|
||||
return &api.SPFRecord{
|
||||
results = append(results, api.SPFRecord{
|
||||
Domain: &domain,
|
||||
Record: &spfRecord,
|
||||
Valid: false,
|
||||
Error: api.PtrTo("Multiple SPF records found (RFC violation)"),
|
||||
}
|
||||
})
|
||||
return &results
|
||||
}
|
||||
|
||||
// Basic validation
|
||||
if !d.validateSPF(spfRecord) {
|
||||
return &api.SPFRecord{
|
||||
Record: &spfRecord,
|
||||
Valid: false,
|
||||
Error: api.PtrTo("SPF record appears malformed"),
|
||||
valid := d.validateSPF(spfRecord)
|
||||
|
||||
// Check for strict -all mechanism
|
||||
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 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.")
|
||||
}
|
||||
}
|
||||
|
||||
return &api.SPFRecord{
|
||||
results = append(results, api.SPFRecord{
|
||||
Domain: &domain,
|
||||
Record: &spfRecord,
|
||||
Valid: true,
|
||||
Valid: valid,
|
||||
Error: errMsg,
|
||||
})
|
||||
|
||||
// Extract and resolve include: directives
|
||||
includes := d.extractSPFIncludes(spfRecord)
|
||||
for _, includeDomain := range includes {
|
||||
includedRecords := d.resolveSPFRecords(includeDomain, visited, depth+1)
|
||||
if includedRecords != nil {
|
||||
results = append(results, *includedRecords...)
|
||||
}
|
||||
}
|
||||
|
||||
return &results
|
||||
}
|
||||
|
||||
// extractSPFIncludes extracts all include: domains from an SPF record
|
||||
func (d *DNSAnalyzer) extractSPFIncludes(record string) []string {
|
||||
var includes []string
|
||||
re := regexp.MustCompile(`include:([^\s]+)`)
|
||||
matches := re.FindAllStringSubmatch(record, -1)
|
||||
for _, match := range matches {
|
||||
if len(match) > 1 {
|
||||
includes = append(includes, match[1])
|
||||
}
|
||||
}
|
||||
return includes
|
||||
}
|
||||
|
||||
// validateSPF performs basic SPF record validation
|
||||
|
|
@ -216,6 +287,11 @@ func (d *DNSAnalyzer) validateSPF(record string) bool {
|
|||
return hasValidEnding
|
||||
}
|
||||
|
||||
// hasSPFStrictFail checks if SPF record has strict -all mechanism
|
||||
func (d *DNSAnalyzer) hasSPFStrictFail(record string) bool {
|
||||
return strings.HasSuffix(record, " -all")
|
||||
}
|
||||
|
||||
// checkapi.DKIMRecord looks up and validates DKIM record for a domain and selector
|
||||
func (d *DNSAnalyzer) checkDKIMRecord(domain, selector string) *api.DKIMRecord {
|
||||
// DKIM records are at: selector._domainkey.domain
|
||||
|
|
@ -468,12 +544,32 @@ func (d *DNSAnalyzer) CalculateDNSScore(results *api.DNSResults) (int, string) {
|
|||
}
|
||||
}
|
||||
|
||||
// SPF Record: 20 points
|
||||
// SPF Records: 20 points
|
||||
// SPF is essential for email authentication
|
||||
if results.SpfRecord != nil {
|
||||
if results.SpfRecord.Valid {
|
||||
if results.SpfRecords != nil && len(*results.SpfRecords) > 0 {
|
||||
// Check the main domain's SPF record (first in the list)
|
||||
mainSPF := (*results.SpfRecords)[0]
|
||||
if mainSPF.Valid {
|
||||
// Full points for valid SPF
|
||||
score += 20
|
||||
} else if results.SpfRecord.Record != nil {
|
||||
|
||||
// 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") {
|
||||
// Softfail - moderate penalty
|
||||
score -= 5
|
||||
} else if strings.HasSuffix(*mainSPF.Record, " +all") ||
|
||||
strings.HasSuffix(*mainSPF.Record, " ?all") ||
|
||||
strings.HasSuffix(*mainSPF.Record, " all") {
|
||||
// Pass/neutral - severe penalty
|
||||
score -= 10
|
||||
} else {
|
||||
// No 'all' mechanism at all - severe penalty
|
||||
score -= 10
|
||||
}
|
||||
}
|
||||
} else if mainSPF.Record != nil {
|
||||
// Partial credit if SPF record exists but has issues
|
||||
score += 5
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,35 +88,46 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- SPF Record -->
|
||||
{#if dnsResults.spf_record}
|
||||
<!-- SPF Records -->
|
||||
{#if dnsResults.spf_records && dnsResults.spf_records.length > 0}
|
||||
<div class="mb-4">
|
||||
<h5 class="text-muted mb-2">
|
||||
<span class="badge bg-secondary">SPF</span> Sender Policy Framework
|
||||
</h5>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<div class="mb-2">
|
||||
<strong>Status:</strong>
|
||||
{#if dnsResults.spf_record.valid}
|
||||
<span class="badge bg-success">Valid</span>
|
||||
{:else}
|
||||
<span class="badge bg-danger">Invalid</span>
|
||||
{#each dnsResults.spf_records as spf, index}
|
||||
<div class="card mb-2">
|
||||
<div class="card-body">
|
||||
{#if spf.domain}
|
||||
<div class="mb-2">
|
||||
<strong>Domain:</strong> <code>{spf.domain}</code>
|
||||
{#if index > 0}
|
||||
<span class="badge bg-info ms-2">Included</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mb-2">
|
||||
<strong>Status:</strong>
|
||||
{#if spf.valid}
|
||||
<span class="badge bg-success">Valid</span>
|
||||
{:else}
|
||||
<span class="badge bg-danger">Invalid</span>
|
||||
{/if}
|
||||
</div>
|
||||
{#if spf.record}
|
||||
<div class="mb-2">
|
||||
<strong>Record:</strong><br>
|
||||
<code class="d-block mt-1 text-break">{spf.record}</code>
|
||||
</div>
|
||||
{/if}
|
||||
{#if spf.error}
|
||||
<div class="alert alert-{spf.valid ? 'warning' : 'danger'} mb-0 mt-2">
|
||||
<i class="bi bi-{spf.valid ? 'exclamation-triangle' : 'x-circle'} me-1"></i>
|
||||
<strong>{spf.valid ? 'Warning:' : 'Error:'}</strong> {spf.error}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if dnsResults.spf_record.record}
|
||||
<div class="mb-2">
|
||||
<strong>Record:</strong><br>
|
||||
<code class="d-block mt-1 text-break">{dnsResults.spf_record.record}</code>
|
||||
</div>
|
||||
{/if}
|
||||
{#if dnsResults.spf_record.error}
|
||||
<div class="text-danger">
|
||||
<strong>Error:</strong> {dnsResults.spf_record.error}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue