security: transcript proxy forwards caller Authorization to weakly-validated agent_card URL #2130

Closed
opened 2026-06-02 19:31:32 +00:00 by molecule-code-reviewer · 0 comments
Member

Summary

Another sibling to PR #2029: the transcript proxy sends a credential-bearing backend request to an agent-card URL that can be workspace-controlled and is only weakly validated.

TranscriptHandler.Get reads agent_card->>'url', validates it with validateWorkspaceURL, then forwards the original caller Authorization header to that target. The validator blocks only a few metadata/link-local cases; it explicitly allows loopback and Docker/internal hostnames and does not use the stricter validateAgentURL / isSafeURL policy used by A2A/MCP dispatch. Separately, /registry/update-card writes arbitrary agent_card JSON after workspace-token auth without validating agent_card.url.

Evidence pointers

  • workspace-server/internal/handlers/transcript.go:48-73 reads agent_card->>'url', parses it, and calls validateWorkspaceURL.
  • workspace-server/internal/handlers/transcript.go:90-103 builds the outbound request and forwards Authorization from the caller.
  • workspace-server/internal/handlers/transcript.go:140-178 validateWorkspaceURL only blocks non-http schemes, empty host, selected metadata hostnames, link-local, and IPv6 ULA; it does not reject loopback/private IPv4 or DNS names resolving to internal ranges.
  • workspace-server/internal/handlers/registry.go:800-816 /registry/update-card accepts workspace-authenticated agent_card JSON and updates workspaces.agent_card without URL validation.
  • workspace-server/internal/models/workspace.go:135-138 UpdateCardPayload carries raw AgentCard JSON.

Reproducer shape

  1. Use a workspace-authenticated /registry/update-card request to set agent_card.url to an internal/loopback/attacker-controlled URL that passes validateWorkspaceURL.
  2. Trigger GET /workspaces/:id/transcript as a platform/canvas caller.
  3. Platform sends a backend request to that URL and forwards the caller Authorization header.

Impact is SSRF plus credential forwarding of the caller bearer token. This is the same security shape as #2029: attacker-controlled host -> server-side request -> credential attached.

Recommended fix shape

Use one fail-closed URL policy for all platform-to-workspace forwarding:

  • In workspace-server/internal/handlers/transcript.go, replace or tighten validateWorkspaceURL to match validateAgentURL / isSafeURL production policy, including DNS resolution checks.
  • Validate agent_card.url on /registry/register and /registry/update-card when it can be used by transcript or any other platform proxy.
  • Prefer reading the already-validated dispatch URL source, or require forward-time validation immediately before setting any credential header.
  • Add regression tests for update-card + transcript refusing loopback/private/metadata targets and verifying Authorization is not sent on rejected URLs.

Related audit anchor: PR #2029 Langfuse LANGFUSE_HOST SSRF + credential forwarding.

## Summary Another sibling to PR #2029: the transcript proxy sends a credential-bearing backend request to an agent-card URL that can be workspace-controlled and is only weakly validated. `TranscriptHandler.Get` reads `agent_card->>'url'`, validates it with `validateWorkspaceURL`, then forwards the original caller `Authorization` header to that target. The validator blocks only a few metadata/link-local cases; it explicitly allows loopback and Docker/internal hostnames and does not use the stricter `validateAgentURL` / `isSafeURL` policy used by A2A/MCP dispatch. Separately, `/registry/update-card` writes arbitrary `agent_card` JSON after workspace-token auth without validating `agent_card.url`. ## Evidence pointers - `workspace-server/internal/handlers/transcript.go:48-73` reads `agent_card->>'url'`, parses it, and calls `validateWorkspaceURL`. - `workspace-server/internal/handlers/transcript.go:90-103` builds the outbound request and forwards `Authorization` from the caller. - `workspace-server/internal/handlers/transcript.go:140-178` `validateWorkspaceURL` only blocks non-http schemes, empty host, selected metadata hostnames, link-local, and IPv6 ULA; it does not reject loopback/private IPv4 or DNS names resolving to internal ranges. - `workspace-server/internal/handlers/registry.go:800-816` `/registry/update-card` accepts workspace-authenticated `agent_card` JSON and updates `workspaces.agent_card` without URL validation. - `workspace-server/internal/models/workspace.go:135-138` `UpdateCardPayload` carries raw `AgentCard` JSON. ## Reproducer shape 1. Use a workspace-authenticated `/registry/update-card` request to set `agent_card.url` to an internal/loopback/attacker-controlled URL that passes `validateWorkspaceURL`. 2. Trigger `GET /workspaces/:id/transcript` as a platform/canvas caller. 3. Platform sends a backend request to that URL and forwards the caller `Authorization` header. Impact is SSRF plus credential forwarding of the caller bearer token. This is the same security shape as #2029: attacker-controlled host -> server-side request -> credential attached. ## Recommended fix shape Use one fail-closed URL policy for all platform-to-workspace forwarding: - In `workspace-server/internal/handlers/transcript.go`, replace or tighten `validateWorkspaceURL` to match `validateAgentURL` / `isSafeURL` production policy, including DNS resolution checks. - Validate `agent_card.url` on `/registry/register` and `/registry/update-card` when it can be used by transcript or any other platform proxy. - Prefer reading the already-validated dispatch URL source, or require forward-time validation immediately before setting any credential header. - Add regression tests for update-card + transcript refusing loopback/private/metadata targets and verifying `Authorization` is not sent on rejected URLs. Related audit anchor: PR #2029 Langfuse `LANGFUSE_HOST` SSRF + credential forwarding.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: molecule-ai/molecule-core#2130