Skip to content

fix(web-fetch): resolve DNS before SSRF guard to block hostname-to-private-IP bypass#27744

Closed
herdiyana256 wants to merge 2 commits into
google-gemini:mainfrom
herdiyana256:fix/ssrf-dns-hostname-private-ip-bypass
Closed

fix(web-fetch): resolve DNS before SSRF guard to block hostname-to-private-IP bypass#27744
herdiyana256 wants to merge 2 commits into
google-gemini:mainfrom
herdiyana256:fix/ssrf-dns-hostname-private-ip-bypass

Conversation

@herdiyana256

@herdiyana256 herdiyana256 commented Jun 8, 2026

Copy link
Copy Markdown

Summary

  • Root cause: isBlockedHost() called the synchronous isPrivateIp() which uses ipaddr.isValid() to test whether the URL's hostname is itself an IP address in a private range. Any non-IP-literal hostname — including wildcard-DNS services like 127.0.0.1.nip.io or 169.254.169.254.nip.io — passes ipaddr.isValid() as false, so isAddressPrivate() returns false and the guard silently allows the request.
  • Fix: Make isBlockedHost() async and replace the synchronous call with isPrivateIpAsync() (already present in utils/fetch.ts but unused here), which resolves all DNS addresses and validates each against isAddressPrivate(). Fail-closed on DNS errors. Expand the explicit literal blocklist to also cover ::1 / [::1] and 0.0.0.0.

Bypass vectors (pre-fix)

Hostname Reason for bypass
127.0.0.1.nip.io Not an IP literal → ipaddr.isValid() false → allowed
169.254.169.254.nip.io Same; resolves to cloud metadata endpoint
localtest.me Any public wildcard-DNS service resolving to private space
Attacker-controlled DNS Hostname resolves to 10.x, 172.16.x, 192.168.x, etc.

Impact

An LLM agent processing attacker-controlled content (indirect prompt injection) can be instructed to call web_fetch with a crafted hostname. Before this fix:

  1. Loopback service exfiltrationhttp://127.0.0.1.nip.io:<port>/ reaches any service bound to loopback (e.g., local dev servers, internal admin APIs).
  2. Cloud metadatahttp://169.254.169.254.nip.io/latest/meta-data/ fetches AWS/GCP/Azure instance metadata including IAM credentials when running in CI or cloud environments.
  3. Internal network pivoting — Any RFC-1918 range reachable from the host can be accessed by pointing an attacker-controlled DNS record to it.
  4. Prompt injection → SSRF chain — A malicious web page or document returned by a prior tool call can inject a web_fetch invocation targeting an internal endpoint, exfiltrating secrets back through the LLM response.

Fix approach

  1. isBlockedHost() is now async.
  2. After blocking known loopback/unspecified literals (localhost, 127.0.0.1, ::1, [::1], 0.0.0.0) synchronously, the method calls isPrivateIpAsync(), which resolves the hostname via node:dns/promises lookup({ all: true }) and checks every returned address with isAddressPrivate().
  3. DNS resolution failure → return true (deny by default).
  4. All three call sites (executeFallbackForUrl, filterAndValidateUrls, executeExperimental) and the execute dispatcher are updated to await the guard.

Tests added

New describe('SSRF guard — DNS hostname-to-private-IP bypass') suite covering:

  • localhost → blocked (explicit literal)
  • 127.0.0.1 → blocked (explicit literal)
  • ::1 → blocked (explicit literal, new)
  • 0.0.0.0 → blocked (explicit literal, new)
  • 127.0.0.1.nip.io → blocked (DNS mock returns private IP)
  • 169.254.169.254.nip.io → blocked (DNS mock returns link-local)
  • https://google.com → allowed
  • Standard (non-experimental) mode path via filterAndValidateUrls also covered.

Security report reference

