Skip to content

Resolve an agent's host before dialling it - #894

Closed
prysthedev wants to merge 1 commit into
CopilotKit:mainfrom
prysthedev:fix/agent-endpoint-ssrf
Closed

prysthedev wants to merge 1 commit into
CopilotKit:mainfrom
prysthedev:fix/agent-endpoint-ssrf

Conversation

@prysthedev

@prysthedev prysthedev commented Oct 29, 2026 •

Copy link
Copy Markdown

The agent endpoint check reads the address as written and does no DNS resolution, so two kinds of address passed it and were then dialled by the server with headers the registering person chose:

  • a single-label service name on the deployment's own network, such as a Compose or Kubernetes service
  • a public-looking name whose DNS answers with a private, loopback or metadata address

The guarded agent fetch now resolves the host of the stored address and of every redirect hop before dialling, and refuses a single-label name or any resolved address the textual check would refuse. Hosts named in AGENT_ENDPOINT_ALLOWED_HOSTS, and hosts the deployment configures itself (the managed Bot, a picked harness, tenant package coworkers), are exempt, as is everything when the private-host floor is down. The connection test goes through the same fetch.

The textual check also now treats 0.0.0.0/8 and 100.64.0.0/10 as internal.

DNS rebinding between the lookup and the dial is not closed by this; it needs a pinned dialler or a network policy around the server.

What this changes

Where it runs

OpenBot is deployed as several server processes behind a load balancer, serving a whole company.
Consecutive requests from the same person reach different processes, and the process that answered a
WebSocket upgrade is rarely the one that answers the next call on that conversation.

State that outlives a single request therefore has to be shared, or the change works on one machine
and stops working the moment there are two, without saying so. That failure is worse than not
shipping the feature: it passes review, passes CI, passes a local demo, and only surfaces as a Bot
that forgets, a question nobody can answer, or a boundary that never fires.

Answer these even when the answer is "none":

  • New state that outlives a request? Where does it live? A Map or Set held in a module or
    a factory closure does not count as somewhere.
  • What happens on the second replica? Name the concrete outcome, not "should be fine".
  • Anything serialised? Say what stops two processes doing it at once. A unique index, a
    conditional update, or an advisory lock are answers. A check-then-write is not.
  • Anything fanned out to a browser? Say how it reaches a socket held by another process.
  • New listener, port, or schedule? Say how it is reached through the same ingress as the API,
    and what a hundred copies of it do.

Postgres is already there and is the default answer to all of the above: a table, a unique index, a
conditional update, LISTEN/NOTIFY for fan-out.

Boundary and audit

  • Every acting call still goes through the gateway: resolve, decide, audit, then act.
  • New refusals and new failures each write a row.
  • Nothing new is trusted from the client that the server can resolve itself.

Changelog

  • A line in CHANGELOG.md under Unreleased, or a sentence on why a deployment behaves no
    differently afterwards.

Proof

The agent endpoint check reads the address as written and does no DNS
resolution, so two kinds of address passed it and were then dialled by
the server with headers the registering person chose:

- a single-label service name on the deployment's own network, such as
  a Compose or Kubernetes service
- a public-looking name whose DNS answers with a private, loopback or
  metadata address

The guarded agent fetch now resolves the host of the stored address and
of every redirect hop before dialling, and refuses a single-label name
or any resolved address the textual check would refuse. Hosts named in
AGENT_ENDPOINT_ALLOWED_HOSTS, and hosts the deployment configures itself
(the managed Bot, a picked harness, tenant package coworkers), are
exempt, as is everything when the private-host floor is down. The
connection test goes through the same fetch.

The textual check also now treats 0.0.0.0/8 and 100.64.0.0/10 as
internal.

DNS rebinding between the lookup and the dial is not closed by this; it
needs a pinned dialler or a network policy around the server.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-29T23:42:38.025067Z
We couldn’t complete this review. Comment @codex review to try again.
f6b95a1 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@prysthedev prysthedev closed this Oct 29, 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.

2 participants