Back to Checks
cloudflare

DNS records pointing to hostnames have valid targets without takeover risk

dns_record_cname_target_valid
Service: dnsSeverity: high
OfficialBuilt-inVerified
Categories:internet-exposed
Resource Type:DNSRecord

Cloudflare DNS records (CNAME, MX, NS, SRV) that point to hostnames are assessed for dangling record vulnerabilities by checking if the target domain resolves to a valid address, preventing subdomain takeover, mail interception, and service hijacking attacks.

Risk

Dangling DNS records pointing to non-existent targets create multiple vulnerabilities. Dangling CNAME/NS allows subdomain takeover; dangling MX allows mail interception. Attackers can impersonate your organization, hijack services, or redirect legitimate traffic to attacker-controlled infrastructure.

Run this check with Prowler

Run in Prowler Cloud
Run
prowler cloudflare --checks dns_record_cname_target_valid

Recommendation

Remove or update dangling DNS records to prevent takeover and interception attacks.

  • Regularly audit DNS records when decommissioning services
  • Remove CNAME, MX, NS, and SRV records pointing to deprovisioned resources
  • Monitor for unauthorized changes to DNS records
  • Consider using DNS monitoring tools to detect dangling records

Remediation

Other

1Log in to the Cloudflare dashboard and select your account and domain
2Go to DNS > Records
3Identify CNAME, MX, NS, or SRV records with dangling targets
4Either update the record to point to a valid target or delete the record
5If the target service was decommissioned, remove the DNS record

Check Code

import socket

from prowler.lib.check.models import Check, CheckReportCloudflare
from prowler.providers.cloudflare.services.dns.dns_client import dns_client

# Record types that point to hostnames and can be dangling:
# - CNAME: Alias to another hostname
# - MX: Mail server hostname (dangling = potential mail interception)
# - NS: Nameserver delegation (dangling = subdomain takeover)
# - SRV: Service location hostname
DANGLING_RISK_TYPES = {"CNAME", "MX", "NS", "SRV"}

# Risk descriptions for each record type
RISK_DESCRIPTIONS = {
    "CNAME": "subdomain takeover risk",
    "MX": "potential mail interception risk",
    "NS": "subdomain delegation takeover risk",
    "SRV": "service discovery vulnerability",
}


class dns_record_cname_target_valid(Check):
    """Ensure that DNS records pointing to hostnames have valid, resolvable targets.

    Dangling DNS records that point to non-existent or unresolvable targets pose
    significant security risks. CNAME and NS records can lead to subdomain takeover,
    MX records can allow mail interception, and SRV records can expose service
    vulnerabilities. Attackers can claim orphaned target resources and serve
    malicious content, intercept email, or hijack services under your domain.
    """

    def execute(self) -> list[CheckReportCloudflare]:
        """Execute the dangling DNS record validation check.

        Iterates through all DNS records that point to hostnames (CNAME, MX, NS, SRV)
        and attempts to resolve their targets using DNS lookup. Records pointing to
        unresolvable targets are flagged as potential security risks.

        Returns:
            A list of CheckReportCloudflare objects with PASS status if the
            target resolves successfully, or FAIL status if the target
            cannot be resolved (dangling record).
        """
        findings = []

        for record in dns_client.records:
            # Check record types that point to hostnames
            if record.type not in DANGLING_RISK_TYPES:
                continue

            report = CheckReportCloudflare(
                metadata=self.metadata(),
                resource=record,
            )

            target = self._extract_target(record.type, record.content)
            is_valid = self._check_target_resolves(target)
            risk_desc = RISK_DESCRIPTIONS.get(record.type, "security risk")

            if is_valid:
                report.status = "PASS"
                report.status_extended = f"{record.type} record {record.name} points to valid target {target}."
            else:
                report.status = "FAIL"
                report.status_extended = (
                    f"{record.type} record {record.name} points to potentially dangling "
                    f"target {target} - {risk_desc}."
                )
            findings.append(report)

        return findings

    def _extract_target(self, record_type: str, content: str) -> str:
        """Extract the target hostname from record content.

        Different record types have different content formats:
        - CNAME: hostname
        - MX: priority hostname (e.g., "10 mail.example.com")
        - NS: hostname
        - SRV: Cloudflare returns "weight port hostname" (e.g., "5 80 sip.example.com")
        """
        if record_type == "MX":
            # MX format: "priority hostname"
            parts = content.split(None, 1)
            return parts[1] if len(parts) > 1 else content
        elif record_type == "SRV":
            # SRV format from Cloudflare: "weight port hostname"
            parts = content.split()
            # Target is the last part (hostname)
            return parts[-1] if parts else content
        else:
            # CNAME and NS are just hostnames
            return content

    def _check_target_resolves(self, target: str) -> bool:
        """Check if target hostname resolves to a valid address."""
        # Remove trailing dot if present
        target = target.rstrip(".")

        try:
            # Attempt DNS resolution
            socket.getaddrinfo(target, None, socket.AF_UNSPEC)
            return True
        except socket.gaierror:
            # DNS resolution failed - potential dangling record
            return False
        except Exception:
            # On any other error, assume valid to avoid false positives
            return True