Find and remove dangling DNS records to prevent subdomain takeover
For developers who manage DNS, custom domains, or decommissioned services. This shows how to enumerate subdomains, identify dangling CNAME/A/NS records that point at unclaimed infrastructure, confirm the risk with HTTP/DNS checks, and remove or replace the records safely.
TL;DR — Subdomain takeover usually happens because DNS still points at a service you deleted or never claimed. The fastest win is: export your DNS zone, enumerate live subdomains, probe each hostname for NXDOMAIN/unclaimed-service responses, then delete or repoint every stale record you can prove is unused. Reading time: ~5 min
Goal
When you are done, your zone has no dangling subdomain records pointing at unclaimed third-party services or dead infrastructure, and you can prove it with DNS and HTTP checks that return either a valid owned service or no record at all.
Prerequisites
- DNS provider access with permission to view and edit records
- Access to the systems that should legitimately answer for your subdomains, or the owner/team list for them
digfrom BIND 9 tools — check withdig -vcurl8+ — check withcurl --versionjq1.6+ — check withjq --versionsubfinder2.6+ oramass4+ for enumeration — check withsubfinder -versionoramass versionhttpx1.6+ for bulk HTTP probing — check withhttpx -version- A text file containing your root domain, for example
example.com - If your provider supports export: the current zone file or CSV export of DNS records
Steps
Step 1: Export or list your current DNS records
Use your DNS provider dashboard to export the zone if available. In your provider's dashboard, open the domain and use the DNS export path your UI provides (for example, a DNS/Records page often has an Export action).
If export is not available, query the records you care about directly:
DOMAIN=example.com
for type in A AAAA CNAME MX NS TXT; do
echo "### $type"
dig +noall +answer "$DOMAIN" "$type"
dig +noall +answer "*.$DOMAIN" "$type"
done
What you should see on success: either a zone export file downloaded locally, or dig output listing current answers such as app.example.com. 300 IN CNAME myapp.hostedservice.net.
Step 2: Enumerate subdomains that currently resolve
Collect candidate hostnames from DNS and from passive enumeration. This catches records that exist even if nobody documented them.
DOMAIN=example.com
subfinder -silent -d "$DOMAIN" | sort -u > subs.txt
while read -r host; do
dig +short "$host" | sed "s/^/$host /"
done < subs.txt | tee resolved.txt
If you use amass instead:
DOMAIN=example.com
amass enum -passive -d "$DOMAIN" -norecursive -noalts | sort -u > subs.txt
What you should see on success: subs.txt contains hostnames, and resolved.txt shows only the ones that still return an address or CNAME target.
Step 3: Isolate records most likely to be dangling
You are looking for CNAMEs to third-party platforms, A/AAAA records to decommissioned IPs, and NS records delegating abandoned subzones.
cut -d' ' -f1 resolved.txt | sort -u | while read -r host; do
echo "### $host"
dig +noall +answer "$host" CNAME
dig +noall +answer "$host" A
dig +noall +answer "$host" AAAA
dig +noall +answer "$host" NS
done | tee dns-audit.txt
Review dns-audit.txt for patterns like:
### old-docs.example.com
old-docs.example.com. 300 IN CNAME team-docs.some-platform.example.
### legacy-api.example.com
legacy-api.example.com. 300 IN A 203.0.113.24
### dev.example.com
dev.example.com. 300 IN NS ns1.abandoned-dns-host.example.
dev.example.com. 300 IN NS ns2.abandoned-dns-host.example.
What you should see on success: a short list of hostnames whose targets are external platforms, old IPs, or delegated nameservers you no longer control.
Step 4: Probe each candidate over HTTP/HTTPS and inspect the exact failure
A dangling DNS record is risky when the target is unclaimed and the platform will let someone else claim it. The HTTP response often tells you that directly.
awk '/CNAME|A|AAAA|NS/ {print $2}' dns-audit.txt | sort -u > candidates.txt
httpx -l candidates.txt -follow-host-redirects -status-code -title -location -tech-detect -json | jq -r '[.input,.status_code,.location,.title] | @tsv' | column -t
For a single host, inspect headers manually:
curl -I --max-time 10 https://old-docs.example.com || true
curl -I --max-time 10 http://old-docs.example.com || true
Typical risky output shapes:
HTTP/2 404
server: nginx
date: Tue, 30 Jun 2026 10:14:22 GMT
content-type: text/html
content-length: 162
HTTP/1.1 404 Not Found
Server: cloud service edge
Location: https://signup.target-service.example/
A dead target often looks like this instead:
curl: (6) Could not resolve host: team-docs.some-platform.example
What you should see on success: candidates split into three buckets — valid owned services, dead targets, and unclaimed-service responses that indicate takeover risk.
Step 5: Confirm whether the target is still intentionally owned
Before deleting anything, verify ownership with the service owner or deployment config. Search your repos and infra code for the hostname.
grep -RIn --exclude-dir=.git --exclude-dir=node_modules 'old-docs\.example\.com\|team-docs\.some-platform\.example' .
If you manage infrastructure as code, search there too:
grep -RIn --exclude-dir=.terraform 'old-docs\.example\.com\|legacy-api\.example\.com' infra/
What you should see on success: either a clear current owner/config reference, or no hits and no owner, which means the record is a removal candidate.
⚠️ Deleting the wrong DNS record can cause production downtime. If the hostname appears in code, certificates, redirects, webhooks, OAuth callbacks, or customer docs, stop and get the owning team to approve the change.
Step 6: Remove or replace the dangling record
If the hostname is unused, delete the record in your DNS provider dashboard under the domain's DNS/Records page. Delete the exact stale record type and name, for example:
- delete
old-docs CNAME team-docs.some-platform.example. - delete
legacy-api A 203.0.113.24 - delete delegated
NSrecords fordev.example.comonly if you intend to remove that subzone
If the hostname should still exist, repoint it to infrastructure you control. Example zone-file style replacements:
old-docs 300 IN CNAME docs.internal.example.
legacy-api 300 IN A 198.51.100.42
What you should see on success: the record disappears from the DNS UI or now points at the replacement target you entered.
Step 7: Wait one TTL, then re-check authoritative and recursive answers
Use the TTL from the old record. Query both your normal resolver and the authoritative nameserver.
DOMAIN=example.com
HOST=old-docs.example.com
AUTH_NS=$(dig +short NS "$DOMAIN" | head -n1)
echo "Authoritative: $AUTH_NS"
dig +noall +answer "$HOST"
dig +noall +answer "@$AUTH_NS" "$HOST"
What you should see on success: either no answer for a deleted record, or the new target/IP from both resolvers after caches expire.
Verify it works
Run end-to-end checks for every hostname you changed.
while read -r host; do
echo "### $host"
dig +short "$host"
curl -I --max-time 10 "https://$host" || true
curl -I --max-time 10 "http://$host" || true
done < candidates.txt
Expected results:
- Deleted hostname:
dig +shortprints nothing, andcurlreturnscurl: (6) Could not resolve host - Repointed hostname:
dig +shortprints the new CNAME or IP, andcurl -Ireturns your expected status, commonly one of:
HTTP/2 200
HTTP/1.1 301 Moved Permanently
Location: https://www.example.com/docs
For delegated subzones, verify the delegation is gone or valid:
dig +trace dev.example.com
Expected result: no abandoned nameservers in the trace.
Common pitfalls
Wrong record type removed
Mistake: deleting the A record while a CNAME for the same logical service still exists elsewhere, or vice versa.
Symptom: the hostname still resolves or starts resolving differently than expected.
Fix: query all record types with dig +noall +answer host A AAAA CNAME NS and delete or update the exact live record.
Propagation impatience
Mistake: testing immediately after deleting a record with a 3600-second TTL.
Symptom: your laptop still resolves the old target, but the authoritative server shows the new state.
Fix: check the old TTL, query the authoritative nameserver with dig @ns1.your-dns host, and wait one TTL before declaring failure.
Cached HTTP redirects hide the real state
Mistake: trusting a browser that cached a 301 from the old service.
Symptom: browser keeps redirecting even after DNS is fixed; curl shows something else.
Fix: verify with curl -I --max-time 10 and a fresh DNS lookup; do not use browser behavior as the source of truth.
Trailing-dot and hostname typos in manual edits
Mistake: entering target.example.com.example.com because the DNS UI auto-appends the zone, or omitting a required absolute target in zone-file style tooling.
Symptom: dig shows a malformed CNAME target under your own zone.
Fix: in zone files, use a trailing dot for absolute targets like docs.internal.example.; in provider UIs, enter exactly what that UI expects and confirm with dig after saving.
Forgetting delegated NS records
Mistake: auditing only A and CNAME records and ignoring subzone delegation.
Symptom: dev.example.com is still controlled by external nameservers you no longer own.
Fix: query NS for subdomains and remove abandoned delegations or re-point them to nameservers you control.
Assuming NXDOMAIN equals safe when wildcard DNS exists
Mistake: seeing inconsistent answers because *.example.com or provider parking records exist.
Symptom: random hostnames resolve, but the intended hostname still serves an unclaimed app page.
Fix: inspect the exact hostname with dig +noall +answer host, compare with a nonsense hostname, and verify the HTTP response for the real subdomain before closing the ticket.
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI