- 対象: 自社ドメインの DNS を管理している Web 担当者・サイト運営者・制作会社の方
- 解決すること: 使い終わったサービスを指したままの「放置 CNAME」を見つけて消し、二度と生まれない運用にする
- ゴール: 棚卸しスクリプト → 生死判定 → 撤去順序 → 所有確認 TXT → 週次監視 までコピペで揃える
はじめに:サブドメイン乗っ取りとは
キャンペーンサイトを GitHub Pages で公開し、campaign.example.com を CNAME で向けていたとします。キャンペーン終了後にリポジトリだけ削除し、DNS の CNAME を消し忘れると、DNS 上は「自社のサブドメイン」なのに、行き先には誰もいない状態になります。これを dangling DNS(ぶら下がった DNS レコード)と呼びます。
行き先が「名前を後から誰でも取得できる」種類のサービス(静的ホスティング・クラウドストレージ・PaaS など)だと、第三者が同じ名前でリソースを作り、自社サブドメインの上に任意のコンテンツを載せられる可能性があります。これがサブドメイン乗っ取り(subdomain takeover)です。
MDN・Microsoft Learn・AWS の公式ドキュメントも同じ構図を注意喚起しています(AWS は「対応するバケットが無い S3 エンドポイントを CNAME で指していると、どの AWS ユーザーでも同名バケットを作ってコンテンツを公開できる」と明記)。
- MDN: https://developer.mozilla.org/en-US/docs/Web/Security/Attacks/Subdomain_takeover
- Microsoft Learn: https://learn.microsoft.com/en-us/azure/security/fundamentals/subdomain-takeover
- AWS: https://docs.aws.amazon.com/AmazonS3/latest/userguide/VirtualHosting.html
何が起きるのか
Microsoft Learn が挙げている被害は次のとおりです。
| 被害 | 内容 |
|---|---|
| コンテンツの乗っ取り | 自社サブドメインに第三者のページが表示され、ブランド毀損につながる |
| Cookie の窃取 | 親ドメイン(.example.com)向けに発行した Cookie はサブドメインにも送られる |
| フィッシング | 本物のドメイン名なので見分けにくい。有効な SSL 証明書も取得されうる |
| メールの受信 | MX レコードが残っていると、そのサブドメイン宛メールを受け取られうる |
「HTTPS だから大丈夫」は通りません。乗っ取った側はそのサブドメインの正規の証明書を取得できるため、鍵マークも付きます。
以下、自社のドメインを対象に「見つける → 消す → 生まれないようにする → 見張る」の順で 5 つの対策を示します。
対策1:サブドメインを全件棚卸しする
まず「自社のサブドメインが何件あって、それぞれどこを向いているか」を一覧にします。担当者の記憶ではなく、DNS の実データから出すのがポイントです。
Cloudflare の場合
API トークンは Zone → DNS → Read(読み取りのみ) の権限で発行してください。
#!/usr/bin/env bash
# dns_inventory_cf.sh — Cloudflare のゾーンから全レコードを TSV に書き出す
# 使い方: CF_API_TOKEN=xxx ZONE_ID=yyy ./dns_inventory_cf.sh > dns_inventory.tsv
set -euo pipefail
page=1
while :; do
resp=$(curl -fsS "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/dns_records?per_page=100&page=${page}" \
-H "Authorization: Bearer ${CF_API_TOKEN}")
echo "$resp" | jq -r '.result[] | [.type, .name, .content, (.proxied|tostring)] | @tsv'
total=$(echo "$resp" | jq -r '.result_info.total_pages')
[ "$page" -ge "$total" ] && break
page=$((page + 1))
done
Amazon Route 53 の場合
#!/usr/bin/env bash
# dns_inventory_r53.sh — Route 53 のホストゾーンから CNAME とエイリアスを TSV に書き出す
# 使い方: HOSTED_ZONE_ID=Zxxxx ./dns_inventory_r53.sh > dns_inventory.tsv
set -euo pipefail
aws route53 list-resource-record-sets --hosted-zone-id "$HOSTED_ZONE_ID" --output json \
| jq -r '.ResourceRecordSets[]
| select(.Type == "CNAME" or .AliasTarget != null)
| [(if .AliasTarget then "ALIAS" else .Type end),
(.Name | rtrimstr(".")),
((.ResourceRecords[0].Value // .AliasTarget.DNSName) | rtrimstr(".")),
"-"] | @tsv'
管理画面しかない DNS(レンタルサーバー等)の場合
ゾーンのエクスポート機能を使います。あわせて過去に証明書を発行したホスト名を Certificate Transparency(証明書の公開ログ)から拾うと、忘れられたサブドメインが見つかることがあります(自社ドメインに限って実行)。
# 自社ドメインで過去に証明書が発行されたホスト名の一覧(重複除去)
curl -fsS "https://crt.sh/?q=%25.example.com&output=json" \
| jq -r '.[].name_value' | tr 'A-Z' 'a-z' | sed 's/^\*\.//' | sort -u
出力した一覧には、「誰が・何のために作ったか(オーナー)」列を追記しておきます。オーナー不明のレコードが一番危険です。
対策2:CNAME の行き先が「生きているか」を判定する
棚卸しした TSV(1 列目=種類、2 列目=名前、3 列目=行き先)を読み、外部サービスを指しているレコードの生死を機械的に確認します。
#!/usr/bin/env bash
# dangling_check.sh — 自社ゾーンの CNAME/ALIAS の行き先を点検する
# 使い方: ./dangling_check.sh dns_inventory.tsv
set -uo pipefail
printf 'name\ttarget\tdns_status\thttp\tverdict\n'
while IFS=$'\t' read -r type name target _; do
case "$type" in CNAME|ALIAS) ;; *) continue ;; esac
target="${target%.}"
# 行き先の名前が DNS で解決できるか(NXDOMAIN なら行き先が消えている)
dns_status=$(dig +noall +comments "$target" A | grep -o 'status: [A-Z]*' | awk '{print $2}')
# サブドメイン自体に HTTPS でアクセスしたときの応答コード(000 は接続・TLS 失敗)
http=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 "https://${name}/" || true)
verdict="OK"
if [ "$dns_status" = "NXDOMAIN" ]; then
verdict="DANGER(行き先が存在しない)"
elif [ "$http" = "404" ] || [ "$http" = "000" ]; then
verdict="要確認(応答なし/404)"
fi
printf '%s\t%s\t%s\t%s\t%s\n' "$name" "$target" "$dns_status" "$http" "$verdict"
done < "${1:?TSVファイルを指定してください}"
DANGER は行き先の名前自体が消えているので最優先で DNS レコードを削除、要確認 は外部サービスの管理画面で自社アカウントにリソースが残っているかを確認します。200 でも自社のものとは限らないため、最終判断は「そのリソースが自社アカウントに存在するか」を管理画面で確認して行ってください。また、解約したクラウドの IP を指したままの A レコードや使わなくなった MX / NS レコードも同じ問題を起こすので、対策1の一覧でオーナー確認の対象にします。
対策3:撤去と構築の「順序」を手順書に固定する
放置 CNAME は、ほぼ例外なく撤去作業の順番ミスから生まれます。MDN と Microsoft Learn はどちらも次の順序を推奨しています。
- 構築するとき: 先に外部サービス側でリソースを確保し、DNS レコードは最後に作る
- 撤去するとき: DNS レコードを最初に消し、そのあとで外部サービスのリソースを削除する
撤去手順書には次の 4 行を入れておきます。
1. DNS の CNAME / A / MX / TXT(所有確認用含む)を削除する
2. dig +short campaign.example.com が空になったことを確認する
3. 外部サービスのリソース(リポジトリ・バケット・アプリ)を削除する
4. 棚卸し台帳の行と、コードに残る旧 URL への参照(OAuth のリダイレクト先など)を消す
対策4:外部サービスの「ドメイン所有確認」を必ず有効にする
主要なサービスには、ドメインの持ち主だけが設定できる所有確認の仕組みがあります。有効にしておくと、CNAME が残っていても第三者がそのドメインを自分のリソースに紐づけられなくなります。
| サービス | 所有確認の方法 |
|---|---|
| GitHub Pages |
_github-pages-challenge-<ユーザー名 or Organization 名>.example.com に TXT レコード |
| Azure App Service |
asuid.<サブドメイン> に TXT レコード(ドメイン検証 ID) |
| Amazon S3(静的サイト) | バケット名=ホスト名の仕組みのため、バケットを消すなら CNAME も必ず消す |
設定されているかは dig で確認できます。
# GitHub Pages のドメイン所有確認(YOUR-ORG は自社のアカウント名に置き換え)
dig +short TXT "_github-pages-challenge-YOUR-ORG.example.com"
# Azure App Service のドメイン所有確認
dig +short TXT "asuid.www.example.com"
# ワイルドカードレコードの有無(存在しないはずの名前が解決したら * レコードがある)
dig +short "no-such-host-$(date +%s).example.com"
最後の確認で IP や CNAME が返ってきたら、ワイルドカード(*.example.com)レコードが設定されています。GitHub のドキュメントは、ワイルドカードは所有確認をしていても乗っ取りのリスクがあるとして使わないよう強く勧めています。外部サービスへ向けたワイルドカード CNAME は、個別のレコードに置き換えてください。
- GitHub Docs: Verifying your custom domain for GitHub Pages
https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/verifying-your-custom-domain-for-github-pages
対策5:週次で自動点検し、Cookie の送信範囲を絞る
5-1. 週次の自動点検
対策1・2のスクリプトを組み合わせ、結果に DANGER / 要確認 があればメールで知らせます。
#!/usr/bin/env bash
# /opt/dnscheck/run_weekly.sh — 棚卸し + 生死判定を実行し、要対応があれば通知する
set -euo pipefail
cd /opt/dnscheck
set -a; . ./env; set +a # CF_API_TOKEN / ZONE_ID / MAILTO を記載(chmod 600)
./dns_inventory_cf.sh > dns_inventory.tsv
./dangling_check.sh dns_inventory.tsv > result.tsv
if grep -qE 'DANGER|要確認' result.tsv; then
grep -E 'DANGER|要確認' result.tsv | mail -s "[DNS点検] 要対応のサブドメインがあります" "$MAILTO"
fi
# cron 登録例(/etc/cron.d/dnscheck): 毎週月曜 6:15
# 15 6 * * 1 root /opt/dnscheck/run_weekly.sh >> /var/log/dnscheck.log 2>&1
新しく作ったサブドメインも翌週には自動で点検対象に入ります。
5-2. Cookie を親ドメインに発行しない
乗っ取られたときの被害を小さくするため、ログイン Cookie は必要なホストだけに送られるようにしておきます。Domain 属性を付けない Cookie は発行したホストにだけ送られ、サブドメインには送られません。
<?php
// Domain を指定しない(=このホスト限定)。__Host- 接頭辞は Secure・Path=/・Domain なしを強制する
setcookie('__Host-session', $sessionId, [
'expires' => 0,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
WordPress では wp-config.php に COOKIE_DOMAIN を .example.com のような親ドメインで明示していないか確認してください。サブドメイン間でログインを共有する必要がなければ、この定数は設定しないのが安全です。
もし乗っ取りを見つけたら
Microsoft Learn の是正手順に沿って、①DNS レコードを削除(または自社リソースへ向け直し)②認証情報や個人情報がそのサブドメインへ送られる経路がなかったか確認し、可能性があれば Cookie・トークンを失効 ③撤去時に DNS が消されなかった理由を対策3の手順書に反映、の順で対応します。
まとめ
| # | 対策 | 目的 |
|---|---|---|
| 1 | サブドメインの全件棚卸し(API / CT ログ) | 忘れられたレコードを見つける |
| 2 | CNAME の行き先の生死判定 | 放置 CNAME を機械的に洗い出す |
| 3 | 「DNS は最後に作り、最初に消す」順序の固定 | 放置 CNAME を生まない |
| 4 | 外部サービスの所有確認 TXT + ワイルドカード廃止 | 残っても第三者に紐づけさせない |
| 5 | 週次の自動点検 + Cookie のホスト限定 | 見張り続け、被害範囲を小さくする |
サブドメイン乗っ取りは、高度な攻撃ではなく撤去作業の抜けから起きます。まずは対策1の棚卸しを 1 回実行し、オーナー不明のレコードが無いか確認するところから始めてみてください。
関連記事
- Cookieのセキュリティ属性 SameSite / Secure / HttpOnly の正しい付け方
- Cloudflare配下でもオリジンIP直アクセスで素通り──塞ぐ5つの設定
- SSL証明書切れを防ぐ5つの対策──certbot自動更新の確認と期限監視
本記事のような設定の抜けは、Web サイトを 9 つの守りでまるごと守るサイトドックでまとめて対策できます → https://sitedock.jp