Reliance on Reverse DNS Resolution for a Security-Critical Action

Description

Reliance on Reverse DNS Resolution for a Security-Critical Action occurs when software performs a security decision based on the result of a reverse DNS lookup (PTR record). Reverse DNS is easily spoofable—attackers who control a DNS server can configure any hostname to resolve to their IP address. This allows attackers to bypass IP-based access controls, forge log entries, or impersonate trusted systems by manipulating reverse DNS records.

Risk

DNS is fundamentally untrusted infrastructure. Attackers can set up PTR records pointing any hostname to their IP addresses. Systems relying on reverse DNS for authentication or authorization are trivially bypassed. Even legitimate reverse DNS can be inconsistent or stale. This vulnerability has been exploited to bypass firewall rules, gain access to restricted services, and poison logs with false attribution. Many legacy systems still use hostname-based trust models that are vulnerable.

Solution

Never use reverse DNS for security decisions. Use IP address-based allowlists directly. Implement proper authentication mechanisms (certificates, API keys, mutual TLS). If hostname logging is needed, treat it as informational only—log both IP and hostname. Use forward-confirmed reverse DNS (FCrDNS) if hostname information is required: verify the reverse lookup by performing a forward lookup and confirming the IP matches. Implement certificate-based authentication for service-to-service communication.

Common Consequences

ImpactDetails
Access ControlScope: Authentication Bypass

Attackers can spoof trusted hostnames to bypass access controls.
IntegrityScope: Log Forgery

Attackers can make malicious actions appear to originate from trusted systems.
Non-RepudiationScope: False Attribution

Security logs become unreliable for forensic analysis.

Example Code + Solution Code

Vulnerable Code

# VULNERABLE: Using reverse DNS for access control
import socket

TRUSTED_HOSTS = ['admin.internal.company.com', 'backup.internal.company.com']

def check_access_vulnerable(client_ip):
    try:
        # Reverse DNS lookup - easily spoofable!
        hostname = socket.gethostbyaddr(client_ip)[0]

        # Attacker can set their PTR record to admin.internal.company.com
        if hostname in TRUSTED_HOSTS:
            return True
        if hostname.endswith('.internal.company.com'):
            return True
    except socket.herror:
        pass

    return False

# VULNERABLE: Logging with reverse DNS as attribution
def log_request_vulnerable(client_ip, action):
    hostname = socket.gethostbyaddr(client_ip)[0]

    # Log can be manipulated with fake hostname
    log_entry = f"{hostname} ({client_ip}) performed {action}"
    logger.info(log_entry)

# VULNERABLE: hosts.allow style checking
def is_allowed_host_vulnerable(client_ip):
    hostname, _, _ = socket.gethostbyaddr(client_ip)

    # Pattern matching on untrusted hostname
    if hostname.endswith('.trusted-partner.com'):
        return True
    return False
// VULNERABLE: Java reverse DNS access control
import java.net.InetAddress;

public class VulnerableAccessControl {

    private static final Set<String> TRUSTED_DOMAINS = Set.of(
        "admin.internal.company.com",
        "backup.internal.company.com"
    );

    public boolean isAllowed(String clientIp) throws Exception {
        InetAddress addr = InetAddress.getByName(clientIp);

        // Reverse DNS - attacker controlled!
        String hostname = addr.getCanonicalHostName();

        // Trust based on hostname - vulnerable!
        if (TRUSTED_DOMAINS.contains(hostname)) {
            return true;
        }

        if (hostname.endsWith(".internal.company.com")) {
            return true;
        }

        return false;
    }

    // VULNERABLE: Servlet filter using hostname
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {

        String hostname = request.getRemoteHost();  // Reverse DNS lookup!

        if (!hostname.endsWith(".company.com")) {
            ((HttpServletResponse) response).sendError(403);
            return;
        }

        chain.doFilter(request, response);
    }
}
// VULNERABLE: Node.js reverse DNS checking
const dns = require('dns');

const TRUSTED_HOSTS = new Set([
    'admin.internal.company.com',
    'monitoring.internal.company.com'
]);

async function checkAccessVulnerable(clientIp) {
    return new Promise((resolve, reject) => {
        // Reverse DNS - attacker can spoof!
        dns.reverse(clientIp, (err, hostnames) => {
            if (err) {
                resolve(false);
                return;
            }

            // Trusting spoofable DNS
            for (const hostname of hostnames) {
                if (TRUSTED_HOSTS.has(hostname)) {
                    resolve(true);
                    return;
                }
                if (hostname.endsWith('.internal.company.com')) {
                    resolve(true);
                    return;
                }
            }
            resolve(false);
        });
    });
}

// VULNERABLE: Express middleware
app.use((req, res, next) => {
    dns.reverse(req.ip, (err, hostnames) => {
        if (err || !hostnames.some(h => h.endsWith('.trusted.com'))) {
            return res.status(403).send('Access denied');
        }
        next();
    });
});

Fixed Code

# SAFE: IP-based access control
import ipaddress
import socket

# Define trusted IPs/networks directly
TRUSTED_IPS = {
    ipaddress.ip_address('10.0.1.5'),
    ipaddress.ip_address('10.0.1.6'),
}

TRUSTED_NETWORKS = [
    ipaddress.ip_network('10.0.0.0/8'),
    ipaddress.ip_network('192.168.1.0/24'),
]

def check_access_safe(client_ip):
    """Check access based on IP address only."""
    try:
        ip = ipaddress.ip_address(client_ip)

        # Check specific IPs
        if ip in TRUSTED_IPS:
            return True

        # Check network ranges
        for network in TRUSTED_NETWORKS:
            if ip in network:
                return True

    except ValueError:
        pass  # Invalid IP

    return False

# SAFE: Forward-Confirmed Reverse DNS (FCrDNS)
def get_verified_hostname(ip_address):
    """Get hostname only if forward lookup confirms IP."""
    try:
        # Step 1: Reverse lookup
        hostname, _, _ = socket.gethostbyaddr(ip_address)

        # Step 2: Forward lookup to verify
        _, _, ip_list = socket.gethostbyname_ex(hostname)

        # Step 3: Confirm original IP is in forward results
        if ip_address in ip_list:
            return hostname  # Verified!
        else:
            return None  # Forward lookup doesn't match

    except (socket.herror, socket.gaierror):
        return None

# SAFE: Logging with informational hostname
def log_request_safe(client_ip, action):
    # Get verified hostname if available
    verified_hostname = get_verified_hostname(client_ip)

    # Log IP as authoritative, hostname as informational
    if verified_hostname:
        log_entry = f"[{client_ip}] (verified: {verified_hostname}) performed {action}"
    else:
        log_entry = f"[{client_ip}] performed {action}"

    logger.info(log_entry)

# SAFE: Using mutual TLS for service authentication
from flask import Flask
import ssl

app = Flask(__name__)

# Client certificate authentication
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ssl_context.load_cert_chain('server.crt', 'server.key')
ssl_context.load_verify_locations('trusted_ca.crt')
ssl_context.verify_mode = ssl.CERT_REQUIRED  # Require client cert

@app.before_request
def verify_client_cert():
    # Get client certificate info
    client_cert = request.environ.get('SSL_CLIENT_CERT')
    if not client_cert:
        abort(403, 'Client certificate required')

    # Verify against known clients
    cert_cn = extract_cn_from_cert(client_cert)
    if cert_cn not in TRUSTED_CLIENTS:
        abort(403, 'Unknown client')
// SAFE: Java IP-based access control
import java.net.InetAddress;
import java.util.Set;

public class SecureAccessControl {

    // IP-based allowlist
    private static final Set<String> TRUSTED_IPS = Set.of(
        "10.0.1.5",
        "10.0.1.6",
        "192.168.1.100"
    );

    // CIDR-based allowlist
    private static final List<IpRange> TRUSTED_NETWORKS = List.of(
        new IpRange("10.0.0.0/8"),
        new IpRange("192.168.0.0/16")
    );

    public boolean isAllowed(String clientIp) {
        // Check specific IPs
        if (TRUSTED_IPS.contains(clientIp)) {
            return true;
        }

        // Check network ranges
        for (IpRange range : TRUSTED_NETWORKS) {
            if (range.contains(clientIp)) {
                return true;
            }
        }

        return false;
    }

    // SAFE: FCrDNS verification
    public String getVerifiedHostname(String ipAddress) {
        try {
            InetAddress addr = InetAddress.getByName(ipAddress);

            // Reverse lookup
            String hostname = addr.getCanonicalHostName();

            // If no reverse, returns IP string
            if (hostname.equals(ipAddress)) {
                return null;
            }

            // Forward lookup verification
            InetAddress[] forwardAddrs = InetAddress.getAllByName(hostname);

            for (InetAddress fwd : forwardAddrs) {
                if (fwd.getHostAddress().equals(ipAddress)) {
                    return hostname;  // Verified!
                }
            }

            return null;  // Forward doesn't confirm reverse

        } catch (Exception e) {
            return null;
        }
    }
}

// SAFE: Servlet filter with IP-based control
@WebFilter("/*")
public class SecureAccessFilter implements Filter {

    private final SecureAccessControl accessControl = new SecureAccessControl();

    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
                         FilterChain chain) throws IOException, ServletException {

        String clientIp = request.getRemoteAddr();

        // Use IP, not hostname!
        if (!accessControl.isAllowed(clientIp)) {
            HttpServletResponse httpResponse = (HttpServletResponse) response;
            httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN);
            return;
        }

        chain.doFilter(request, response);
    }
}

// SAFE: Using client certificates
@Configuration
public class MutualTLSConfig {

    @Bean
    public TomcatServletWebServerFactory servletContainer() {
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();

        factory.addConnectorCustomizers(connector -> {
            connector.setScheme("https");
            connector.setSecure(true);

            Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
            protocol.setSSLEnabled(true);
            protocol.setClientAuth("true");  // Require client cert
            protocol.setKeystoreFile("server.jks");
            protocol.setTruststoreFile("trusted-clients.jks");
        });

        return factory;
    }
}
// SAFE: Node.js IP-based access control
const ipRangeCheck = require('ip-range-check');

const TRUSTED_IPS = new Set([
    '10.0.1.5',
    '10.0.1.6',
    '192.168.1.100'
]);

const TRUSTED_RANGES = [
    '10.0.0.0/8',
    '192.168.0.0/16',
    '172.16.0.0/12'
];

function checkAccess(clientIp) {
    // Check specific IPs
    if (TRUSTED_IPS.has(clientIp)) {
        return true;
    }

    // Check CIDR ranges
    if (ipRangeCheck(clientIp, TRUSTED_RANGES)) {
        return true;
    }

    return false;
}

// SAFE: Express middleware with IP check
app.use((req, res, next) => {
    const clientIp = req.ip || req.connection.remoteAddress;

    if (!checkAccess(clientIp)) {
        return res.status(403).send('Access denied');
    }

    next();
});

// SAFE: FCrDNS verification
const dns = require('dns').promises;

async function getVerifiedHostname(ipAddress) {
    try {
        // Reverse lookup
        const hostnames = await dns.reverse(ipAddress);
        if (!hostnames.length) return null;

        const hostname = hostnames[0];

        // Forward lookup verification
        const addresses = await dns.resolve(hostname);

        if (addresses.includes(ipAddress)) {
            return hostname;  // Verified!
        }

        return null;  // Not confirmed

    } catch (error) {
        return null;
    }
}

// SAFE: Mutual TLS authentication
const https = require('https');
const fs = require('fs');

const options = {
    key: fs.readFileSync('server-key.pem'),
    cert: fs.readFileSync('server-cert.pem'),
    ca: fs.readFileSync('trusted-ca.pem'),
    requestCert: true,
    rejectUnauthorized: true  // Require valid client cert
};

const server = https.createServer(options, (req, res) => {
    // Client certificate is verified by TLS handshake
    const clientCert = req.socket.getPeerCertificate();

    if (!clientCert.subject) {
        res.writeHead(403);
        res.end('Client certificate required');
        return;
    }

    // Use certificate CN for identification
    const clientId = clientCert.subject.CN;
    console.log(`Authenticated client: ${clientId}`);

    // Process request...
});

Exploited in the Wild

Historical Unix r-commands

The rlogin, rsh, and rexec commands used .rhosts files for hostname-based trust, leading to widespread network compromises through DNS spoofing.

IP-based Access Control Bypass

Numerous systems have been compromised by attackers setting up PTR records for their IP addresses to match trusted internal hostnames.

Log Injection Attacks

Attackers have used reverse DNS spoofing to inject misleading information into security logs, complicating incident response.


Tools to test/exploit

  • nslookup — manual DNS testing.

  • dig — DNS query tool.

  • DNS server configuration — set custom PTR records for testing.

  • Wireshark — analyze DNS traffic.


CVE Examples


References

  1. MITRE. "CWE-350: Reliance on Reverse DNS Resolution for a Security-Critical Action." https://cwe.mitre.org/data/definitions/350.html

  2. CERT. "DNS Spoofing." https://www.us-cert.gov/ncas/alerts