In short

Never let a DNS record outlive the resource it names. Create the resource and verify the domain with the provider before you publish the record, and remove the record, wait out its TTL, and only then delete the resource. Encode that order in infrastructure as code by building each record from its resource's attributes, and prefer alias records and domain-verification features (Azure's asuid TXT record, GitHub Pages verified domains, CloudFront's certificate check, S3's account regional namespace) that stop a stranger from binding your name. Keep cookies host-only with __Host-, list exact origins in CORS, CSP and OAuth instead of *.example.com, and scan every zone on a schedule for CNAME targets that do not exist, delegations that are refused and addresses you no longer own.

  • A takeover needs two things: a record still pointing at a provider name or address, and a provider that lets a stranger claim that name once you release it.
  • CNAMEs are the usual case, but A records to released IP addresses and NS delegations to deleted hosted zones are takeovers too, and an NS takeover hands over the whole subzone.
  • Delete the record first: the Route 53 documentation says to remove a delegation, wait for its TTL to expire and only then delete the child zone, or someone could hijack it.
  • Domain verification closes the provider side: with an asuid TXT record in place, Microsoft says no other Azure subscription can bind the custom domain.
  • A taken-over subdomain receives cookies scoped to the parent domain, passes *.example.com allowlists and can obtain a publicly trusted certificate for itself.
  • CAA records with accounturi limit which ACME account can get certificates for your names; they do not prevent the takeover itself.

Verify it

  1. Export every public zone you own and run a dangling-record scan over CNAME, NS, A, AAAA and MX records; confirm it reports zero findings, and that it does report one when you point a test record at a .invalid name.
  2. For each CNAME to a cloud or SaaS provider, confirm the provider's domain verification is in place (Azure asuid TXT, GitHub Pages verified domain, CloudFront certificate).
  3. Destroy a test stack built with your IaC module and confirm the DNS record is deleted before the resource, and the delegation before the child zone, with the wait in between.
  4. Confirm session cookies use the __Host- prefix and that CORS, CSP and OAuth redirect allowlists contain no *.example.com patterns.
  5. Confirm CAA records exist for your domains and, if your CA supports it, use accounturi; confirm Certificate Transparency alerts reach someone.
  6. Check that your decommissioning checklist lists DNS removal, the TTL wait and allowlist cleanup before resource deletion.
Applies to
  • Amazon Route 53, S3 and CloudFront as documented October 2026
  • Azure DNS and Azure App Service as documented July 2026
  • GitHub Pages verified custom domains
  • Terraform 1.16 (tested with 1.16.5, AWS provider 6.68.0, AzureRM provider 5.9.0)
  • dnspython 2.9.0

Public DNS zones and the cloud and SaaS services they point to, on AWS, Azure, Google Cloud, GitHub Pages and other hosting providers. Internal DNS has the same failure mode with a smaller audience.

Part of
Learning path: Runtime and cloud defense (step 13 of 13). Workflow: Review a design
Learning goals and prerequisitesAbout 50 minutes: 20 to read, 10 to run the scanner and 20 for the exercise

What you will learn

  • Explain how a dangling CNAME, A or NS record becomes a takeover, and which provider names can be reclaimed.
  • Order creation and deletion of records and resources so no dangling window opens, and encode that order in Terraform.
  • Use domain verification, alias records and reserved namespaces where providers offer them.
  • Limit what a taken-over subdomain can reach: cookies, CORS, CSP, OAuth redirects and certificates.
  • Scan zones for dangling records and respond when one is found.

Before you start

Platform, cloud and DNS engineers who create and retire public endpoints, and application teams whose cookies, CORS and OAuth settings trust their subdomains.

  • Know the DNS record types CNAME, A, AAAA, NS and MX, and what a TTL does.
  • Manage DNS records in a provider console or with Terraform or OpenTofu.
  • For the examples: Python 3.13 or later, and Docker for terraform validate.

Refresh first: Review infrastructure changes as access changes

Go to the guided practice

How a deleted resource becomes someone else's subdomain

Your teamYour DNS zoneCloudproviderAttackerUser'sbrowserdelete app shop-old1stale CNAMEto shop-old1create appshop-old, bindshop.example.com2resolve shop.example.com3GET / with .example.com cookies4attacker's page4Trust boundaryUntrusted input1Step, described below
  1. The resource is deleted, the record is notA team deletes the shop-old App Service, but shop.example.com still has a CNAME to shop-old.azurewebsites.net.
  2. Someone else claims the nameAn attacker who finds the dangling record creates a resource with the same name in their own subscription. Without domain verification, the provider binds the custom domain to it.
  3. Users resolve your name to their resourceBrowsers look up shop.example.com, follow the CNAME and connect to the attacker's resource.
  4. The page runs with your domain's trustIt receives cookies scoped to .example.com, passes *.example.com allowlists and can obtain a certificate for the name.
Removing the record before the resource, and verifying the domain with the provider, each break this chain.

A record that outlives its resource

Microsoft's definition is the practical one: a subdomain takeover can occur when a DNS record points to a deprovisioned resource, and such records are called dangling DNS entries. The record still says shop.example.com lives at shop-old.azurewebsites.net, but nothing of yours answers there. If the provider lets a new customer create a resource with that same name, and bind your host name to it, that customer now serves shop.example.com.

The OWASP cheat sheet explains why it keeps happening: cloud resources are short-lived while DNS records persist, different teams often own the resource and the zone, nothing links a record to the existence of its target, and acquisitions bring zones nobody documented. OWASP WSTG-CONF-10 describes how testers look for it. The record types below can all dangle.

A record that outlives its resource
RecordDangles whenWhat the claimant gets
CNAME (most common)The target host name at a cloud or SaaS provider is released and can be re-created by anyoneThe subdomain's web traffic
A or AAAAA released elastic or public IP address is reassigned to another customerAll traffic to that name on every port
NS (delegation)The delegated zone is deleted at the DNS host, and someone can create it again on the same name serversThe whole subzone: any record under it, including MX and TXT
MXThe mail service behind the record is releasedMail for the name, including password resets, and mail-based certificate validation
Wildcard to a providerAny name under it is unclaimed at the providerEvery unused subdomain under the wildcard

Why it is an application security problem

A taken-over subdomain is a same-site origin under your registrable domain, so everything that trusts "our subdomains" trusts the attacker. Cookies set with Domain=example.com are sent to it, and it can set cookies for the parent that your application will read. CORS policies that accept any https://*.example.com origin, CSP sources such as *.example.com, postMessage checks on the domain suffix, and OAuth or OIDC clients that register wildcard redirect URIs all give it access. Microsoft notes that the attacker can also apply for and receive a valid certificate for the hijacked name, which defeats the assumption that HTTPS protects secure cookies.

The certificate part follows from how domain validation works. ACME's HTTP challenge (RFC 8555) proves control of a name by serving a file from it, and whoever controls the content behind the name can do that. MX takeovers add mail: password reset links and mail-based validation for certificates. Phishing on a real company host name is the least technical consequence and often the first one reported.

Treat these as the blast radius to shrink, independently of preventing takeovers: name session cookies __Host- so they never leave the exact host (ASVS 5.0 V3.3.3, and /domains/appsec/browser-security/security-headers/), validate CORS origins against an exact allowlist (V3.4.2, /domains/appsec/browser-security/cors-csp-browser-boundaries/), and register exact redirect URIs (V10.4.1, /domains/identity-crypto/authentication/oauth-oidc-attacks/).

Which provider names can be reclaimed

Whether a dangling record is exploitable depends on the provider. The question is whether a new customer can claim a name you released and bind your host name to it. The table summarizes what each provider documents today; the OWASP cheat sheet keeps a broader list and points to the community project can-i-take-over-xyz, which is third-party and changes often, so check the provider's own documentation before you rely on either.

Which provider names can be reclaimed
ServiceReclaimable?Provider control that helps
Amazon S3 buckets (website endpoints)Yes: AWS says another account can create a deleted bucket's name and receive requests meant for itKeep the bucket and empty it, or create buckets in the account regional namespace (-<account>-<region>-an); website buckets must match the host name, so front them with CloudFront
Amazon CloudFrontOnly with a certificateAdding an alternate domain name requires a trusted certificate covering it
Route 53 hosted zones (NS delegation)Yes: AWS warns that deleting a hosted zone lets someone hijack the domainDelete the parent's NS record first, wait for its TTL, then delete the child zone
Azure App Service, Front Door, Traffic Manager, CDN, Blob Storage, public IP DNS labelsYes for released namesasuid TXT verification for App Service; alias records for Front Door, Traffic Manager, CDN and public IPs; Defender for App Service alerts
Azure classic cloud services (cloudapp.net)After a reservation periodOnly subscriptions in the original Microsoft Entra tenant can reclaim the name during the reservation
GitHub PagesYes for unverified custom domainsVerify the domain for the user or organization; it also covers immediate subdomains
Elastic and public IP addressesYes once released to the poolKeep allocations you still publish, or use provider alias records