This vulnerability was reported to Google Cloud VRP on 2026-06-07 (Issue #520981635). A working proof-of-concept was submitted demonstrating end-to-end exfiltration of loopback-only content via 127.0.0.1.nip.io and the cloud metadata endpoint via 169.254.169.254.nip.io, both bypassing the existing isBlockedHost() guard. This PR is submitted as a Patch Rewards contribution to strengthen the report and demonstrate the remediation path.

@herdiyana256 herdiyana256 requested a review from a team as a code owner June 8, 2026 14:58
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a critical SSRF vulnerability where attackers could bypass security guards by using wildcard DNS services or specific hostname patterns that resolve to private network addresses. By shifting from synchronous IP validation to an asynchronous approach that resolves DNS before checking against private IP ranges, the system now correctly identifies and blocks these malicious requests. The changes include updating core validation logic and expanding the static blocklist to cover additional loopback and unspecified address formats.

Highlights

  • SSRF Guard Enhancement: Upgraded the SSRF guard to perform asynchronous DNS resolution, effectively blocking hostname-to-private-IP bypass techniques like nip.io.
  • Async Refactoring: Converted isBlockedHost and filterAndValidateUrls to asynchronous methods to support DNS-based validation.
  • Expanded Blocklist: Added explicit literal blocking for IPv6 loopback (::1) and unspecified (0.0.0.0) addresses.
  • Security Hardening: Implemented a fail-closed policy for DNS resolution errors to prevent potential SSRF via ambiguous or unresolvable hostnames.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@github-actions github-actions Bot added the size/m A medium sized PR label Jun 8, 2026
@google-cla

google-cla Bot commented Jun 8, 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.

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

📊 PR Size: size/L

  • Lines changed: 289
  • Additions: +229
  • Deletions: -60
  • Files changed: 4

@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

🛑 Action Required: Evaluation Approval

Steering changes have been detected in this PR. To prevent regressions, a maintainer must approve the evaluation run before this PR can be merged.

Maintainers:

  1. Go to the Workflow Run Summary.
  2. Click the yellow 'Review deployments' button.
  3. Select the 'eval-gate' environment and click 'Approve'.

Once approved, the evaluation results will be posted here automatically.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the WebFetchTool to use asynchronous DNS resolution (isPrivateIpAsync) to prevent SSRF bypasses via hostname-to-private-IP mappings (such as nip.io), and updates the unit tests accordingly. The review feedback identifies a critical DNS rebinding vulnerability because the subsequent HTTP request still uses the original hostname instead of the validated IP address. Additionally, the feedback suggests synchronously blocking the IPv6 unspecified address [::] and adding corresponding test coverage.

Comment on lines +283 to +287
// Resolve DNS to catch hostname-to-private-IP bypasses such as
// 127.0.0.1.nip.io, 169.254.169.254.nip.io, or attacker-controlled DNS
try {
return await isPrivateIpAsync(urlStr);
} catch {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

DNS Rebinding Vulnerability leading to SSRF Bypass

Vulnerability Description:
The SSRF protection implemented in isBlockedHost resolves the URL's hostname to its IP addresses using isPrivateIpAsync (which uses dns.promises.lookup) and checks if any of the resolved IPs are private. However, the subsequent HTTP request in fetchWithTimeout is made using the original URL string (which contains the hostname, not the resolved IP address).

This introduces a classic DNS Rebinding vulnerability. An attacker can configure a malicious DNS server to return a public IP address on the first resolution (bypassing the isBlockedHost check) and then return a private IP address (such as 127.0.0.1 or 169.254.169.254) on the second resolution (when fetch actually connects to the host). This allows the attacker to bypass the SSRF guard completely and access internal resources or cloud metadata endpoints.

Remediation:
To prevent DNS rebinding, the application should either:

  1. Pin the resolved IP address: Resolve the hostname to an IP address, validate it, and then perform the HTTP request directly to that IP address (while setting the Host header to the original hostname).
  2. Use a custom undici Agent/Dispatcher: Configure a custom undici Agent or Dispatcher with a custom connect or lookup function that validates the resolved IP address at connection time, ensuring that the IP address actually connected to is the same one that was validated.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review. I've already implemented the recommended fix: IP pinning. The hostname is resolved once, validated, and then the fetch is performed directly to the resolved IP address with the original Host header set. This prevents DNS rebinding. The changes have been pushed (commit fe2a8ef). Please re-review.

Comment thread packages/core/src/tools/web-fetch.ts Outdated
Comment on lines +280 to +282
if (isLoopbackHost(hostname) || hostname === '0.0.0.0') {
return true;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The IPv6 unspecified address [::] can behave similarly to loopback/unspecified IPv4 0.0.0.0 on many platforms, potentially routing to localhost. While isPrivateIpAsync will eventually block it, doing so requires a DNS lookup that is guaranteed to fail (due to square brackets in the hostname passed to dns.lookup) or throw an error. Explicitly blocking [::] synchronously alongside 0.0.0.0 is more robust, avoids unnecessary DNS lookup errors, and ensures consistent protection.

Suggested change
if (isLoopbackHost(hostname) || hostname === '0.0.0.0') {
return true;
}
if (isLoopbackHost(hostname) || hostname === '0.0.0.0' || hostname === '[::]') {
return true;
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Added hostname === '[::]' to the synchronous blocklist. The change is included in the latest push. Thanks.

Comment on lines +1174 to +1178
label: '0.0.0.0 (unspecified literal)',
url: 'http://0.0.0.0',
dnsPrivate: false,
},
{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Add a test case for the IPv6 unspecified address literal [::] to ensure it is blocked synchronously and correctly.

        {
          label: '0.0.0.0 (unspecified literal)',
          url: 'http://0.0.0.0',
          dnsPrivate: false,
        },
        {
          label: ':: (IPv6 unspecified literal)',
          url: 'http://[::]',
          dnsPrivate: false,
        },

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added test case for http://[::] as suggested. It's now covered in the SSRF guard test suite. Please re-check.

@herdiyana256 herdiyana256 force-pushed the fix/ssrf-dns-hostname-private-ip-bypass branch from ce44fea to fe2a8ef Compare June 8, 2026 15:13
@herdiyana256 herdiyana256 requested a review from a team as a code owner June 8, 2026 15:51
@github-actions github-actions Bot added the size/l A large sized PR label Jun 8, 2026
@herdiyana256

Copy link
Copy Markdown
Author

@google-gemini/gemini-cli-prompt-approvers Could someone please approve the eval-gate workflow for this PR? The SSRF fix is complete (IP pinning, block [::], tests added) and needs to run CI. Thanks!

@gemini-cli

gemini-cli Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Hi there! Thank you for your interest in contributing to Gemini CLI.

To ensure we maintain high code quality and focus on our prioritized roadmap, we only guarantee review and consideration of pull requests for issues that are explicitly labeled as 'help wanted'.

This PR will be closed in 7 days if it remains without that designation. We encourage you to find and contribute to existing 'help wanted' issues in our backlog! Thank you for your understanding.

@herdiyana256 herdiyana256 force-pushed the fix/ssrf-dns-hostname-private-ip-bypass branch from 312351b to 17a70c7 Compare June 21, 2026 05:01
@herdiyana256

Copy link
Copy Markdown
Author

Rebased onto latest main ,now cleanly mergeable. This PR fixes an SSRF vulnerability reported to Google Cloud VRP (Issue 520981635); patch is complete with IP pinning (DNS rebinding), [::] blocking, and full test coverage.

Since this is a security fix rather than a feature, could a maintainer consider it for review before the auto-close window?

@gemini-cli

gemini-cli Bot commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

This pull request is being closed as it has been open for 14 days without a 'help wanted' designation. We encourage you to find and contribute to existing 'help wanted' issues in our backlog! Thank you for your understanding.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l A large sized PR size/m A medium sized PR status/pr-nudge-sent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant