Dead Link Detection: Subdomain Takeover and Phishing Risk
Broken links as attack surface: subdomain takeovers and phishing via dangling references, with automation notes.
Dead Link Detection: Subdomain Takeover and Phishing Risk#
Why this topic matters: a 404 on your own site is a content issue. A dangling reference to something you no longer control is a security issue. Browsers do not distinguish the two.
Ethics and scope: only enumerate hosts you own or may audit, and do not register takeover candidates for offensive use. For the surrounding web testing workflow, see web hacking 101.
What a Dead Reference Is#
A dead reference is any pointer from infrastructure you control to infrastructure you no longer control:
- A DNS record pointing at a service you deleted.
- A link to a profile page that was removed.
- A CNAME to a cloud bucket that was deprovisioned.
- An integration token or webhook URL that now resolves nowhere.
Each one is a claim of legitimacy that outlived the resource it described.
Subdomain Takeover#
The standard takeover shape is:
- You once pointed
blog.example.comat a third-party host. - The third-party app was deleted, but the DNS record stayed.
- An attacker claims the matching name on that service.
- Visitors now reach attacker content under your domain.
What the dangling record looks like#
blog.example.com. CNAME your-app.example-cdn.com.
If your-app.example-cdn.com no longer resolves to content you own, the CNAME is a claim you abandoned.
Why it matters beyond defacement#
Attacker content on your subdomain inherits your cookie scope, your reputation, and your readers' trust. Even without cookies, it is a phishing landing page that starts with your name.
Broken Link Hijacking on the Open Web#
A subtler version lives in your outbound links:
- Your README links to a deleted GitHub repo.
- Your docs link to an account that closed.
- Your forum post links to a resource that moved.
If the target namespace is registerable again, the mirror can be re-created with content that imitates yours. Readers follow the trusted link and land on the imitation.
Dangling CNames and Reusable Namespaces#
Some services hand out names that get noticed quickly once freed. The general triage applies whether or not a specific takeover works:
- List every CNAME that does not resolve to live content you own.
- Check the target service's error page or claim flow.
- Record which ones return "not found" versus "claimable".
- Remove or re-point the record regardless of whether you could claim it.
The defensible fix is removal or re-pointing, not registering the name yourself and calling it remediation.
DNS Assets to Audit#
| Asset | Check | Why |
|---|---|---|
| CNAME records | Resolve to live, owned content | Dangling targets invite claims |
| A records for retired hosts | Point somewhere current | Old IPs may be reassigned |
| MX records | Still routed to your mail | Stale MX leaks routing intent |
| TXT records for verification | Still valid tokens | Expired tokens invite asset confusion |
| Subdomain links in docs | Resolve to owned pages | Broken doc links become hijack bait |
Writing the Checker#
A minimal checker needs three inputs: a root, a crawl set, and a list of external domains it is allowed to touch.
import requests from urllib.parse import urljoin, urlparse def check_link(base, href): target = urljoin(base, href) try: r = requests.head(target, allow_redirects=True, timeout=10) except requests.RequestException as e: return ("error", str(e)) return ("ok", r.status_code)
What to record per finding#
- The page that holds the dead reference.
- The target host and status code.
- Whether the target domain is re-registerable.
- The exact text shown to visitors.
False positive shapes#
- 429 or 403 pages that only appear to automation.
- Geo-blocked hosts that are live from elsewhere.
- Temporary outages; require two observations on different days.
- Redirect chains that end at a login page, not a dead end.
Scaling Notes#
- Crawl your own sitemap first, then your docs, then your archived posts.
- Keep a scope file with the exact hosts to check.
- Log to JSONL so deltas are diffable.
- Re-run monthly; link rot is a background process.
# JSONL output line, one per check {"page": "/docs/setup", "target": "https://example.com/old", "status": 404, "ts": "2026-10-01"}
Triage Table#
| Target state | Severity signal | Action |
|---|---|---|
| 404 with claimable service | High | Remove record, file report |
| 404 with unclaimable host | Medium | Remove record |
| 410 gone | Medium | Remove link |
| Redirect to parking page | High | Remove link, note phishing shape |
| TLS error | Low until proven | Verify from a second network |
| Timeout | Low | Recheck twice before recording |
OSINT Pivot Notes#
When a dead reference points at a service that is re-registerable, note the service name and the namespace, not just the status code. The shape of the namespace determines whether a takeover is a technical report or a social-engineering report.
- GitHub and GitLab: repository names.
- Cloud storage: bucket names.
- CDN and app platforms: subdomain names.
- Social platforms: handle names.
Automation Ethics#
Crawling at a modest rate, storing only what you need, and reporting takeover opportunities through the vendor or owner channel keeps the automation inside a testing posture. Registering the dangling name to "prove" the bug is not authorized verification; it is the attack.
Reporting Shape#
Keep each report to one record, one reference, one asset:
Asset: blog.example.com Record type: CNAME Target: your-app.example-cdn.com Observed: target returns a "not found" claim page Risk: visitor content on your subdomain Recommended fix: remove or re-point the record
Common Misses#
- Checking only the homepage.
- Ignoring links inside archived or draft pages.
- Trusting HEAD when the target rate-limits HEADs.
- Forgetting links in PDFs, slide decks, and READMEs.
- Recording a single observation as a finding.
Verification Before You Report#
- Re-check from a different network.
- Capture the response body for the claim page.
- Confirm the record still exists in your DNS.
- Rule out that you re-created the service recently.
- Watch for the record being silently re-pointed between checks.
Limits of the Method#
- Takeover feasibility changes as vendors change claim flows.
- A name that looks claimable from one region may be taken in another.
- Some dead links are harmless content bugs, not security findings.
- Automation finds; humans triage. Do not invert that order.
Internal Map#
- Recon and scanning notes: Metasploit task map.
- Proxy work behind these checks: Burp Suite for beginners.
- Stored sinks found during crawls: stored XSS behavior.
- Hidden sinks in exports and tickets: attacking secondary contexts.
- Cloud references like this: AWS testing notes.
- WordPress dangling plugin links: WordPress attack surface.
- Payloads that behave this way via Unicode: WAF and Unicode notes.
- Exploit stages that begin at reconnaissance: exploit development stages.
- The web classes under all of this: web hacking 101.
- Dead branches in code, not links: dead-branch review is manual.
Ongoing Hygiene#
- Quarterly: re-run the checker, diff against the previous JSONL.
- On every DNS change: re-check the affected records the same week.
- On every service deletion: remove the matching records the same day.
- On every doc update: grep for stale domains before publishing.
Short Notes for the Writeup#
The finding is not "we found a 404". The finding is "a reference we control points at a name that is re-claimable". Keep the two sentences separate in the report, and keep the JSONL evidence attached.
Closing Notes#
Broken links are the background radiation of a site. The few that matter are the ones that still claim your name while pointing nowhere you control. Sweep them on a schedule, record the deltas, and the few real takeovers and hijack risks surface on their own.
A One-Afternoon Scope#
- Export every CNAME from your DNS provider.
- Resolve each one; mark the failures.
- For each failure, identify the claim flow of the target service.
- Cross-reference against your registered assets.
- Remove or re-point anything you do not own.
- Re-run the export next quarter and diff.
Common Shapes of the Mistake#
| Mistake | Why it happens |
|---|---|
| "Temporary" CNAME left for months | No owner after the service was retired |
| Old bucket links in docs | Bucket deleted, docs never updated |
| Old bucket links in onboarding mail | Template kept, asset gone |
| Hostname reused for a new app | Old DNS record left behind |
| Webhook pointing to a deleted app | No cleanup checklist at deletion |
Each row is a dangling claim. Each claim is the attack surface.
Severity Framing for the Report#
- Direct takeover under your domain: highest, because trust is inherited.
- Claimable third-party namespace in an official link: high.
- Dead link to a parked or expired domain: medium.
- Slow or geo-blocked host: low, recheck.
- Truly dead internal link with no hijack path: content bug, not security.
Tools to Consider for the Scope#
- Your DNS provider's export as the asset list.
- A crawler for the outbound link pass.
- A status-recorder that writes JSONL.
- A spreadsheet or ledger to track one owner per record.
Pick the smallest tool that produces the JSONL; fancy tooling does not fix a missing owner.
Closing the Loop#
Assign every DNS record an owner and a deletion checklist. The record survives staff changes only when its reason is written next to it. That single habit removes most dangling references, because the reference was abandoned by silence, not by design.
Reporting Sentinels#
Write each dangling record into the bug tracker with four fields: asset, record, target, observed claim page. One line per record. Link the JSONL evidence. Close only after the record is removed or re-pointed and the claim page no longer responds.
Short Glossary#
- Dangling reference: a pointer from your infrastructure to a name you no longer control.
- CNAME: a DNS record that maps one name to another.
- Takeover: re-claiming a freed name on the original service.
- Hijacking: abusing a dead link or record before you notice it.
- JSONL: one JSON object per line, diffable between runs.
Difference Between This and a Plain 404#
A plain 404 says "this content is gone". A dangling reference says "this name still points at the place, but the place is not ours anymore". The first is a content bug. The second is an invitation. Keep the two finding types separate in the tracker, because their fixes and owners differ.
What Not to Do#
- Do not register the claimable name to prove the point.
- Do not leave parked names under your DNS as a placeholder.
- Do not let automation file raw 404s as security bugs.
- Do not treat a redirect to a login page as a dead end.
- Do not close findings on a single observation.
Final Notes#
Dead links fail slowly, so they need a schedule, not a hero. Put the checker where the DNS change process lives, and the report drafts itself from the deltas.
A Sample Triage Line#
blog.example.com CNAME retired-app.example-cdn.com NXDOMAIN-claim-page recheck 2026-11-01
That single line is the finding: asset, record type, target, observed state, next check date.
A Note on Rate#
Keep checks polite. A few requests per second per host, one user agent string that names your tool, and no hammering of third-party namespaces. The goal is a record, not a load test.
One More Shape to Watch#
Marketing pages and docs pages outlive the services they describe. A link to a retired app looks identical to a link to a live one until you click it, and nobody clicks it except the hijacker and your readers. Treat every external reference in your own pages as suspect until the last crawl proves otherwise. The crawl date is part of the finding, not a footnote. Note it next to every verdict, so a stale verdict is obvious on sight.
The Reporting Habit#
Write the record first, then the fix, then the verification date. That order keeps the report honest: the failure is observed, not argued.
Trust Boundary#
your infra (DNS, docs, READMEs) ---> third-party services you no longer own | | CNAME / link records claimable namespace
Every pointer crossing that boundary is a claim that outlived its resource. The checker re-validates the crossing on a schedule.
Detection#
- CNAME targets that no longer resolve to owned content.
- A records pointing at retired IPs.
- Outbound links in docs that 404.
- MX and TXT records referencing removed services.
Limitations#
- A name that is "not found" can become claimable later; re-check cadence matters.
- NXDOMAIN is not the same as deprovisioned: a service can keep resolving while claiming nothing.
- Only clean up records you own; never register a dangling name yourself.
Cikarilar#
- Dangling references are attack surface, not typos.
- Removal or re-pointing is the fix; claiming the name yourself is not remediation.
- Crawl dates belong in the finding.
- Re-run the audit on a schedule, not once.
What do you think?
React to show your appreciation