Notice: Due to size constraints and loading performance considerations, scripts referenced in blog posts are not attached directly. To request access, please complete the following form: Script Request Form Note: A Google account is required to access the form.
Disclaimer: I do not accept responsibility for any issues arising from scripts being run without adequate understanding. It is the user's responsibility to review and assess any code before execution. More information

Building a Real-Time SPF Record Checker with PHP

Email authentication has become critical in today's digital landscape. Sometimes you need to look up an SPF record and determine, based on that record and all the IPv4 addresses, IPv6 addresses, and include records, whether a specific IP address is authorized to send mail for a domain. This verification process is essential for email security, deliverability, and compliance.

Visual Walkthrough

Lets take a look at the visual walkthrough of the website, this shows the main interface:


If we then enter a domain and IP address and click Check SPF record you will get the results underneath the search box:


This shows that the IP address is valid:



This this shows that the IP is not authorised to send mail from that domain as per the SPF record:


It then also has a query log to see what has been requested from the website as below, this can be downloaded as a log file or CSV file.

Core Functionality: DNS Lookup and SPF Parsing

public function lookupSPFRecord($domain) {
    // Get TXT records for the domain
    $txtRecords = dns_get_record($domain, DNS_TXT);
    
    if (!$txtRecords) {
        return null;
    }
    
    // Find SPF record
    foreach ($txtRecords as $record) {
        if (isset($record['txt']) && strpos($record['txt'], 'v=spf1') === 0) {
            return $record['txt'];
        }
    }
    
    return null;
}

This function performs actual DNS queries and locates the SPF record among the domain's TXT records by looking for the standard v=spf1 prefix.

SPF Mechanism Processing

Once the SPF record is retrieved, the script needs to parse and evaluate various SPF mechanisms. The core validation happens in the checkIPInSPF() method:

public function checkIPInSPF($spfRecord, $ipAddress) {
    $mechanisms = explode(' ', $spfRecord);
    
    foreach ($mechanisms as $mechanism) {
        $mechanism = trim($mechanism);
        
        // Check ip4 mechanisms
        if (strpos($mechanism, 'ip4:') === 0) {
            $spfIP = substr($mechanism, 4);
            
            if (strpos($spfIP, '/') !== false) {
                // CIDR notation
                if ($this->isIPInCIDR($ipAddress, $spfIP)) {
                    return true;
                }
            } else {
                // Exact match
                if ($spfIP === $ipAddress) {
                    return true;
                }
            }
        }
        // ... additional mechanism checks
    }
    
    return false;
}

The script handles several types of SPF mechanisms:

  • ip4/ip6: Direct IP address matches and CIDR ranges
  • include: Recursive SPF lookups on other domains
  • mx: Mail exchanger record validation
  • a: A record validation
  • all: Default policy handling

CIDR Range Calculation

One of the more complex aspects is properly handling CIDR notation for IP ranges. The script includes a dedicated method for this:

private function isIPInCIDR($ip, $cidr) {
    list($network, $prefixLength) = explode('/', $cidr);
    
    $ipLong = ip2long($ip);
    $networkLong = ip2long($network);
    $mask = -1 << (32 - (int)$prefixLength);
    
    return ($ipLong & $mask) == ($networkLong & $mask);
}

This ensures that IP ranges like 192.168.1.0/24 are properly evaluated against the IP address being checked.

Recursive Include Processing

SPF records often reference other domains through include: mechanisms. The script handles these recursively:

// Check include mechanisms (simplified - would need recursive lookup)
if (strpos($mechanism, 'include:') === 0) {
    $includeDomain = substr($mechanism, 8);
    $includeRecord = $this->lookupSPFRecord($includeDomain);
    if ($includeRecord && $this->checkIPInSPF($includeRecord, $ipAddress)) {
        return true;
    }
}

This allows the checker to follow SPF includes like include:_spf.google.com and validate against the referenced domain's SPF policy.

Preventing Abuse with reCAPTCHA v3 Integration

A publicly accessible SPF checker tool can quickly become a target for automated abuse - bots attempting to overwhelm the DNS lookup functionality or use the service for malicious reconnaissance. To address this, I integrated Google's reCAPTCHA v3 to provide invisible protection without disrupting the user experience.

reCAPTCHA Server-Side Verification

The verification process happens on the server side to ensure security. Here's the core verification method:

public function verifyRecaptcha($recaptchaResponse) {
    if (empty($recaptchaResponse)) {
        return false;
    }
    
    $url = 'https://www.google.com/recaptcha/api/siteverify';
    $data = array(
        'secret' => RECAPTCHA_SECRET_KEY,
        'response' => $recaptchaResponse,
        'remoteip' => isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : ''
    );
    
    $context = stream_context_create(array(
        'http' => array(
            'header' => "Content-type: application/x-www-form-urlencoded\r\n",
            'method' => 'POST',
            'content' => http_build_query($data)
        )
    ));
    
    $result = file_get_contents($url, false, $context);
    $resultJson = json_decode($result, true);
    
    // Check both success and score threshold
    if (isset($resultJson['success']) && $resultJson['success'] === true) {
        $score = isset($resultJson['score']) ? $resultJson['score'] : 0;
        return $score >= 0.5; // Adjustable threshold
    }
    
    return false;
}

