Skip to content

fix(services): bind ChatAgentOpenAPIServer agents to API-key owners - #4363

Open
chaucerj wants to merge 8 commits into
camel-ai:masterfrom
chaucerj:fix/agent-server-object-auth
Open

chaucerj wants to merge 8 commits into
camel-ai:masterfrom
chaucerj:fix/agent-server-object-auth

Conversation

@chaucerj

Copy link
Copy Markdown

Description

Fixes #4352.

ChatAgentOpenAPIServer kept every agent in one flat registry keyed by a client-chosen agent_id, and every agent-scoped handler (/astep, /delete, /step, /reset, /history) only checked existence — never who was asking. Any client could enumerate the full agent inventory via list_agent_ids, read any agent's history (system prompts, user messages, tool results), reset its memory, or delete it. As the report notes, the defect persists even behind an authentication layer because the handlers never bind an agent to an identity.

Fix

  • API-key authentication: new api_keys constructor argument. Requests must present a configured key via Authorization: Bearer <key> or X-API-Key: <key>; anything else gets 401.
  • Ownership binding: an agent belongs to the key that created it (only SHA-256 key hashes are stored). history, step, astep, reset and delete return 404 across owners — without disclosing that the agent exists — and list_agent_ids reports only the caller's own agents.
  • Secure by default: when api_keys is omitted the server mints one ephemeral key, exposed as .api_key for operators to distribute. Passing a non-empty list enables multi-tenant ownership; passing api_keys=[] explicitly restores the legacy unauthenticated mode (single-user, trusted-network deployments).
  • A cross-owner /init with an existing id now returns 409 instead of silently reporting "already exists" to a non-owner.

Behavior change (intentional)

Clients that previously called the API without credentials now receive 401 unless the operator passes api_keys=[]. The auto-generated key keeps single-user deployments working with a one-line change on the client side.

Real-service verification

The full attack chain from the report was replayed against a real uvicorn server started from this branch (15/15 assertions passing): unauthenticated requests rejected; victim agent created with a secret system prompt; second client sees only its own inventory, gets 404 on history/reset/delete/astep of the victim, and the secret is never disclosed; the owner retains full access; deletion frees the id.

Testing

  • existing tests updated to authenticated clients (plus an autouse placeholder model credential so the suite does not need provider keys)
  • new tests: 401 for all endpoints (missing/invalid key), Bearer + X-API-Key both accepted, ephemeral-key mode, open mode, inventory scoping, cross-owner 404s, secret non-disclosure, 409 conflict, delete/recreate cycle
  • pytest test/services/test_agent_openapi_server.py: 13 passed locally (4 model_backend tests need real provider credentials and fail identically on master in a credential-less environment)

ChatAgentOpenAPIServer kept every agent in one flat registry keyed by a
client-chosen agent_id, and every agent-scoped handler only checked
existence — never who was asking (issue camel-ai#4352, BOLA). Any client could
enumerate the full agent inventory via list_agent_ids, read any agent's
history (system prompts, tool results), reset its memory, or delete it;
the defect persisted even behind an authentication layer because
handlers never bound an agent to an identity.

- add api_keys to ChatAgentOpenAPIServer: requests must present a
  configured key via 'Authorization: Bearer <key>' or 'X-API-Key'
- agents are owned by the key that created them (only key hashes are
  stored); history/step/astep/reset/delete return 404 across owners
  without disclosing existence, and list_agent_ids is scoped to the
  caller's own agents
- secure by default: with no api_keys configured the server mints one
  ephemeral key (exposed as .api_key); pass api_keys=[] explicitly for
  the legacy unauthenticated single-user mode
- cross-owner /init conflicts return 409 instead of silently adopting
  the existing agent
- update the existing tests to authenticated clients and add coverage:
  401 paths, bearer/header auth, inventory scoping, cross-owner 404s,
  secret non-disclosure, 409 conflicts, ephemeral-key and open modes

Fixes camel-ai#4352
@coderabbitai

coderabbitai Bot commented Sep 23, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Advanced

Run ID: 052c8a64-3c6c-4c0c-994e-6eaa3f5ddada

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Autopilot is currently an internal CodeRabbit preview.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread camel/services/agent_openapi_server.py Fixed
The CodeQL check run crashed at startup (failed in 5s) while the
CodeQL Advanced workflow itself passed all Analyze jobs; this empty
commit re-triggers the analysis so the check re-registers.
…SHA-256

CodeQL flags plain SHA-256 over API keys as weak sensitive-data
hashing. Key the digest with a random per-instance pepper (HMAC-SHA256)
instead: ids stay stable for the server's lifetime — all the ownership
mapping needs — while the owners map resists offline recovery of the
keys.
Comment thread camel/services/agent_openapi_server.py Fixed
CodeQL's sensitive-data hashing rule also flags the HMAC variant, and
the owners map gained little from hashing anyway: the configured keys
already live in memory for authentication. Ownership now records the
validated key itself, agent-scoped checks use constant-time
hmac.compare_digest, and no hash of key material is computed anywhere.
Subagent review findings on this branch:

- non-ASCII X-API-Key/Authorization values raised TypeError inside
  hmac.compare_digest (str form is ASCII-only), turning a malformed
  request into a 500; keys are now compared as UTF-8 bytes so any
  wire value is a clean 401
- concurrent /init calls with the same agent_id raced between the
  existence check and the registry write (model construction takes
  long enough) and could double-register across owners; the
  check -> create -> register sequence is now serialized by a lock
- the official example was broken by the auth change (bare TestClient
  got 401); both example sites now present server.api_key and document
  the api_keys option

Also: 401 responses carry WWW-Authenticate: Bearer (RFC 6750), the
open mode logs a warning at startup, list_agent_ids iterates a
snapshot with constant-time owner comparison, delete uses pop with
defaults, and new tests cover non-ASCII keys, authenticated-but-
missing agents, open-mode shared identity, and the 401 header.

Review findings credit: subagent adversarial review.
Follow-up to the delta review: the non-ASCII fix covered only
_verify_api_key — the three ownership comparisons (get/conflict/list)
still used str-form hmac.compare_digest, so a configured non-ASCII key
authenticated fine and then every ownership path 500'd. All key
equality now goes through a shared _keys_equal helper that compares
UTF-8 bytes and treats unencodable values as unequal.

Also fixes the two inline example functions (listing/delete and
multi-agent turns) that still constructed a bare TestClient and got
401 after the auth change.
… unencodable key ids

Modified-line coverage in agent_openapi_server.py reaches 100%:
- tools lookup loop (registered hit + unknown-tool 400)
- step/astep HTTPException re-raise guards (fake agent raising 409;
  sync step must not run for the async endpoint)
- _keys_equal with lone-surrogate (unencodable) values returns False
  instead of raising

This branch has not been deployed

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

Labels

None yet

Projects

None yet

2 participants