Order the record and resource lifecycle

Most takeovers are an ordering bug in a manual process. On creation, create the resource, complete the provider's domain verification, and publish the DNS record last. On removal, do the reverse: point the name at a maintenance page or delete the record, wait at least one TTL so resolvers drop the cached answer, and only then delete the resource. The OWASP cheat sheet warns that deleting the resource first opens an immediate window for takeover.

Delegations need the same care at a larger scale. The Route 53 guide says to delete the parent's NS record for a child zone first and wait for that record's TTL to expire before deleting the child hosted zone, so no one can hijack the child zone while resolvers still have its name servers cached. Make "remove DNS records" a required step in every decommissioning checklist, together with removing the host name from OAuth clients, CORS and CSP allowlists, and revoking its certificates. Microsoft suggests a delete lock on any resource with a custom DNS entry, as a reminder that the record must go first.

Some names should never be released. AWS recommends emptying a bucket and keeping it rather than deleting it if you want to stop another account from receiving its requests. Where the provider offers a namespace only your account can own, such as S3's account regional namespace, use it for new resources.

Terraform on AWS: alias records and a delegation that waits

Infrastructure as code turns the lifecycle rule into a dependency graph. A record whose value is computed from the resource (aws_lb.shop.dns_name, aws_route53_zone.dev.name_servers) is created after the resource and destroyed before it. Route 53 alias records also track the target's current address, so no raw host name or IP is copied into the zone. For a delegated subzone, the time_sleep resource from the hashicorp/time provider makes destruction wait 300 seconds, the NS record's TTL, between removing the delegation and deleting the child zone. The configuration passed terraform validate with Terraform 1.16.5 and the AWS provider 6.68.0; it was not applied to a real account.

Configurationaws/main.tfHCL
terraform {
  required_providers {
    aws  = { source = "hashicorp/aws", version = "6.68.0" }
    time = { source = "hashicorp/time", version = "0.14.2" }
  }
}

provider "aws" {
  region = "eu-west-1"
}

variable "public_subnet_ids" {
  type = list(string)
}

resource "aws_route53_zone" "main" {
  name = "example.com"
}

resource "aws_lb" "shop" {
  name               = "shop"
  load_balancer_type = "application"
  subnets            = var.public_subnet_ids
}

# An alias record built from the load balancer's own attributes. Terraform creates the load
# balancer first and, on destroy, deletes this record before the load balancer.
resource "aws_route53_record" "shop" {
  zone_id = aws_route53_zone.main.zone_id
  name    = "shop.example.com"
  type    = "A"

  alias {
    name                   = aws_lb.shop.dns_name
    zone_id                = aws_lb.shop.zone_id
    evaluate_target_health = true
  }
}

# A delegated subdomain. On destroy the NS record goes first, then Terraform waits out its
# TTL before deleting the child zone, as the Route 53 documentation recommends.
resource "aws_route53_zone" "dev" {
  name = "dev.example.com"
}

resource "time_sleep" "dev_ns_ttl" {
  depends_on       = [aws_route53_zone.dev]
  destroy_duration = "300s"
}

resource "aws_route53_record" "dev_delegation" {
  depends_on = [time_sleep.dev_ns_ttl]
  zone_id    = aws_route53_zone.main.zone_id
  name       = "dev.example.com"
  type       = "NS"
  ttl        = 300
  records    = aws_route53_zone.dev.name_servers
}

Terraform on Azure: domain verification and alias records

On Azure, the App Service custom domain is protected by the asuid.<subdomain> TXT record holding the app's domain verification ID: Microsoft states that while that record exists, no other Azure subscription can validate the custom domain or take it over. Someone can still create an app with the old name, but they cannot bind your host name to it. For public IPs, Front Door, Traffic Manager and CDN endpoints, an Azure DNS alias record (target_resource_id) couples the record to the resource, so deleting the resource leaves an empty record set instead of a dangling one. The configuration passed terraform validate with the AzureRM provider 5.9.0. The OWASP cheat sheet suggests keeping the verification TXT record even after decommissioning; if you do, manage it outside the app's lifecycle.

Configurationazure/main.tfHCL
terraform {
  required_providers {
    azurerm = { source = "hashicorp/azurerm", version = "5.9.0" }
  }
}

provider "azurerm" {
  features {}
}

resource "azurerm_resource_group" "shop" {
  name     = "rg-shop"
  location = "westeurope"
}

resource "azurerm_dns_zone" "main" {
  name                = "example.com"
  resource_group_name = azurerm_resource_group.shop.name
}

resource "azurerm_service_plan" "shop" {
  name                = "asp-shop"
  resource_group_name = azurerm_resource_group.shop.name
  location            = azurerm_resource_group.shop.location
  os_type             = "Linux"
  sku_name            = "B1"
}

resource "azurerm_linux_web_app" "shop" {
  name                = "app-shop-7f3c"
  resource_group_name = azurerm_resource_group.shop.name
  location            = azurerm_resource_group.shop.location
  service_plan_id     = azurerm_service_plan.shop.id
  site_config {}
}

# Domain verification: with asuid.shop in place, no other subscription can bind shop.example.com.
resource "azurerm_dns_txt_record" "shop_asuid" {
  name                = "asuid.shop"
  zone_name           = azurerm_dns_zone.main.name
  resource_group_name = azurerm_resource_group.shop.name
  ttl                 = 3600
  record {
    value = azurerm_linux_web_app.shop.custom_domain_verification_id
  }
}

resource "azurerm_dns_cname_record" "shop" {
  name                = "shop"
  zone_name           = azurerm_dns_zone.main.name
  resource_group_name = azurerm_resource_group.shop.name
  ttl                 = 300
  record              = azurerm_linux_web_app.shop.default_hostname
}

resource "azurerm_app_service_custom_hostname_binding" "shop" {
  hostname            = "shop.example.com"
  app_service_name    = azurerm_linux_web_app.shop.name
  resource_group_name = azurerm_resource_group.shop.name
  depends_on          = [azurerm_dns_txt_record.shop_asuid, azurerm_dns_cname_record.shop]
}

# An alias A record: if the public IP is deleted, the record set becomes empty instead of dangling.
resource "azurerm_public_ip" "api" {
  name                = "pip-api"
  resource_group_name = azurerm_resource_group.shop.name
  location            = azurerm_resource_group.shop.location
  allocation_method   = "Static"
}

resource "azurerm_dns_a_record" "api" {
  name                = "api"
  zone_name           = azurerm_dns_zone.main.name
  resource_group_name = azurerm_resource_group.shop.name
  ttl                 = 300
  target_resource_id  = azurerm_public_ip.api.id
}

Limit certificate issuance and watch for it

A CAA record (RFC 8659) names the certificate authorities allowed to issue for a domain. On its own it does not stop a takeover, because the attacker can usually use the same CA you do. RFC 8657 adds two parameters that do help: accounturi restricts issuance to a specific ACME account, and validationmethods restricts the challenge types. Let's Encrypt documents accounturi as a way to ensure that someone who temporarily hijacks your domain but lacks your ACME account key cannot issue certificates. Support differs between CAs, and CAA is checked on the exact name and then its parents, so test the records with your CA before relying on them.

Monitor Certificate Transparency logs for your domains, through crt.sh, a CT monitoring service or your CA's notifications, and alert on certificates issued to names or accounts you do not recognise. An unexpected certificate for a forgotten subdomain is often the first sign of a takeover. TLS operations in general are covered in /domains/identity-crypto/key-management-pki/tls-pki-operations/.

Configurationexample.com zone (CAA)Plain text
; Only Let's Encrypt, only from your ACME account, only by DNS-01 validation.
example.com.  3600  IN  CAA  0 issue "letsencrypt.org;accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1234567890;validationmethods=dns-01"
example.com.  3600  IN  CAA  0 issuewild ";"
example.com.  3600  IN  CAA  0 iodef "mailto:security@example.com"

Scan your zones for dangling records

Detection compares each record with the thing it names. For a CNAME, resolve the target: NXDOMAIN means the provider name no longer exists, and some providers, such as S3 website endpoints, answer for every name, so you must also check for their "nothing here" response, such as NoSuchBucket. For an NS delegation, ask each listed name server for the zone's SOA: REFUSED or SERVFAIL means the zone is gone from that server. For an A or AAAA record, compare the address with the inventory of addresses your accounts hold. Run the scan daily and after every DNS change or teardown.

The script below implements those checks with dnspython. Its resolver, name-server query and HTTP fetch are injectable, so the tests run offline against a fixed table, plus one live lookup of a .invalid name, which RFC 6761 reserves so that it never exists. All eight tests pass on Python 3.14.8 with dnspython 2.9.0. For Azure, Microsoft publishes the Get-DanglingDnsRecords PowerShell tool, and the Defender for App Service plan alerts when an App Service is decommissioned while its custom domain is still in DNS. Scanners with large provider fingerprint lists exist as community projects; treat their lists as hints and confirm with the provider.

Testdangling.pyPython
"""Find DNS records in your zones that point at things that no longer exist or are not yours.

Input: a JSON list of records, e.g. exported from your DNS provider or IaC state:
  [{"name": "shop.example.com", "type": "CNAME", "values": ["shop-prod.azurewebsites.net"]}, ...]
Usage: python3 dangling.py records.json --owned-ips owned-ips.txt
"""
import json
import sys

import dns.exception
import dns.message
import dns.query
import dns.rcode
import dns.resolver

# Provider host suffixes whose names a stranger can claim once you release them, and the text
# their "nothing here" page shows. Keep your own list current; these two are examples.
FINGERPRINTS = {
    ".s3-website": "NoSuchBucket",
    ".github.io": "There isn't a GitHub Pages site here.",
}


class Checker:
    def __init__(self, resolver=None, ns_rcode=None, http_get=None, owned_ips=frozenset()):
        self.resolver = resolver or dns.resolver.Resolver()
        self.ns_rcode = ns_rcode or self._ns_rcode
        self.http_get = http_get
        self.owned_ips = owned_ips

    def _ns_rcode(self, server_name, zone):
        """Ask one of the delegated name servers for the zone's SOA; return the DNS rcode."""
        address = next(iter(self.resolver.resolve(server_name, "A"))).to_text()
        query = dns.message.make_query(zone, "SOA")
        return dns.query.udp(query, address, timeout=3).rcode()

    def check(self, record):
        name, rtype, values = record["name"], record["type"].upper(), record["values"]
        if rtype == "CNAME":
            target = values[0].rstrip(".")
            try:
                self.resolver.resolve(target, "A")
            except dns.resolver.NXDOMAIN:
                return f"{name}: CNAME target {target} does not exist (NXDOMAIN)"
            except dns.resolver.NoAnswer:
                pass  # the name exists; it may have only AAAA or other records
            for suffix, marker in FINGERPRINTS.items():
                if suffix in target and self.http_get and marker in self.http_get(f"http://{name}/"):
                    return f"{name}: {target} answers with the provider's unclaimed page ({marker!r})"
        elif rtype == "NS":
            for server in values:
                try:
                    rcode = self.ns_rcode(server.rstrip("."), name)
                except (dns.exception.DNSException, OSError) as error:
                    return f"{name}: delegated name server {server} unreachable ({type(error).__name__})"
                if rcode != dns.rcode.NOERROR:
                    return f"{name}: {server} answers {dns.rcode.to_text(rcode)} for the delegated zone"
        elif rtype in ("A", "AAAA"):
            stray = [ip for ip in values if ip not in self.owned_ips]
            if stray:
                return f"{name}: {rtype} {', '.join(stray)} is not in your inventory of allocated addresses"
        return None


def main(argv):
    records = json.load(open(argv[1]))
    owned = set()
    if "--owned-ips" in argv:
        owned = {line.strip() for line in open(argv[argv.index("--owned-ips") + 1]) if line.strip()}
    checker = Checker(owned_ips=owned)
    findings = [f for f in (checker.check(r) for r in records) if f]
    print("\n".join(findings) or "no dangling records found")
    return 1 if findings else 0


if __name__ == "__main__":
    sys.exit(main(sys.argv))

Check one name by hand

When a report names a single host, these commands answer the questions in order: what does the record say, does its target exist, and does the delegated zone still answer. A name server that does not host a zone answers REFUSED for it; in a test against a real delegation, a Route 53 name server answered NOERROR for the zone it hosts and REFUSED for example.com. Run the commands only against your own names.

Manual checksShell
# 1. What does the record point to?
dig +short CNAME shop.example.com
# 2. Does the target exist? status: NXDOMAIN means no.
dig shop-old.azurewebsites.net A | grep status:
# 3. A delegated zone: read the delegation from a parent name server, then ask each child server.
PARENT_NS=$(dig +short NS example.com | head -1)
dig +norecurse NS dev.example.com @"$PARENT_NS" | sed -n '/AUTHORITY SECTION/,/^$/p'
dig +norecurse SOA dev.example.com @ns-1.dns-host.example | grep status:   # REFUSED or SERVFAIL: dangling
# 4. For a provider that answers every name, read the page.
curl -s http://www.example.com/ | grep -E "NoSuchBucket|There isn.t a GitHub Pages site here"

If a subdomain has been taken over

  • Remove or repoint the DNS record first; that ends new traffic once caches expire. Lower the TTL in advance for names you are about to change.
  • Reclaim the provider resource name if you can, so the record can point somewhere you control again.
  • Search Certificate Transparency logs for certificates issued for the name during the exposure window and ask the CA to revoke them.
  • Assume cookies scoped to the parent domain were exposed: invalidate sessions and rotate any tokens that could have been sent to the name.
  • Remove the name from OAuth redirect URIs, CORS and CSP allowlists and SPF records, and review authorization-server logs for codes delivered to it.
  • Scan every zone for other dangling records, then fix the process gap that left this one, and record the timeline as you would for any incident (/domains/appsec/vulnerability-response/product-incident-response/).

Pitfalls

  • Deleting the resource first and the record "later": the window opens immediately and nobody owns closing it.
  • Records created by hand in a zone that is otherwise managed by IaC, so no state file knows they exist.
  • Trusting *.example.com in CORS, CSP or OAuth because every subdomain is "ours": one forgotten record makes it untrue.
  • Wildcard DNS records pointing at a hosting provider, which leave every unused name claimable.
  • Scanning only CNAMEs: NS delegations hand over a whole zone, and A records to released IPs are easy to miss.
  • Assuming CAA alone blocks certificate issuance after a takeover; without accounturi, the attacker can usually use your CA too.
  • Zones from acquisitions, marketing campaigns and old environments that are in no inventory and no scan.

Guided practice

The zone export and lookup results below come from example.com. Classify each record as healthy, dangling or at risk, decide the fix and its order, and say what the dangling ones would give an attacker.

Supplied scenario material
Plain text
Zone export (name, type, value)                       Lookup result
R1 shop.example.com     A (alias)  -> ALB shop-prod        alias target exists in account 1111
R2 www.example.com      CNAME www.example.com.s3-website-eu-west-1.amazonaws.com
                                                            resolves; HTTP body contains "NoSuchBucket"
R3 promo.example.com    CNAME promo-2024.azurewebsites.net  NXDOMAIN; no asuid.promo TXT record
R4 dev.example.com      NS ns-1.dns-host.example, ns-2.dns-host.example
                                                            both name servers answer REFUSED for dev.example.com
R5 legacy.example.com   A 198.51.100.7                      address not in any account's allocated list
R6 docs.example.com     CNAME example-org.github.io         Pages site exists; example.com is a verified domain
R7 *.apps.example.com   CNAME apps-gw.herokudns.example     wildcard to a hosting provider

Application settings: session cookie "sid; Domain=example.com"; CORS allows any https://*.example.com
origin; the OAuth client registers https://*.example.com/callback.
  1. 1. Classify the records

    Mark R1 to R7 healthy, dangling or at risk, with one reason each.

    Expected result and reasoning

    R1 healthy (alias to an existing resource). R2 dangling: the website endpoint answers for any name and the bucket is gone, so anyone can create a bucket named www.example.com. R3 dangling: the app name is free and nothing verifies the domain. R4 dangling: the delegated zone no longer exists at its DNS host. R5 dangling or at risk: an address you no longer hold. R6 healthy: the domain is verified. R7 at risk: every unused name under the wildcard is claimable.

  2. 2. Rank by blast radius

    Decide which dangling record is most serious and why.

    Expected result and reasoning

    R4: whoever recreates dev.example.com at that DNS host controls every name and record type under it, including MX and TXT. Then R2 and R3, which serve web content under the parent domain's cookies and allowlists.

  3. 3. Fix in the right order

    Write the remediation steps for R2, R3 and R4.

    Expected result and reasoning

    Delete the R2 and R3 records now (or point them at a maintenance page you own) and the R4 NS record in the parent zone; then decide whether each name is still needed. If www should keep working, recreate the bucket yourself and put CloudFront with a certificate in front; for promo, add the asuid.promo TXT before re-creating the record. Remove R5 unless the address is reallocated to you.

  4. 4. Shrink the blast radius

    Change the three application settings so a future takeover gets nothing.

    Expected result and reasoning

    Rename the cookie __Host-sid with no Domain attribute; replace the CORS pattern with an exact list of origins; register exact OAuth callback URIs. These are ASVS 5.0 V3.3.3, V3.4.2 and V10.4.1.

  5. 5. Prevent the next one

    Name two controls that would have stopped R2 to R5 from appearing.

    Expected result and reasoning

    Records built from resource attributes in IaC, so destroy deletes the record first (with a TTL wait for delegations); and a scheduled scan of every zone that fails on NXDOMAIN targets, refused delegations and unowned addresses. Add the TTL wait and allowlist cleanup to the decommissioning checklist.

Your deliverable: A table of R1 to R7 with the classification, the fix and its order, plus the three corrected application settings.

Paper exercise on a fictional zone. Run scanners only against zones you own, and never claim a provider resource to "prove" a takeover on someone else's domain.

Save a draft on this device to resume later. Automatic saving begins after you choose Save. Drafts are never sent by this application; use synthetic data.

No draft saved on this device yet.

Sources & further reading

The standards, specifications and documentation behind the guidance above. Each is the evidence for a claim on this page; consult it for detail beyond the defaults and versions listed here.

  1. OWASP Subdomain Takeover Prevention Cheat Sheet
  2. OWASP WSTG: Test for Subdomain Takeover (WSTG-CONF-10). Version 4.2.
  3. Microsoft Learn: Prevent dangling DNS entries and avoid subdomain takeover. Updated 20 July 2026.
  4. Microsoft Learn: Azure DNS alias records
  5. Amazon S3 User Guide: General purpose bucket naming rules (name reuse and account regional namespace)
  6. Amazon Route 53 Developer Guide: Deleting a public hosted zone
  7. Amazon CloudFront Developer Guide: Use custom URLs by adding alternate domain names
  8. GitHub Docs: Verifying your custom domain for GitHub Pages
  9. RFC 8659: DNS Certification Authority Authorization (CAA) Resource Record. RFC 8659 (November 2019).
  10. RFC 8657: CAA Record Extensions for Account URI and ACME Method Binding. RFC 8657 (November 2019).
  11. Let's Encrypt: Certificate Authority Authorization (CAA)
  12. RFC 8555: Automatic Certificate Management Environment (ACME), HTTP challenge. RFC 8555 (March 2019).
  13. RFC 6761: Special-Use Domain Names (.invalid). RFC 6761 (February 2013).
  14. Terraform Registry: time_sleep resource (hashicorp/time). hashicorp/time 0.14.2.
  15. OWASP ASVS 5.0.0: V3 Web Frontend Security. Version 5.0.0.

Reference information

Checked against the OWASP Subdomain Takeover Prevention Cheat Sheet, WSTG 4.2, Microsoft Learn (updated 20 July 2026), the Amazon S3, Route 53 and CloudFront developer guides and GitHub Docs. The Terraform examples passed `terraform validate` with Terraform 1.16.5; the scanner ran on Python 3.14.8 with dnspython 2.9.0. Nothing was created in a real cloud account.

ASVS 5.0.0 requirements
v5.0.0-3.3.3 · v5.0.0-3.4.2 · v5.0.0-10.4.1
OWASP WSTG v4.2 tests
WSTG-CONF-10

Download this guide as Markdown