AppSec · Aug 9, 2026 · 7 min read

Subdomain takeover: the DNS record that outlived the service

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.

01How a dangling CNAME becomes a takeover path

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.

staging.example.com CNAME ──► my-app.herokudns.com A ──► 52.x.x.x (Heroku infra) [app deleted - slot released by provider] attacker registers 'my-app' on Heroku my-app.herokudns.com ──► attacker content victim user visits staging.example.com DNS ──► 52.x.x.x ──► attacker-controlled response
The CNAME never changes. Only the resource at the provider end was deleted - and then reclaimed.

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.

02Confirming the dangling condition without claiming the slot

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?

dangling-check.pypython
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.

03Provider signatures differ - probe per provider or miss most of the surface

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.

ProviderTakeover vectorUnclaimed signatureCommon in
HerokuApp name registration"No such app"API backends, preview envs
GitHub PagesRepo + CNAME file claim"There isn't a GitHub Pages site here"Docs, landing pages
AWS S3Bucket name registration<Code>NoSuchBucket</Code> in XMLStatic hosting, assets
AzureCloudApp DNS / Traffic Manager"404 Web Site not found"Enterprise workloads
FastlyService domain registration"Fastly error: unknown domain"CDN-fronted sites
NetlifySite 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.

Proven - CriticalDangling CNAME - subdomain takeover - auth.example.com (OAuth redirect URI)
GET https://auth.example.com/ ──► HTTP 404 body: "No such app" (Heroku unclaimed signature) CNAME: auth.example.com ──► example-auth.herokudns.com A: example-auth.herokudns.com ──► 52.x.x.x (Heroku infra, confirmed IP range) auth.example.com is listed as an allowed OAuth redirect_uri in the IDP config
Oracle: provider-signature match on unclaimed response body. No registration step performed during the probe. Severity critical - the subdomain is a registered OAuth redirect URI; an attacker who claims the Heroku slot receives authorization codes from the IDP on every login flow that targets it. Session hijacking without any further vulnerability required.
Reachable - HighDangling CNAME - subdomain takeover candidate - staging.example.com
GET https://staging.example.com/ ──► HTTP 404 body: "There isn't a GitHub Pages site here" CNAME: staging.example.com ──► example-staging.github.io Slot open: GitHub Pages requires only a CNAME file in a public repo to claim
Oracle: GitHub Pages unclaimed signature. No registration performed. Severity high - staging domain carries no OAuth redirect or cookie scope; impact is content injection and phishing delivery from a trusted-looking hostname. Fix: remove the CNAME record before deleting the repo.

04Severity, SLA, and the fix

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.

6
major cloud providers fingerprinted
0
registrations performed during probe
3
facts required to confirm: live CNAME, provider IP, unclaimed response

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.

Evidence over heuristicsReachable beats foundMonitor → Block: rolling out a gate without slowing teamsIaC-grounded threat modelingAir-gapped AppSec with no phone-homeOne platform vs three tools
See it on your own app →