The verification checks both the success status and the risk score. I set the threshold at 0.5, which blocks obvious bots while allowing legitimate users through. This threshold can be adjusted based on the level of protection needed.

reCAPTCHA Client-Side Integration

The JavaScript integration is designed to be completely transparent to the user:

document.addEventListener('DOMContentLoaded', function() {
    const form = document.getElementById('spfForm');
    
    form.addEventListener('submit', function(e) {
        e.preventDefault();
        
        if (typeof grecaptcha === 'undefined') {
            alert('Security verification is loading. Please wait a moment and try again.');
            return;
        }
        
        grecaptcha.ready(function() {
            grecaptcha.execute('SITE_KEY', {action: 'spf_check'}).then(function(token) {
                // Add token to form and submit
                const tokenInput = document.createElement('input');
                tokenInput.type = 'hidden';
                tokenInput.name = 'g-recaptcha-response';
                tokenInput.value = token;
                form.appendChild(tokenInput);
                
                form.submit();
            });
        });
    });
});

The system intercepts form submissions, generates a reCAPTCHA token, and includes it with the request. Users experience no delays or interruptions - the form simply submits as expected.

reCAPTCHA Form Validation Integration

The reCAPTCHA verification is integrated into the main form validation logic:

if ($_POST) {
    $domain = trim(isset($_POST['domain']) ? $_POST['domain'] : '');
    $ipAddress = trim(isset($_POST['ip_address']) ? $_POST['ip_address'] : '');
    $recaptchaResponse = isset($_POST['g-recaptcha-response']) ? $_POST['g-recaptcha-response'] : '';
    
    if (empty($domain) || empty($ipAddress)) {
        $error = 'Please provide both domain name and IP address.';
    } elseif (!filter_var($ipAddress, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
        $error = 'Please enter a valid IPv4 address.';
    } elseif (empty($recaptchaResponse)) {
        $error = 'Security verification failed. Please try again.';
    } else {
        $checker = new SPFChecker();
        
        if (!$checker->verifyRecaptcha($recaptchaResponse)) {
            $error = 'Security verification failed. Please refresh the page and try again.';
        } else {
            // Proceed with SPF checking...
        }
    }
}

Failed reCAPTCHA verification is treated the same as other validation errors, providing clear feedback.

reCAPTCHA Logging Security Events

The logging system also captures security-related events. Failed reCAPTCHA attempts are logged alongside DNS errors, providing visibility into potential abuse attempts:

// Log security failures
if (!$checker->verifyRecaptcha($recaptchaResponse)) {
    $checker->logQuery($domain, $ipAddress, 'security_failed', 'reCAPTCHA verification failed');
}

This creates an audit trail of both legitimate usage and blocked attempts, helping identify patterns in potential abuse.

reCAPTCHA Configuration and Monitoring

The reCAPTCHA keys are configured at the top of the script:

define('RECAPTCHA_SITE_KEY', 'your_site_key_here');
define('RECAPTCHA_SECRET_KEY', 'your_secret_key_here');

The score threshold can be adjusted based on observed traffic patterns. A lower threshold (0.3) allows more traffic through but may permit some automated requests, while a higher threshold (0.7) provides stricter protection but may occasionally block legitimate users.

Global Logging System

Finally, I needed a log to capture every request to the website lookup and the result of that request - either fail or pass. The logging system captures detailed information about each query:

public function logQuery($domain, $ipAddress, $result, $spfRecord = null) {
    $logFile = 'spf_queries.log';
    $timestamp = date('Y-m-d H:i:s');
    $clientIP = $_SERVER['REMOTE_ADDR'] ?? 'unknown';
    $userAgent = $_SERVER['HTTP_USER_AGENT'] ?? 'unknown';
    
    $logEntry = [
        'timestamp' => $timestamp,
        'client_ip' => $clientIP,
        'domain' => $domain,
        'checked_ip' => $ipAddress,
        'result' => $result,
        'spf_record' => $spfRecord ? str_replace('"', '""', $spfRecord) : 'N/A',
        'user_agent' => str_replace('"', '""', $userAgent)
    ];
    
    // Create CSV format
    $csvLine = '"' . implode('","', $logEntry) . '"' . PHP_EOL;
    
    // Write to log file
    file_put_contents($logFile, $csvLine, FILE_APPEND | LOCK_EX);
    
    return true;
}

The logging system provides:

  • Timestamp of each query
  • Client IP address for audit trails
  • Domain and IP checked
  • Result (pass, fail, no_record, error)
  • Complete SPF record for reference
  • User agent information

Log Viewing and Export

The script includes a web-based log viewer accessible through a "View Query Log" button. Users can:

  • View recent queries in a formatted table
  • Download logs as CSV files for analysis
  • Export raw log files for external processing
// Handle log viewing
if (isset($_GET['action']) && $_GET['action'] === 'view_log') {
    $checker = new SPFChecker();
    $logEntries = $checker->getLogEntries(200); // Get last 200 entries
    
    // Display formatted table of log entries
    // ... HTML output code
}

Conclusion

Building a server-side SPF checker solved the fundamental limitations of browser-based DNS lookups while providing comprehensive logging and analysis capabilities. The script accurately validates IP addresses against SPF records, handles complex mechanisms like includes and CIDR ranges, and maintains detailed logs of all queries.

Previous Post Next Post

نموذج الاتصال