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
asuidTXT 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.comallowlists and can obtain a publicly trusted certificate for itself. - CAA records with
accounturilimit which ACME account can get certificates for your names; they do not prevent the takeover itself.
| Setting | Value | Note | Source |
|---|---|---|---|
| Order on creation | Resource, then domain verification, then the DNS record | A record that exists before its resource is dangling from the start. | OWASP Subdomain Takeover Prevention Cheat Sheet |
| Order on removal | Remove the record, wait one TTL, then delete the resource | For a delegated subzone, remove the NS record in the parent first. | Route 53 Developer Guide: Deleting a public hosted zone |
| Record definition | Built from the resource's attributes in IaC, as an alias record where the provider offers one | The dependency graph then orders creation and deletion for you. | Microsoft Learn: Prevent dangling DNS entries |
| Domain verification | On wherever the provider supports it, and kept in place | Azure App Service asuid.<sub> TXT, GitHub Pages verified domains, a certificate for every CloudFront alternate domain name. | Microsoft Learn; GitHub Docs; CloudFront Developer Guide |
| Reusable resource names | Keep the resource, or use a namespace only you can own | Empty an S3 bucket instead of deleting it, or create buckets in the account regional namespace. | Amazon S3 User Guide: bucket naming rules |
| Wildcard records to third-party services | None | GitHub warns that a wildcard leaves deeper names open even on a verified domain. | GitHub Docs: Verifying your custom domain |
| Cookies and allowlists | __Host- cookies; exact origins in CORS, CSP and OAuth redirect URIs | A taken-over sibling then gets no session and no trust. | ASVS 5.0 V3.3.3, V3.4.2, V10.4.1 |
| Certificate issuance | CAA with accounturi (and validationmethods) where your CA supports them; Certificate Transparency monitoring | Limits who can get certificates; does not prevent the takeover. | RFC 8657; Let's Encrypt CAA documentation |
| Scanning | Every zone, daily and after each DNS or teardown change | Check CNAME targets, NS delegations and A records against your inventory. | OWASP Subdomain Takeover Prevention Cheat Sheet |
- 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
.invalidname. - For each CNAME to a cloud or SaaS provider, confirm the provider's domain verification is in place (Azure
asuidTXT, GitHub Pages verified domain, CloudFront certificate). - 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.
- Confirm session cookies use the
__Host-prefix and that CORS, CSP and OAuth redirect allowlists contain no*.example.compatterns. - Confirm CAA records exist for your domains and, if your CA supports it, use
accounturi; confirm Certificate Transparency alerts reach someone. - 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 prerequisites
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 practiceHow a deleted resource becomes someone else's subdomain
- The resource is deleted, the record is notA team deletes the
shop-oldApp Service, butshop.example.comstill has a CNAME toshop-old.azurewebsites.net. - 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.
- Users resolve your name to their resourceBrowsers look up
shop.example.com, follow the CNAME and connect to the attacker's resource. - The page runs with your domain's trustIt receives cookies scoped to
.example.com, passes*.example.comallowlists and can obtain a certificate for the name.
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.
| Record | Dangles when | What the claimant gets |
|---|---|---|
| CNAME (most common) | The target host name at a cloud or SaaS provider is released and can be re-created by anyone | The subdomain's web traffic |
| A or AAAA | A released elastic or public IP address is reassigned to another customer | All 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 servers | The whole subzone: any record under it, including MX and TXT |
| MX | The mail service behind the record is released | Mail for the name, including password resets, and mail-based certificate validation |
| Wildcard to a provider | Any name under it is unclaimed at the provider | Every unused subdomain under the wildcard |
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/).
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.
| Service | Reclaimable? | 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 it | Keep 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 CloudFront | Only with a certificate | Adding 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 domain | Delete 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 labels | Yes for released names | asuid 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 period | Only subscriptions in the original Microsoft Entra tenant can reclaim the name during the reservation |
| GitHub Pages | Yes for unverified custom domains | Verify the domain for the user or organization; it also covers immediate subdomains |
| Elastic and public IP addresses | Yes once released to the pool | Keep allocations you still publish, or use provider alias records |
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.
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.
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
}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.
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
}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/.
; 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"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.
"""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))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.
# 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"- 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/).
- 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.comin 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
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. 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. Rank by blast radius
Decide which dangling record is most serious and why.
Expected result and reasoning
R4: whoever recreates
dev.example.comat 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. 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
wwwshould keep working, recreate the bucket yourself and put CloudFront with a certificate in front; forpromo, add theasuid.promoTXT before re-creating the record. Remove R5 unless the address is reallocated to you.4. Shrink the blast radius
Change the three application settings so a future takeover gets nothing.
Expected result and reasoning
Rename the cookie
__Host-sidwith noDomainattribute; 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. 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.
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.
- OWASP Subdomain Takeover Prevention Cheat Sheet
- OWASP WSTG: Test for Subdomain Takeover (WSTG-CONF-10). Version 4.2.
- Microsoft Learn: Prevent dangling DNS entries and avoid subdomain takeover. Updated 20 July 2026.
- Microsoft Learn: Azure DNS alias records
- Amazon S3 User Guide: General purpose bucket naming rules (name reuse and account regional namespace)
- Amazon Route 53 Developer Guide: Deleting a public hosted zone
- Amazon CloudFront Developer Guide: Use custom URLs by adding alternate domain names
- GitHub Docs: Verifying your custom domain for GitHub Pages
- RFC 8659: DNS Certification Authority Authorization (CAA) Resource Record. RFC 8659 (November 2019).
- RFC 8657: CAA Record Extensions for Account URI and ACME Method Binding. RFC 8657 (November 2019).
- Let's Encrypt: Certificate Authority Authorization (CAA)
- RFC 8555: Automatic Certificate Management Environment (ACME), HTTP challenge. RFC 8555 (March 2019).
- RFC 6761: Special-Use Domain Names (.invalid). RFC 6761 (February 2013).
- Terraform Registry: time_sleep resource (hashicorp/time). hashicorp/time 0.14.2.
- 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.
- SDLC phase
- ReleaseMaintenance
- 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