Skip to content

SSRF: fetch tool dials arbitrary URLs with no address validation #156

Description

@jhrozek

Summary

The fetch tool dials the model-supplied URL directly with no validation of the
resolved address. A caller can point it at loopback, link-local, private-range,
or cloud instance-metadata endpoints (e.g. http://169.254.169.254/latest/meta-data/)
and it will fetch them (CWE-918).

Where

  • pkg/fetcher/fetcher.go (fetchURL, ~line 77-90): builds the request straight
    from the input URL and calls http.NewRequest / httpClient.Do with no IP
    classification or scheme restriction. The only pre-check is robots.txt
    (politeness, not security). The existing //nolint:gosec on the Do call
    ("This is a fetch server; fetching user-provided URLs is its core purpose")
    acknowledges the behavior but doesn't mitigate it.
  • pkg/server/server.go (NewFetchServer, ~line 36-49): the http.Client is
    constructed with only a Timeout (and an optional proxy) — nothing on
    Transport.DialContext to gate the resolved peer address.
  • The toolhive registry manifest for this server also declares
    "network": {"outbound": {"allow_port": [443], "insecure_allow_all": true}},
    so there's no compensating network-level restriction either. Note a
    NetworkPolicy alone wouldn't close this anyway — the cloud-metadata route
    isn't pod-to-pod traffic a NetworkPolicy governs.

Suggested fix

Add a Control hook (or a custom net.Dialer) on the http.Client's
Transport that classifies the resolved peer IP before connecting, blocking:

  • loopback (unless explicitly opted in for local/dev use)
  • link-local unicast (covers 169.254.0.0/16, including the AWS/GCP/Azure/Oracle
    metadata endpoint)
  • multicast
  • well-known cloud metadata addresses outside the link-local range that some
    policies would otherwise miss (AWS IMDSv2-over-IPv6 fd00:ec2::254, Alibaba
    100.100.100.200)
  • optionally private/RFC1918 ranges, if this server is ever expected to run
    somewhere those addresses are untrusted

This should run against the resolved address, not just the literal host in
the URL, to defend against DNS rebinding.

Context

Found while evaluating this server as a fetch-tool backend for
stacklok/atrium. Happy to contribute the
fix if useful — Atrium already carries a small, tested internal/ssrf package
implementing exactly this policy that could serve as a reference.

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-triageIssue needs initial triage by a maintainer

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions