A CNAME that still points to a decommissioned provider can be claimed by anyone - confirming the dangling condition without touching live sessions requires a three-fact probe that stops before any registration step.
A decommissioned service that still has a live DNS record is an open invitation. When a team deletes the Heroku app, the S3 bucket, or the GitHub Pages site but forgets the CNAME entry, the provider releases that hostname slot back to the pool. Any attacker can register the same resource name and claim the subdomain - served from the victim's own DNS record, to the victim's own users.
The mechanism is simple: a CNAME points staging.example.com at my-app.herokudns.com. The Heroku app is deleted. Heroku releases the slot. DNS still resolves - the CNAME is live, the provider's A record answers - but the last hop has no owner. Most cloud providers and SaaS platforms let any account register a resource that matches the dangling hostname. The moment that registration completes, DNS delivers the attacker's content from a subdomain that browsers treat as *.example.com.
The window between deletion and DNS removal is the attack surface. In practice that window is often permanent: DNS records are treated as infrastructure and outlive the services they pointed to by months or years. Subdomain sprawl from microservices, preview environments, and marketing campaigns means most organisations have dozens of dangling records they have never audited.
A safe probe establishes three facts in sequence and stops before any registration step. First, DNS resolution: does the CNAME chain resolve to a live IP without returning NXDOMAIN? NXDOMAIN means the provider's own DNS is gone too - a different class of drift, not a takeover candidate. Second, provider identification: does the resolved IP range belong to a known SaaS provider? Third, the unclaimed response: does an HTTP request to the subdomain return that provider's specific 'no app configured' page rather than a real application?
import dns.resolver, requests # Step 1: resolve the CNAME chain def resolve_cname(hostname): try: answers = dns.resolver.resolve(hostname, 'CNAME') return str(answers[0].target).rstrip('.') except dns.resolver.NXDOMAIN: return None # already broken at DNS - no takeover # Step 2 + 3: fingerprint the provider response def probe_unclaimed(subdomain, nonce='62615533'): r = requests.get( f"https://{subdomain}/", headers={"X-AP-Nonce": nonce}, timeout=8, allow_redirects=True, verify=True, ) for provider, sig in PROVIDER_SIGS.items(): if sig in r.text: return provider, r.status_code # confirmed dangling return None, r.status_code # live app or unknown provider PROVIDER_SIGS = { "heroku": "No such app", "github-pages": "There isn't a GitHub Pages site here", "aws-s3": ">NoSuchBucket<", "azure": "404 Web Site not found", "fastly": "Fastly error: unknown domain", "netlify": "Not Found - Request ID", }
No registration step runs during the probe. The three-fact chain - live CNAME, provider-owned A record, provider-specific unclaimed response - is enough to confirm the dangling condition and attach it as evidence. The specific provider response body is the artifact: it is what a human reader can cross-reference to verify the finding without repeating the probe.
The unclaimed response varies so much between providers that a single-string check is a false-negative factory. Heroku returns a text body with 'No such app'. GitHub Pages returns a GitHub-branded 404 with a distinct phrase. AWS S3 returns XML with a NoSuchBucket code. Azure returns a plain HTML 404 with its own wording. Fastly includes its own name in the error. A scanner that knows only Heroku's string silently clears every GitHub Pages dangling record it encounters.
| Provider | Takeover vector | Unclaimed signature | Common in |
|---|---|---|---|
| Heroku | App name registration | "No such app" | API backends, preview envs |
| GitHub Pages | Repo + CNAME file claim | "There isn't a GitHub Pages site here" | Docs, landing pages |
| AWS S3 | Bucket name registration | <Code>NoSuchBucket</Code> in XML | Static hosting, assets |
| Azure | CloudApp DNS / Traffic Manager | "404 Web Site not found" | Enterprise workloads |
| Fastly | Service domain registration | "Fastly error: unknown domain" | CDN-fronted sites |
| Netlify | Site custom domain claim | "Not Found - Request ID" | JAMstack, marketing |
The probe logic branches on the CNAME target before checking the response body. A CNAME ending in .herokudns.com gets Heroku's signature list; one ending in .s3-website.amazonaws.com gets S3's XML pattern. This per-provider dispatch is also how severity is calibrated: providers that allow anonymous registration without email verification carry higher risk than those requiring account linkage.
Severity follows what a claimed subdomain can reach. A dangling record on a subdomain that carries no trust - no cookie scope, no OAuth redirect, no password-reset link - is high, because content injection and phishing delivery from a trusted-looking hostname are real impacts. The rating escalates to critical when the subdomain is in scope for Domain=.example.com cookies, is listed as an OAuth redirect URI, or appears in transactional email templates. In those cases, a claimed slot is a path to session hijacking or account takeover with no further vulnerability required.
The fix has two parts that must happen in the right order. Before deleting a cloud resource, check whether any DNS record points at it. Remove the DNS entry first, wait for TTL expiry, then delete the resource. Reversing the order is the root cause of every subdomain takeover: the resource disappears and opens the registration slot while DNS is still pointing there. Automation helps - a pre-deletion hook that queries the org's DNS zones for CNAMEs targeting the resource being deleted prevents the ordering mistake at scale.
Continuous IaC and DNS scanning closes the retrospective gap. Every CNAME in every zone file is resolved and checked against the live provider response on each commit and on a periodic schedule. The finding surfaces the first time the slot goes unclaimed - not six months later when an attacker has already taken it.
The posture view correlates each dangling record with the asset it was associated with - former staging environments, deleted campaign sites, retired microservices - so remediation is a targeted DNS cleanup rather than a full zone audit. One confirmed finding per dangling record, with the provider, the CNAME chain, and the unclaimed response body attached.