google-gemini / google-gemini/gemini-cli

security(web-fetch): SSRF via redirect to private IP bypasses isPrivateIp check

Open
#29,074 0 comments 0 reactions 0 assignees View on GitHub
area/security status/need-triage
Dominant language
TypeScript
Stars
107k
Forks
14.6k
Avg merge
2d 3h
Merged PRs (30d)
45

Description

### What happened?

In `packages/core/src/tools/web-fetch.ts` and `packages/core/src/utils/fetch.ts` (main @ 812f7a2bc), the web-fetch tool validates the *initial* URL against private IP blocklists via `isPrivateIp()` / `isPrivateIpAsync()`, but `fetch()` is called with default redirect handling (`redirect: "follow"`), and **redirect targets are not re-validated**.

`fetch.ts` lines 190-212:
```ts
export async function fetchWithTimeout(url: string, timeout: number, options?: RequestInit): Promise {
// ...
const response = await fetch(url, { ...options, signal: controller.signal });
return response;
}
```

`web-fetch.ts` lines 297, 636:
```ts
const res = await fetchWithTimeout(url, URL_FETCH_TIMEOUT_MS, { ... });
```

An attacker can host a URL that initially resolves to a public IP (passing `isPrivateIp` checks) but returns a `302` redirect to a private address:
- `http://169.254.169.254/latest/meta-data/` (AWS IMDS)
- `http://127.0.0.1:41242/tasks` (local A2A server, unauthenticated per #29001)
- `http://192.168.1.1/admin`

`fetch()` transparently follows the redirect to the private IP, and the response body is returned to the LLM and then to the user, enabling SSRF.

The existing `isPrivateIpAsync` (which does DNS lookup) is only called at request start, not on redirect. `fetch` does not expose redirect URL to the caller without `redirect: "manual"` handling.

### What did you expect to happen?

- Use `redirect: "manual"` and manually validate each `Location` header with `isPrivateIp` / `isPrivateIpAsync` before following, or
- Use undici's `maxRedirections` with a custom dispatcher that validates redirect targets, or
- At minimum, document and limit `maxRedirections: 0` and surface redirect `Location` to the LLM instead of auto-following

### Client information

- Source-level finding verified against upstream `main` at commit `812f7a2bc`
- Files: `packages/core/src/utils/fetch.ts:190-212`, `packages/core/src/tools/web-fetch.ts:297,636`
- Affects all platforms

### Login information

Not applicable.

### Anything else we need to know?

Sources:
- https://github.com/google-gemini/gemini-cli/blob/812f7a2bc/packages/core/src/utils/fetch.ts#L190-L212
- https://github.com/google-gemini/gemini-cli/blob/812f7a2bc/packages/core/src/tools/web-fetch.ts#L297
- Private IP validation exists (`isPrivateIp`, `isPrivateIpAsync`, `isAddressPrivate`) but is only applied to the initial URL, not redirect chain

**Repro:**
1. Host a server at `https://example.com/redirect` that returns `302 Location: http://127.0.0.1:41242/tasks`
2. In gemini-cli, `web_fetch` with `url: "https://example.com/redirect"`
3. Observe fetch follows to private A2A server and returns task list (SSRF)

Searched existing issues for "web-fetch redirect", "SSRF", "private ip bypass" — no open duplicate found (closed #27635 is about OAuth metadata URLs, distinct).

Contributor guide

Open the contributing guide

Research direction

Start with fetchWithTimeout in packages/core/src/utils/fetch.ts and its callers at packages/core/src/tools/web-fetch.ts:297 and :636. Review the existing isPrivateIp and isPrivateIpAsync helpers, then reproduce the public-to-private redirect described in the issue. Done means redirect targets are checked before access and the SSRF reproduction no longer reaches the private address.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
networking, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.