Skip to content

harden fetch_url against DNS rebinding (TOCTOU) #1

Description

@yoloyash

What

fetchReadable / the fetch_url tool in lib/web.ts validates that a URL's hostname doesn't resolve to a private/internal address before issuing the request. This blocks the obvious SSRF case (LLM follows a search result link to localhost, 169.254.169.254, RFC1918, etc.).

It does not close DNS rebinding. The hostname is resolved twice:

  1. By our pre-flight dns.lookup() in validatePublicHttpUrl — used only for the public-IP check.
  2. By Node's fetch() at connect time — what the actual TCP connection uses.

A rebinding-attacker DNS server can return a public IP for the first lookup and a private IP (e.g. 10.0.0.1, 169.254.169.254) for the second.

Why we shipped v0.1 with the gap

  • Self-hosted, typically single-user or small-team.
  • Attack requires: (a) the LLM to follow a link to an attacker-controlled domain, AND (b) that domain to be running an active rebinding flip.
  • The current code already blocks the realistic 99% case.

Fix path

Pin the resolved IP at connect time. Cleanest in Node is undici with a custom `connect`:

```ts
import { Agent } from "undici";
const safeAgent = new Agent({
connect: { lookup: (hostname, opts, cb) => {
// re-validate here, then return the same address fetch will actually use
}},
});
```

Then pass via `fetch(url, { dispatcher: safeAgent })`. The pre-flight check in `validatePublicHttpUrl` becomes redundant once the connect-time check is in place.

Acceptance

  • `fetch_url` cannot reach a private address even when hostname → A record flips between resolutions.
  • Test with a controlled rebinding DNS endpoint.
  • Drop the redundant pre-flight DNS check (or keep both for defense in depth).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions