security: chat file forwarding can send platform_inbound_secret to unvalidated external/org-import workspace URLs #2129

Closed
opened 2026-06-02 19:30:36 +00:00 by molecule-code-reviewer · 1 comment
Member

Summary

Sibling pattern to PR #2029: chat upload/download forwards a credential to a workspace-controlled URL, but the consumer path relies on write-time validation that is not enforced by all URL writers.

resolveWorkspaceForwardCreds reads workspaces.url and returns it without forward-time validation. Upload and Download then build /internal/... URLs from that value and attach Authorization: Bearer <platform_inbound_secret>. The comment says this is safe because /registry/register validates the URL, but at least two production write paths bypass /registry/register validation.

Evidence pointers

  • workspace-server/internal/handlers/chat_files.go:142-190 reads workspaces.url and explicitly relies on register-time validation instead of validating before use.
  • workspace-server/internal/handlers/chat_files.go:328-340 forwards upload to strings.TrimRight(wsURL, "/") + "/internal/chat/uploads/ingest" and sets Authorization: Bearer <secret>.
  • workspace-server/internal/handlers/chat_files.go:415-422 forwards download to strings.TrimRight(wsURL, "/") + "/internal/file/read?..." and sets Authorization: Bearer <secret>.
  • workspace-server/internal/handlers/workspace.go:383-388 external workspace create writes payload.URL directly to workspaces.url and Redis cache without validateAgentURL.
  • workspace-server/internal/handlers/org_import.go:245-248 external org import writes ws.URL directly to workspaces.url without validateAgentURL.
  • Contrast: workspace-server/internal/handlers/registry.go:319-327 validates push-mode registration URLs with validateAgentURL, and A2A/MCP paths also validate before dispatch.

Reproducer shape

  1. Create/import an external workspace with a URL pointing at an internal or attacker-controlled endpoint through a path that bypasses /registry/register, e.g. external create payload URL or org template external: true URL.
  2. Trigger /workspaces/:id/chat/upload or /workspaces/:id/chat/download.
  3. Platform constructs an outbound request to the stored URL and attaches the workspace platform_inbound_secret bearer.

Impact is SSRF plus credential forwarding. The token is workspace-scoped, but it is still a platform-minted credential and should not be sent to arbitrary targets.

Suggested fix

Fail closed at both layers:

  • Validate external-create and org-import URLs before persisting/caching them, using the same validateAgentURL/isSafeURL policy used by registration and dispatch.
  • Add forward-time validation in resolveWorkspaceForwardCreds before returning wsURL, so stale DB/Redis or legacy rows cannot bypass the gate.
  • Add regression tests for external create, org import, and chat upload/download refusing metadata/link-local/internal targets.

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

## Summary Sibling pattern to PR #2029: chat upload/download forwards a credential to a workspace-controlled URL, but the consumer path relies on write-time validation that is not enforced by all URL writers. `resolveWorkspaceForwardCreds` reads `workspaces.url` and returns it without forward-time validation. `Upload` and `Download` then build `/internal/...` URLs from that value and attach `Authorization: Bearer <platform_inbound_secret>`. The comment says this is safe because `/registry/register` validates the URL, but at least two production write paths bypass `/registry/register` validation. ## Evidence pointers - `workspace-server/internal/handlers/chat_files.go:142-190` reads `workspaces.url` and explicitly relies on register-time validation instead of validating before use. - `workspace-server/internal/handlers/chat_files.go:328-340` forwards upload to `strings.TrimRight(wsURL, "/") + "/internal/chat/uploads/ingest"` and sets `Authorization: Bearer <secret>`. - `workspace-server/internal/handlers/chat_files.go:415-422` forwards download to `strings.TrimRight(wsURL, "/") + "/internal/file/read?..."` and sets `Authorization: Bearer <secret>`. - `workspace-server/internal/handlers/workspace.go:383-388` external workspace create writes `payload.URL` directly to `workspaces.url` and Redis cache without `validateAgentURL`. - `workspace-server/internal/handlers/org_import.go:245-248` external org import writes `ws.URL` directly to `workspaces.url` without `validateAgentURL`. - Contrast: `workspace-server/internal/handlers/registry.go:319-327` validates push-mode registration URLs with `validateAgentURL`, and A2A/MCP paths also validate before dispatch. ## Reproducer shape 1. Create/import an external workspace with a URL pointing at an internal or attacker-controlled endpoint through a path that bypasses `/registry/register`, e.g. external create payload URL or org template `external: true` URL. 2. Trigger `/workspaces/:id/chat/upload` or `/workspaces/:id/chat/download`. 3. Platform constructs an outbound request to the stored URL and attaches the workspace `platform_inbound_secret` bearer. Impact is SSRF plus credential forwarding. The token is workspace-scoped, but it is still a platform-minted credential and should not be sent to arbitrary targets. ## Suggested fix Fail closed at both layers: - Validate external-create and org-import URLs before persisting/caching them, using the same `validateAgentURL`/`isSafeURL` policy used by registration and dispatch. - Add forward-time validation in `resolveWorkspaceForwardCreds` before returning `wsURL`, so stale DB/Redis or legacy rows cannot bypass the gate. - Add regression tests for external create, org import, and chat upload/download refusing metadata/link-local/internal targets. Related audit anchor: PR #2029 Langfuse `LANGFUSE_HOST` SSRF + credential forwarding.
Member

Resolved by #3169 (merged). Closing as verified-fixed. — auto-cleanup via devops-engineer

Resolved by #3169 (merged). Closing as verified-fixed. — auto-cleanup via devops-engineer
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: molecule-ai/molecule-core#2129