Repository navigation
sec(httpclient): resolve hostname before IMDS-block check (closes DNS-bypass gap) #1324
Description
Activity
- addedtriagedItem has been triagedItem has been triagedpriority/p3Polish / idea / may never shipPolish / idea / may never shipseverity/lowMinor harmMinor harmurgency/eventuallyNo deadlineNo deadlineimpact/internalTeam-internal onlyTeam-internal onlyeffort/sHoursHourstype/securitySecurity findingSecurity finding
on Jun 26, 2026 Reviewed commit:
be11bdcb5. Note:origin/mainmoved to3e9660d06during the review; re-verify against currentmainbefore changing code, since a finding may have been fixed or moved.The 2026-07-28 full-repo review re-derived this mechanism independently. The mechanism described here is correct; the scope and the severity assessment are now out of date and, left as-is, will cause this to be picked up as a small defence-in-depth chore.
Where the scope has moved
This issue lists only:
internal/secrets/azure_resolver.go:32-41providers/azure/internal/httpclient/httpclient.go:26-35
The same defect is now in
pkg/httpclient/httpclient.go:41-50, which is the shared implementation that backs the Azure service clients inproviders/azure/services/*/client.go, the Key Vault resolver behind the credential encryption key (internal/credentials/cipher.go:106), and the scheduled-task JWKS fetch. The package header states it is "the single shared implementation".Why the severity assessment no longer holds
The issue currently argues low practical risk on two grounds:
- The Azure Key Vault URL comes from env var
AZURE_KEY_VAULT_URL; attacker control of that env var is already an operational compromise. - The
azsecrets/azcoreHTTP pipeline does not follow redirects by default...
Both are specific to the Key Vault resolver. They do not hold for
pkg/httpclient, whose callers include request-tainted URL paths. And the bypass needs neither an attacker-controlled DNS record nor a redirect:metadata.google.internalis a stable, publicly documented name resolving to169.254.169.254, sohttp://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/tokenpasses the check and returns a service-account token. That is a straightforward credential-exfiltration primitive, not a rebinding race.
Current labels are
priority/p3,severity/low,urgency/eventually,impact/internal. Suggest re-triaging in line with LeanerCloud/cloud-commitments-go#20 (priority/p1,severity/high).One correction to the recommended patch
The snippet in this issue resolves the host, checks the resolved IPs, and then dials the original
addr:return d.inner.DialContext(ctx, network, addr)
That reintroduces a rebinding window: the resolver call and the dial each perform their own lookup, so a short-TTL record can answer benignly for the check and with the metadata address for the connect. Dial the validated IP literal instead, and validate against CIDR ranges rather than the two-entry map.
Also missing from the map in every copy:
169.254.170.2(ECS task metadata, container credentials) and169.254.170.23/fd00:ec2::23(EKS Pod Identity).Related
- pkg/httpclient: extend SSRF block-list (loopback, RFC 1918, all link-local, hostname/DNS resolution) cloud-commitments-go#20 (
pkg/httpclient: extend SSRF block-list) is the P1 sibling; its fix sketch already contains the resolve-then-dial-the-literal step. - chore(secrets): extract IMDS-blocking transport into shared pkg/httpclient cloud-commitments-go#25 (extract the IMDS transport into the shared package) removes the need to apply this fix in three places.
Duplicate of LeanerCloud/cloud-commitments-go#48: that issue covers the same hostname-not-resolved-IP gap in the same blockIMDSDialer.DialContext (pkg/httpclient/httpclient.go:41 and the internal/secrets copy), re-frames it at severity/high P1 instead of P3 defence-in-depth, and its fix closes this one.
- addedduplicateThis issue or pull request already existsThis issue or pull request already exists
on Sep 2, 2026
Severity / Confidence
P3, medium confidence. Defense-in-depth gap. Not exploitable in current code paths but worth closing.
Affected files
internal/secrets/azure_resolver.go:32-41(blockIMDSDialer.DialContext)providers/azure/internal/httpclient/httpclient.go:26-35(same pattern)Evidence
Both IMDS-blocking dialers check
hostfromnet.SplitHostPort(addr)against a map of literal IPs (169.254.169.254,fd00:ec2::254). They do NOT resolve hostnames before checking.net.Dialer.DialContextis invoked byhttp.Transportwithaddrashost:port, wherehostcan be a hostname or IP literal. DNS resolution happens insideinner.DialContext. So if a request targets a hostname that resolves to an IMDS address (for example, the GCP conventionmetadata.google.internal->169.254.169.254, or an attacker-controlled DNS record), the application-layer check atinternal/secrets/azure_resolver.go:37does not fire.In current code the practical risk is low:
AZURE_KEY_VAULT_URL; attacker control of that env var is already an operational compromise.azsecrets/azcoreHTTP pipeline does not follow redirects by default, so a malicious redirect response cannot pivot to an arbitrary host.But this is the SSRF defense the memory
feedback_azure_use_httpclient_new.mdsays is the whole point ofhttpclient.New(), and it currently only catches the literal-IP form of the attack.Recommendation
Resolve the host before the IMDS check, e.g.:
Add a regression test that uses a
net.ResolverwhoseDialreturns a stub DNS response mappingmetadata.google.internal->169.254.169.254, and asserts the block fires.Best landed together with the shared-package refactor in the duplication follow-up so both
internal/secrets/azure_resolver.goandproviders/azure/internal/httpclient/httpclient.goget the hardening at once.Triage labels
type/security,priority/p3,severity/low,urgency/eventually,impact/internal,effort/s,triaged.Source: PR #1224 adversarial review.