dzentota / dns-resolver
A secure DNS resolver library that prevents Time-of-Check to Time-of-Use (TOCTOU) and DNS Rebinding attacks following the AppSec Manifesto principles
Requires
- php: ^8.2
- ext-json: *
- psr/log: ^3.0
- psr/simple-cache: ^3.0
Requires (Dev)
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^10.0
- squizlabs/php_codesniffer: ^3.7
This package is auto-updated.
Last update: 2025-07-08 11:32:54 UTC
README
A secure DNS resolver library that prevents Time-of-Check to Time-of-Use (TOCTOU) and DNS Rebinding attacks, following the AppSec Manifesto principles.
Features
- TOCTOU Prevention: Uses immutable value objects to prevent Time-of-Check to Time-of-Use vulnerabilities
- DNS Rebinding Protection: Implements security policies to prevent DNS rebinding attacks
- Immutable Design: All resolved data is stored in read-only value objects
- Security-First: Validates all inputs at construction time (Parse, don't validate)
- Type Safety: Full PHP 8.2+ type declarations with strict mode
- AppSec Manifesto Compliance: Follows all 12 rules of the AppSec Manifesto
Installation
composer require dzentota/dns-resolver
Quick Start
<?php use Dzentota\DnsResolver\SecurityPolicy; use Dzentota\DnsResolver\SecureNativeResolver; // Create a strict security policy $policy = SecurityPolicy::createStrict(); // Create a secure resolver $resolver = new SecureNativeResolver($policy); try { // Resolve a hostname to an IP address $ipAddress = $resolver->resolveToIp('example.com'); echo "Resolved IP: " . $ipAddress->toString() . "\n"; echo "Is Private: " . ($ipAddress->isPrivate() ? 'Yes' : 'No') . "\n"; echo "Version: IPv" . $ipAddress->getVersion() . "\n"; } catch (SecurityException $e) { echo "Security policy violation: " . $e->getMessage() . "\n"; } catch (QueryFailedException $e) { echo "DNS resolution failed: " . $e->getMessage() . "\n"; }
Security Features
1. Time-of-Check to Time-of-Use (TOCTOU) Prevention
The library prevents TOCTOU vulnerabilities by:
- Parsing at the source: Hostnames and IP addresses are validated and normalized at construction time
- Immutable value objects: Once created, hostname and IP address objects cannot be modified
- Atomic resolution: DNS resolution is performed in a single atomic operation
// ❌ VULNERABLE: Traditional approach function traditionalResolve($hostname) { if (isValidHostname($hostname)) { // Check return resolve($hostname); // Use (vulnerable to TOCTOU) } } // ✅ SECURE: Our approach function secureResolve($hostname) { $hostnameObj = new Hostname($hostname); // Parse and validate once return $resolver->resolveToIp($hostnameObj->toString()); // Use immutable value }
2. DNS Rebinding Attack Prevention
DNS rebinding attacks are prevented through:
- Hostname-IP consistency checks: Ensures public hostnames don't resolve to private IPs
- Security policies: Configurable rules for allowed hostname/IP combinations
- TLD validation: Blocks resolution of private TLDs like
.local
,.internal
// Example: Preventing DNS rebinding $policy = new SecurityPolicy([ 'allowPrivateAddresses' => false, // Block RFC 1918 addresses 'allowLoopbackAddresses' => false, // Block 127.0.0.1, ::1 'blockedTlds' => ['local', 'internal'] // Block private TLDs ]); $resolver = new SecureNativeResolver($policy); // This will throw SecurityException if attacker.com resolves to 192.168.1.1 $resolver->resolveToIp('attacker.com');
3. Input Validation and Sanitization
Following the "Parse, don't validate" principle:
// Hostname validation at construction try { $hostname = new Hostname('example.com'); // Hostname is guaranteed to be valid and safe } catch (InvalidHostnameException $e) { // Handle invalid hostname } // IP address validation at construction try { $ip = new IpAddress('192.168.1.1'); // IP is guaranteed to be valid and categorized echo $ip->isPrivate() ? 'Private' : 'Public'; } catch (InvalidIpAddressException $e) { // Handle invalid IP address }
Security Policies
Strict Policy (Recommended for Production)
$policy = SecurityPolicy::createStrict(); // Blocks: private IPs, loopback, multicast, private TLDs, international domains
Permissive Policy (Development)
$policy = SecurityPolicy::createPermissive(); // Allows: most resolution operations, suitable for development
Custom Policy
$policy = new SecurityPolicy([ 'allowPrivateAddresses' => false, 'allowLoopbackAddresses' => false, 'allowMulticastAddresses' => false, 'allowInternationalHostnames' => false, 'blockedTlds' => ['local', 'internal', 'private'], 'allowedTlds' => ['com', 'org', 'net'], // Only allow specific TLDs 'blockedHostnames' => ['malicious.com'], 'allowedHostnames' => ['trusted.com'], // Whitelist approach ]);
Resolver Types
1. SecureNativeResolver
The primary resolver using native PHP DNS functions with security policies:
$resolver = new SecureNativeResolver(SecurityPolicy::createStrict()); $ip = $resolver->resolveToIp('example.com');
2. ConfigResolver
Static configuration-based resolver for testing or overriding specific hostnames:
$config = [ 'api.example.com' => '203.0.113.1', 'db.example.com' => '203.0.113.2', ]; $resolver = new ConfigResolver($config, SecurityPolicy::createStrict()); $ip = $resolver->resolveToIp('api.example.com'); // Returns 203.0.113.1
3. GoogleResolver
DNS-over-HTTPS resolver using Google's public DNS service for enhanced security:
$resolver = new GoogleResolver( GoogleResolver::DEFAULT_OPTIONS, SecurityPolicy::createStrict(), 'https://8.8.4.4/resolve' // Custom DoH endpoint ); $ip = $resolver->resolveToIp('example.com');
4. ChainResolver
Tries multiple resolvers in sequence for fallback and redundancy:
$configResolver = new ConfigResolver(['local.dev' => '127.0.0.1']); $googleResolver = new GoogleResolver(); $nativeResolver = new SecureNativeResolver(); $chainResolver = new ChainResolver( SecurityPolicy::createStrict(), $configResolver, // Try config first $googleResolver, // Then DoH $nativeResolver // Finally native DNS ); $ip = $chainResolver->resolveToIp('example.com');
5. CachedResolver
Wraps any resolver with PSR-16 caching for improved performance:
use Psr\SimpleCache\CacheInterface; $cache = new YourCacheImplementation(); // PSR-16 cache $baseResolver = new GoogleResolver(); $cachedResolver = new CachedResolver( $cache, $baseResolver, 300, // TTL in seconds SecurityPolicy::createStrict(), 'dns_cache_' // Cache key prefix ); $ip = $cachedResolver->resolveToIp('example.com'); // Cached for 5 minutes
Advanced Usage
Combining Resolvers
// Create a production-ready resolver with multiple fallbacks and caching $cache = new YourCacheImplementation(); $configResolver = new ConfigResolver([ 'internal.api' => '10.0.0.1', 'staging.api' => '10.0.0.2', ]); $googleResolver = new GoogleResolver(); $nativeResolver = new SecureNativeResolver(); $chainResolver = new ChainResolver( SecurityPolicy::createStrict(), $configResolver, $googleResolver, $nativeResolver ); $productionResolver = new CachedResolver( $cache, $chainResolver, 600, // 10 minutes cache SecurityPolicy::createStrict() ); // This will: // 1. Check cache first // 2. Try config resolver for internal hostnames // 3. Fall back to Google DoH // 4. Finally try native DNS // 5. Cache successful results $ip = $productionResolver->resolveToIp('api.example.com');
Custom Security Policies per Resolver
// Strict policy for external DNS $externalPolicy = SecurityPolicy::createStrict(); // Permissive policy for internal config $internalPolicy = SecurityPolicy::createPermissive(); $internalResolver = new ConfigResolver($internalConfig, $internalPolicy); $externalResolver = new GoogleResolver([], $externalPolicy); $resolver = new ChainResolver( $externalPolicy, // Chain-level policy (most restrictive) $internalResolver, $externalResolver );
API Reference
Resolver Interface
interface Resolver { public function resolveToIp(string $hostname): IpAddress; public function isHostnameSafe(string $hostname): bool; }
IpAddress Value Object
final readonly class IpAddress { public function __construct(string $address); public function toString(): string; public function isPrivate(): bool; public function isLoopback(): bool; public function isMulticast(): bool; public function getVersion(): int; // 4 or 6 public function isSafeForExternalConnection(): bool; public function equals(IpAddress $other): bool; }
Hostname Value Object
final readonly class Hostname { public function __construct(string $hostname); public function toString(): string; public function getLabels(): array; public function getTld(): string; public function getSld(): string; public function isInternational(): bool; public function isLocalhost(): bool; public function isPrivateTld(): bool; public function isSafeForResolution(): bool; }
AppSec Manifesto Compliance
This library implements all 12 rules of the AppSec Manifesto:
Rule #0: Absolute Zero – Minimizing Attack Surface
- ✅ No unused code paths
- ✅ Minimal input acceptance
- ✅ Strict validation at boundaries
Rule #1: The Lord of the Sinks – Context-Specific Escaping
- ✅ Secure output encoding for IP addresses
- ✅ Context-aware hostname formatting
Rule #2: The Parser's Prerogative (Parse, don't validate)
- ✅ Hostname and IP parsing at construction
- ✅ Always-valid value objects
- ✅ No shotgun parsing
Rule #3: Forget-me-not – Preserving Data Validity
- ✅ Immutable value objects
- ✅ Type safety throughout the application
- ✅ Guaranteed data integrity
Rule #4: Declaration of Sources Rights – Uniform Input Handling
- ✅ Consistent parsing across all sources
- ✅ Uniform security policy application
Rule #5: The Principle of Pruning (Least Privilege)
- ✅ Minimal resolver permissions
- ✅ Restrictive default policies
Rule #6: The Castle Doctrine (Defense in Depth)
- ✅ Multiple security layers
- ✅ Policy-based validation
- ✅ Input and output validation
Rule #7: The Architect's Oracle (Threat Modeling)
- ✅ TOCTOU threat mitigation
- ✅ DNS rebinding prevention
- ✅ Homograph attack protection
Rule #8: The Vigilant Eye (Logging & Monitoring)
- ✅ Security event logging (via PSR-3)
- ✅ Policy violation tracking
Rule #9: The Cipher's Creed (Secure Cryptography)
- ✅ No custom crypto (uses system DNS)
- ✅ Secure defaults
Rule #10: The Gatekeeper's Gambit (Secure Session Management)
- ✅ Immutable session state
- ✅ No session hijacking possible
Rule #11: The Chain's Custodian (Secure Software Supply Chain)
- ✅ Minimal dependencies
- ✅ Type-safe interfaces
Rule #12: The Sentinel's Shield (API Security)
- ✅ Strong input validation
- ✅ Secure defaults
- ✅ Rate limiting ready
Testing
Run the test suite:
composer test
Run with coverage:
composer test-coverage
Static analysis:
composer phpstan
Code style:
composer cs-check composer cs-fix
Examples
Basic Usage
<?php require 'vendor/autoload.php'; use Dzentota\DnsResolver\SecurityPolicy; use Dzentota\DnsResolver\SecureNativeResolver; $policy = SecurityPolicy::createStrict(); $resolver = new SecureNativeResolver($policy); $ip = $resolver->resolveToIp('github.com'); echo "GitHub IP: " . $ip . "\n";
Handling Security Violations
<?php use Dzentota\DnsResolver\SecurityException; use Dzentota\DnsResolver\QueryFailedException; try { $ip = $resolver->resolveToIp('suspicious.local'); } catch (SecurityException $e) { // Hostname violated security policy error_log("Security violation: " . $e->getMessage()); } catch (QueryFailedException $e) { // DNS resolution failed error_log("Resolution failed: " . $e->getMessage()); }
Working with IP Addresses
<?php $ip = new IpAddress('192.168.1.1'); if ($ip->isPrivate()) { echo "This is a private IP address\n"; } if ($ip->getVersion() === 4) { echo "This is an IPv4 address\n"; } if ($ip->isSafeForExternalConnection()) { echo "Safe to connect externally\n"; } else { echo "Internal use only\n"; }
Working with Hostnames
<?php $hostname = new Hostname('example.com'); echo "TLD: " . $hostname->getTld() . "\n"; // com echo "SLD: " . $hostname->getSld() . "\n"; // example echo "Labels: " . implode('.', $hostname->getLabels()) . "\n"; // example.com if ($hostname->isSafeForResolution()) { echo "Safe to resolve\n"; }
Contributing
- Fork the repository
- Create a feature branch
- Add tests for your changes
- Ensure all tests pass
- Submit a pull request
Security
If you discover a security vulnerability, please email webtota@gmail.com. All security vulnerabilities will be promptly addressed.
License
This library is released under the MIT License. See LICENSE for details.
Related Projects
- AppSec Manifesto - The security principles this library follows
- TypedValue - Value object library for type safety
Author
Alex Tatulchenkov - AppSec Leader | Enterprise Web Defender
- Email: webtota@gmail.com
- LinkedIn: View Profile
- GitHub: @dzentota