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

dev-main 2025-07-07 22:42 UTC

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.

License: MIT PHP Version

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

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for your changes
  4. Ensure all tests pass
  5. 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

Author

Alex Tatulchenkov - AppSec Leader | Enterprise Web Defender