Skip to content

x509util: fix SSRF hostname bypass in rejectPrivateHost() - #1776

Closed
MrINVISO wants to merge 2 commits into
google:masterfrom
MrINVISO:fix/ssrf-hostname-bypass
Closed

x509util: fix SSRF hostname bypass in rejectPrivateHost()#1776
MrINVISO wants to merge 2 commits into
google:masterfrom
MrINVISO:fix/ssrf-hostname-bypass

Conversation

@MrINVISO

Copy link
Copy Markdown

Summary

PR #1761 introduced rejectPrivateHost() to fix SSRF in x509util/files.go, but the fix is incomplete. It only blocks private IP addresses via net.ParseIP(), which returns nil for hostnames. DNS hostnames resolving to private or loopback addresses bypass the fix entirely.

Root Cause

func rejectPrivateHost(u *url.URL) error {
    host := u.Hostname()
    ip := net.ParseIP(host)
    if ip == nil {
        return nil // hostname bypass here
    }

Bypasses Confirmed

URL Result
http://localhost/ ALLOWED (resolves to 127.0.0.1)
http://metadata.google.internal/ ALLOWED (resolves to 169.254.169.254 on GCP)
http://kubernetes.default.svc/ ALLOWED (resolves to 10.x.x.x in k8s)
http://127.0.0.1/ BLOCKED (fix works for IP)

Fix

Resolve hostname via net.LookupHost() before IP validation.

Changes

  • Fix rejectPrivateHost() to resolve hostnames before checking
  • Apply rejectPrivateHost() to ReadFileOrURL() and GetIssuer()
  • Add defer rsp.Body.Close() to prevent resource leaks
  • Add tests for hostname bypass

Testing

=== RUN TestRejectPrivateHost
--- PASS: TestRejectPrivateHost (0.01s)
=== RUN TestReadFileOrURL_SSRFBypass
--- PASS: TestReadFileOrURL_SSRFBypass (0.00s)
=== RUN TestReadFileOrURL_PublicURL
--- PASS: TestReadFileOrURL_PublicURL (0.09s)
PASS

Related: #1759
Fixes incomplete fix in: #1761

@MrINVISO
MrINVISO requested a review from a team as a code owner March 16, 2026 13:04
@MrINVISO
MrINVISO requested review from mhutchinson and removed request for a team March 16, 2026 13:04
@google-cla

google-cla Bot commented Mar 16, 2026

Copy link
Copy Markdown

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

PR google#1761 introduced rejectPrivateHost() to block SSRF via
private IP addresses, but only checked net.ParseIP() which
returns nil for hostnames. DNS hostnames resolving to private
or loopback addresses bypassed the fix entirely.

This change resolves hostnames via net.LookupHost() before
IP validation, blocking bypasses such as:
  - localhost → 127.0.0.1 (loopback)
  - metadata.google.internal → 169.254.169.254 (GCP)
  - kubernetes.default.svc → 10.x.x.x (k8s)

Also adds:
  - rejectPrivateHost() to ReadFileOrURL() and GetIssuer()
  - defer rsp.Body.Close() to prevent resource leaks
  - Tests for hostname bypass and SSRF protection

Fixes incomplete fix in PR google#1761 (Issue google#1759)
@MrINVISO
MrINVISO force-pushed the fix/ssrf-hostname-bypass branch from d39a98c to d87e609 Compare March 16, 2026 13:11
@MrINVISO

Copy link
Copy Markdown
Author

This PR extends the fix from #1761 by also resolving hostnames before IP validation.

Gap in #1761: rejectPrivateHost() returns nil for any non-IP hostname, allowing bypasses such as:

  • http://localhost/ → resolves to 127.0.0.1 (loopback)
  • http://metadata.google.internal/ → resolves to 169.254.169.254 on GCP
  • http://kubernetes.default.svc/ → resolves to 10.x.x.x in k8s

Validated PoC:

BLOCKED : http://127.0.0.1/token (PR #1761 fix works)
ALLOWED : http://localhost/token → {"access_token":"EXFILTRATED"} (bypass)

This PR adds net.LookupHost() resolution before the IP check, closing the hostname bypass while preserving the same behavior for public hostnames.

@MrINVISO MrINVISO closed this Mar 19, 2026
@MrINVISO

Copy link
Copy Markdown
Author

Update: Google Bug Hunter Team reviewed this via issuetracker and marked as Infeasible (below security escalation threshold), but noted the issue is valid and suggested public disclosure. The fix in this PR remains valuable for hardening purposes.

@MrINVISO MrINVISO reopened this Mar 19, 2026
@MrINVISO MrINVISO closed this Apr 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant