# Subdomain Takeover Checker: Dangling DNS & Service Fingerprints (`sysadmin-toolbox/subdomain-takeover-checker`) Actor

Check hostnames for dangling CNAMEs and takeover-prone hosting fingerprints (GitHub Pages, Heroku, S3, Azure, Shopify, Netlify, Vercel…). Passive DNS-over-HTTPS plus a plain HTTP(S) GET. Clear verdicts, evidence snippets, bulk input, optional Certificate Transparency discovery.

- **URL**: https://apify.com/sysadmin-toolbox/subdomain-takeover-checker.md
- **Developed by:** [SysAdmin Toolbox](https://apify.com/sysadmin-toolbox) (community)
- **Categories:** Developer tools, SEO tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.00 / 1,000 host checkeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Subdomain Takeover Checker: Dangling DNS & Service Fingerprints

Check hostnames for **dangling CNAMEs** and **takeover-prone hosting fingerprints** (GitHub Pages, Heroku, AWS S3, Azure, Shopify, Netlify, Vercel, and dozens more). The actor is **passive only**: DNS-over-HTTPS lookups plus a plain HTTP(S) GET of the hostname. It never registers, claims or exploits anything.

Use it to:

- **Find forgotten subdomains** that still point at a SaaS or cloud host you cancelled.
- **Bulk-check** a list of hosts after an acquisition, MSP onboarding or DNS clean-up.
- **Pull candidate names from Certificate Transparency** when you only know the apex domain.
- Get a **clear verdict with evidence**, not a vague "maybe vulnerable" score.

### Why this one

- **Accuracy first.** Fingerprints carry the source project's status (`vulnerable` / `edge case` / `not vulnerable`), so the actor does not overclaim. Edge-case services (GitHub Pages, Heroku, Netlify, Shopify…) are reported as `POSSIBLE`, not `VULNERABLE`.
- **Clear evidence.** Every finding includes the CNAME chain, the matched fingerprint string (or `NXDOMAIN`), a short body snippet, and a concrete recommendation.
- **Bulk-friendly input.** Paste hostnames, URLs or email addresses. Optional Certificate Transparency discovery (Cert Spotter, crt.sh fallback) expands an apex into subdomains.
- **Honest DNS.** NXDOMAIN, empty NOERROR and SERVFAIL/timeouts are kept separate. Failed lookups are retried, then sent to a second public resolver.

### Passive method (what it does and does not do)

1. Resolve the **CNAME chain** for each hostname over DNS-over-HTTPS (Cloudflare, Google fallback).
2. Match the CNAME target against a maintained fingerprint list of takeover-prone services.
3. Optionally **GET** `https://` and `http://` the hostname (short timeout, small body, few redirects, honest User-Agent naming this actor) and match the provider's "not found / unclaimed" page text or status.
4. Emit a verdict with evidence.

It does **not** register domains, claim SaaS projects, send exploit payloads, or probe anything beyond DNS and a plain GET. **Only run it on domains you own or are authorised to test.** Every result row repeats that disclaimer.

### Fingerprint source

Fingerprints are based on the public [can-i-take-over-xyz](https://github.com/EdOverflow/can-i-take-over-xyz) project's `fingerprints.json` (commit `5bd4e128`, dated **2025-02-08**, fetched 2026-10-16). Each service keeps that project's status. A few CNAME suffixes were added so the actor can identify the service from the target alone (for example `github.io`, `herokuapp.com`, `azurewebsites.net`, `trafficmanager.net`).

### Input

```json
{
  "hostnames": ["www.example.com", "blog.example.com", "https://docs.example.net/"],
  "discoverFromCt": false,
  "httpCheck": true,
  "onlyFindings": false
}
```

| Field | Default | Notes |
| --- | --- | --- |
| `hostnames` | required | Hostnames, URLs or emails. One per line, or comma/space separated; `#` starts a comment. Duplicates removed; invalid lines listed in `SUMMARY`. Up to 10,000 per run. |
| `discoverFromCt` | false | Pull extra hostnames for each apex from Certificate Transparency (Cert Spotter → crt.sh) |
| `maxSubdomainsPerDomain` | 200 | Cap on CT names added per apex (1–2000) |
| `httpCheck` | true | GET http/https and match body/status fingerprints |
| `onlyFindings` | false | Dataset only gets VULNERABLE / LIKELY / POSSIBLE / DANGLING rows |
| `concurrency` | 20 | Lookups in flight (max 100) |
| `dnsResolver` | `cloudflare` | `cloudflare` or `google`; the other is the fallback |
| `timeoutSecs` | 5 | Per DNS and HTTP request |

### Output

One dataset row per hostname. Views: **Overview** and **Findings only**.

```json
{
  "host": "old.example.com",
  "verdict": "VULNERABLE",
  "service": "Microsoft Azure",
  "serviceStatus": "vulnerable",
  "cnameChain": ["dead.azurewebsites.net"],
  "finalTarget": "dead.azurewebsites.net",
  "dnsStatus": "NXDOMAIN",
  "httpStatus": null,
  "matchedFingerprint": "NXDOMAIN",
  "matchedBy": "cname-nxdomain",
  "evidence": "CNAME target dead.azurewebsites.net is NXDOMAIN (Microsoft Azure)",
  "recommendation": "Confirm the finding, then either remove the dangling DNS record…",
  "disclaimer": "Results need manual confirmation. Only act on domains you own or are authorised to test…"
}
```

#### Verdict meanings

| Verdict | Meaning |
| --- | --- |
| `VULNERABLE` | Fingerprint matched on a service listed **vulnerable** (HTTP body/status, or dangling CNAME NXDOMAIN on an nxdomain-signal service such as Azure / Elastic Beanstalk). |
| `LIKELY` | Dangling CNAME (target NXDOMAIN) to a takeover-prone service that is vulnerable or edge-case, without a confirming HTTP fingerprint. |
| `POSSIBLE` | Edge-case service fingerprint matched (GitHub Pages, Heroku, Netlify, Shopify, Vercel…). Often not trivially claimable. |
| `DANGLING` | CNAME target is NXDOMAIN; provider unknown. |
| `OK` | Resolves and no takeover fingerprint matched (or a not-vulnerable fingerprint matched). |
| `NO_RECORD` | Hostname is NXDOMAIN or has no records. |
| `ERROR` | DNS or HTTP lookup failed after retries. |

The key-value store record **`SUMMARY`** has verdict counts, a findings list, CT discovery notes, invalid inputs and DoH statistics.

### Pricing

Pay per event: **$2 per 1,000 hostnames** (`host-checked` at $0.002 each). Invalid and duplicate inputs are not charged. CT discovery names that are checked are charged like any other hostname.

### Limits

- Fingerprints and statuses can change; always **confirm manually** before removing DNS or reclaiming a resource.
- HTTP checks use a short timeout and a small body cap; a slow or huge error page may be missed.
- Certificate Transparency is not a complete inventory of every subdomain.
- Apify cloud has no IPv6 egress; hosts that only answer on IPv6 may look like errors.
- A records pointing at known cloud IP ranges (without a CNAME) are **out of scope** and are not flagged.

### FAQ

**Is a POSSIBLE finding safe to ignore?**
No. It means an edge-case provider page matched. Confirm with that provider's docs; many require an extra verification step before a name can be claimed.

**Why is GitHub / Heroku / Netlify not VULNERABLE?**
The fingerprint source lists them as edge cases (extra steps or provider mitigations). This actor follows that classification.

**Can I discover subdomains automatically?**
Yes: set `discoverFromCt` to true. The actor asks Cert Spotter (and crt.sh if needed) for names under each apex in your list, then checks them.

**Does this claim anything?**
No. DNS lookups and a GET only. Claiming or registering resources is intentionally out of scope.

### Related

- [Bulk SPF / DMARC / MX Auditor](https://apify.com/sysadmin-toolbox/email-security-audit)
- [Bulk DKIM Selector Finder](https://apify.com/sysadmin-toolbox/dkim-selector-finder)
- [Bulk SSL Certificate Expiry Monitor](https://apify.com/sysadmin-toolbox/bulk-ssl-expiry-monitor)
- [Microsoft 365 Tenant Finder](https://apify.com/sysadmin-toolbox/m365-tenant-finder)

# Actor input Schema

## `hostnames` (type: `array`):

Hostnames to check. One per line (comma- or space-separated lists work too). URLs and email addresses are reduced to their host (https://www.example.com/page, user@example.com). Duplicates are removed; invalid entries are listed in SUMMARY. Max 10,000 hostnames per run.

## `discoverFromCt` (type: `boolean`):

For each apex domain in the hostname list, pull additional hostnames from Certificate Transparency (Cert Spotter, with crt.sh fallback). Useful when you only know the apex and want forgotten subdomains. Cap per apex is maxSubdomainsPerDomain.

## `maxSubdomainsPerDomain` (type: `integer`):

Only used when discoverFromCt is on. Caps how many CT names are added per apex domain.

## `httpCheck` (type: `boolean`):

GET http:// and https:// for each hostname (short timeout, small body, few redirects) and match provider 'not found / unclaimed' page fingerprints. Turn off for DNS-only dangling-CNAME checks.

## `onlyFindings` (type: `boolean`):

If on, the dataset only gets VULNERABLE / LIKELY / POSSIBLE / DANGLING rows. OK, NO_RECORD and ERROR are still counted in SUMMARY.

## `concurrency` (type: `integer`):

DNS-over-HTTPS and HTTP requests in flight at once.

## `dnsResolver` (type: `string`):

Public resolver used for lookups. SERVFAIL, timeouts and HTTP errors are retried and then sent to the other resolver.

## `timeoutSecs` (type: `integer`):

Per DNS-over-HTTPS and HTTP request.

## Actor input object example

```json
{
  "hostnames": [
    "www.example.com",
    "blog.example.com",
    "docs.github.io"
  ],
  "discoverFromCt": false,
  "maxSubdomainsPerDomain": 200,
  "httpCheck": true,
  "onlyFindings": false,
  "concurrency": 20,
  "dnsResolver": "cloudflare",
  "timeoutSecs": 5
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `summary` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "hostnames": [
        "www.example.com",
        "blog.example.com",
        "docs.github.io"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("sysadmin-toolbox/subdomain-takeover-checker").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "hostnames": [
        "www.example.com",
        "blog.example.com",
        "docs.github.io",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("sysadmin-toolbox/subdomain-takeover-checker").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "hostnames": [
    "www.example.com",
    "blog.example.com",
    "docs.github.io"
  ]
}' |
apify call sysadmin-toolbox/subdomain-takeover-checker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,sysadmin-toolbox/subdomain-takeover-checker"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/DDON1qB1CQ4oiRnQV/builds/AdQASRw8JZcp4Hupr/openapi.json
