gcp-clouddns-dangling-cname-takeover
A CNAME (or ALIAS) resource record set in a public Cloud DNS zone whose target resolves to a GCP-managed FQDN suffix that no longer has a backing resource in collected GCP inventory enables subdomain takeover: an external attacker can claim that resource name and inherit DNS resolution for the dangling hostname.
where
emit
| source type | DNS |
|---|---|
| target type | AnonymousIdentity |
| source | <public Cloud DNS zone containing the dangling CNAME record> |
| target | <AnonymousIdentity> |
| state logic | ACTIVE when: (1) the zone is public (visibility == public), AND (2) the CNAME target maps to a known GCP-claimable FQDN suffix, AND (3) the backing resource is confirmed absent from all collected GCP projects. POTENTIAL when the CNAME target project is outside collection scope (absence may be a collection gap rather than true deprovisioning) — downgrade to POTENTIAL(service_state) in that case. Do not emit for private zones (visibility == private): private zone CNAMEs are not internet-resolvable and do not yield external subdomain takeover. |
Narrative
DNS zone {zone.name} (project {zone.project}) contains a public CNAME record {record.name} pointing to {record.cname_value}, which targets a GCP-managed hostname suffix whose backing resource ({target_resource_type}) was not found in collected GCP inventory. An external attacker can claim that resource name and inherit public DNS resolution for {record.name}, enabling subdomain takeover (content injection, cookie theft, OAuth redirect URI abuse, TLS certificate acquisition via DNS-01 ACME challenge).
Raw rule rules/derived/gcp/clouddns.yaml
id: gcp-clouddns-dangling-cname-takeover
emits: ExposedToInternet
description: 'A CNAME (or ALIAS) resource record set in a public Cloud DNS zone whose target resolves
to a GCP-managed FQDN suffix that no longer has a backing resource in collected GCP inventory enables
subdomain takeover: an external attacker can claim that resource name and inherit DNS resolution for
the dangling hostname.'
where:
- The managed zone has visibility == 'public'
- The RRset type is CNAME (or an alias-target A/AAAA record whose value is a GCP-managed FQDN)
- "The CNAME value (rrdatas[0]) ends in a known GCP-claimable suffix: *.appspot.com, *.run.app, *.cloudfunctions.net,\
\ *.a.run.app, *.googleapis.com (only storage.googleapis.com sub-domains), *.googleusercontent.com,\
\ *.web.app, *.firebaseapp.com, *.cloudstorage.googleapis.com (partial list \u2014 verify against current\
\ GCP guidance)"
- The backing resource (App Engine app, Cloud Run service, Cloud Functions function, GCS bucket at that
hostname, Firebase project) does NOT exist in collected GCP inventory across all projects in scope
emit:
source_type: DNS
target_type: AnonymousIdentity
source: <public Cloud DNS zone containing the dangling CNAME record>
target: <AnonymousIdentity>
permissions: []
state_logic: "ACTIVE when: (1) the zone is public (visibility == public), AND (2) the CNAME target maps\
\ to a known GCP-claimable FQDN suffix, AND (3) the backing resource is confirmed absent from all\
\ collected GCP projects. POTENTIAL when the CNAME target project is outside collection scope (absence\
\ may be a collection gap rather than true deprovisioning) \u2014 downgrade to POTENTIAL(service_state)\
\ in that case. Do not emit for private zones (visibility == private): private zone CNAMEs are not\
\ internet-resolvable and do not yield external subdomain takeover."
derived_from:
- '<dns.resourceRecordSets.list: zone + RRset type=CNAME + rrdatas value>'
- '<GCP resource inventory cross-reference: target resource not found in any collected project>'
false_positive_note: "Only emit for confirmed GCP-claimable FQDN suffixes. A CNAME pointing to a customer-owned\
\ hostname (non-GCP suffix, e.g. *.example.com) is outside GCP's claimable surface and must not trigger\
\ this rule. An App Engine service or Cloud Run service that still exists in a project NOT in collection\
\ scope looks identical to a deprovisioned resource \u2014 always downgrade to POTENTIAL when the\
\ target project is uncollected. GCS bucket hostnames (BUCKET.storage.googleapis.com) are claimable\
\ only if the bucket name is available; verify against GCS bucket existence checks, not just project\
\ scope. Firebase Hosting hostnames (PROJECT.web.app, PROJECT.firebaseapp.com) are claimable if the\
\ Firebase project is deprovisioned. Do not emit ACTIVE when collection coverage is incomplete across\
\ GCP projects."
narrative: DNS zone {zone.name} (project {zone.project}) contains a public CNAME record {record.name}
pointing to {record.cname_value}, which targets a GCP-managed hostname suffix whose backing resource
({target_resource_type}) was not found in collected GCP inventory. An external attacker can claim
that resource name and inherit public DNS resolution for {record.name}, enabling subdomain takeover
(content injection, cookie theft, OAuth redirect URI abuse, TLS certificate acquisition via DNS-01
ACME challenge).