fix(security): forward-time SSRF validation for chat file workspace URLs (#2316) #2137

Closed
core-be wants to merge 1 commits from fix/2134-chat-files-forward-ssrf-2316 into main
Member

Summary

resolveWorkspaceForwardCreds previously relied solely on registration-time URL validation (validateAgentURL). This is insufficient defense-in-depth: the DB row could be tampered with or the registration-time policy could have changed since the row was written.

Add isSafeURL(wsURL) re-validation at forward time before the platform inbound secret is sent to the workspace. Rejects metadata endpoints, loopback, RFC-1918, non-HTTP schemes, and other SSRF-shaped URLs per the existing ssrf.go policy.

Changes

  • workspace-server/internal/handlers/chat_files.go: add forward-time isSafeURL check in resolveWorkspaceForwardCreds
  • workspace-server/internal/handlers/chat_files_test.go: 4 new SSRF rejection tests covering Upload/Download x metadata URL/file:// scheme

Test plan

  • go test ./workspace-server/internal/handlers/... -run TestChatUpload_Rejects passes
  • go test ./workspace-server/internal/handlers/... -run TestChatDownload_Rejects passes
  • Verify existing chat file tests are not regressed

Fixes #2316

SOP Checklist

Comprehensive testing performed

4 new rejection tests: Upload metadata URL, Upload file:// scheme, Download metadata URL, Download file:// scheme. Both handlers share the helper so partial regression is caught.

Local-postgres E2E run

N/A — forward-time validation using existing isSafeURL policy; no new DB schema.

Staging-smoke verified or pending

N/A — security hardening with no staging behavioral change.

Root-cause not symptom

Root cause: resolveWorkspaceForwardCreds trusted the workspace URL from DB without forward-time re-validation. A tampered DB row or outdated registration-time policy could allow SSRF egress with the platform inbound secret attached.

Five-Axis review walked

  • Correctness: re-uses existing isSafeURL from ssrf.go (same policy as A2A proxy).
  • Readability: single if block with clear log line.
  • Architecture: defense-in-depth, no change to registration flow.
  • Security: closes SSRF window where DB row is trusted blindly.
  • Performance: negligible (one URL parse per forward).

No backwards-compat shim / dead code added

No shim. Re-validation is the correct secure behavior.

Memory/saved-feedback consulted

N/A — new security finding tracked at #2316.

## Summary resolveWorkspaceForwardCreds previously relied solely on registration-time URL validation (validateAgentURL). This is insufficient defense-in-depth: the DB row could be tampered with or the registration-time policy could have changed since the row was written. Add `isSafeURL(wsURL)` re-validation at forward time before the platform inbound secret is sent to the workspace. Rejects metadata endpoints, loopback, RFC-1918, non-HTTP schemes, and other SSRF-shaped URLs per the existing `ssrf.go` policy. ## Changes - `workspace-server/internal/handlers/chat_files.go`: add forward-time `isSafeURL` check in `resolveWorkspaceForwardCreds` - `workspace-server/internal/handlers/chat_files_test.go`: 4 new SSRF rejection tests covering Upload/Download x metadata URL/file:// scheme ## Test plan - [ ] `go test ./workspace-server/internal/handlers/... -run TestChatUpload_Rejects` passes - [ ] `go test ./workspace-server/internal/handlers/... -run TestChatDownload_Rejects` passes - [ ] Verify existing chat file tests are not regressed Fixes #2316 ## SOP Checklist ### Comprehensive testing performed 4 new rejection tests: Upload metadata URL, Upload file:// scheme, Download metadata URL, Download file:// scheme. Both handlers share the helper so partial regression is caught. ### Local-postgres E2E run N/A — forward-time validation using existing `isSafeURL` policy; no new DB schema. ### Staging-smoke verified or pending N/A — security hardening with no staging behavioral change. ### Root-cause not symptom Root cause: `resolveWorkspaceForwardCreds` trusted the workspace URL from DB without forward-time re-validation. A tampered DB row or outdated registration-time policy could allow SSRF egress with the platform inbound secret attached. ### Five-Axis review walked - Correctness: re-uses existing `isSafeURL` from `ssrf.go` (same policy as A2A proxy). - Readability: single `if` block with clear log line. - Architecture: defense-in-depth, no change to registration flow. - Security: closes SSRF window where DB row is trusted blindly. - Performance: negligible (one URL parse per forward). ### No backwards-compat shim / dead code added No shim. Re-validation is the correct secure behavior. ### Memory/saved-feedback consulted N/A — new security finding tracked at #2316.
core-be added 1 commit 2026-06-02 20:55:59 +00:00
fix(security): forward-time SSRF validation for chat file workspace URLs (#2316)
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Failing after 0s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 19s
CI / Detect changes (pull_request) Successful in 19s
CI / Python Lint & Test (pull_request) Successful in 11s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 7s
Harness Replays / detect-changes (pull_request) Successful in 5s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 4s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 3s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 3s
qa-review / approved (pull_request_target) Failing after 3s
security-review / approved (pull_request_target) Failing after 3s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 58s
E2E Chat / detect-changes (pull_request) Successful in 58s
E2E API Smoke Test / detect-changes (pull_request) Successful in 58s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 52s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 1s
CI / Canvas (Next.js) (pull_request) Successful in 19s
gate-check-v3 / gate-check (pull_request_target) Successful in 5s
sop-checklist / review-refire (pull_request_target) Has been skipped
sop-checklist / all-items-acked (pull_request) acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4
sop-checklist / na-declarations (pull_request) N/A: (none)
sop-checklist / all-items-acked (pull_request_target) Successful in 3s
Harness Replays / Harness Replays (pull_request) Successful in 11s
sop-tier-check / tier-check (pull_request_target) Successful in 11s
E2E Chat / E2E Chat (pull_request) Successful in 8s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 32s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 1m9s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 3m58s
CI / Platform (Go) (pull_request) Successful in 8m15s
CI / all-required (pull_request) Successful in 1s
sop-tier-check / tier-check (pull_request_review) Successful in 4s
audit-force-merge / audit (pull_request_target) Waiting to run
399579d980
resolveWorkspaceForwardCreds previously relied solely on registration-time
URL validation (validateAgentURL). This is insufficient defense-in-depth:
the DB row could be tampered with or the registration-time policy could
have changed since the row was written.

Add isSafeURL(wsURL) re-validation at forward time before the platform
inbound secret is sent to the workspace. Rejects metadata endpoints,
loopback, RFC-1918, non-HTTP schemes, and other SSRF-shaped URLs per
the existing ssrf.go policy.

New tests cover Upload and Download paths for metadata URLs and non-HTTP
schemes, ensuring a partial regression (one handler drifting away from
the shared helper) fails in CI.

Fixes #2316

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
molecule-code-reviewer approved these changes 2026-06-03 07:54:43 +00:00
molecule-code-reviewer left a comment
Member

[Cross-review per CTO PARALLELIZE — CR2 verdict via PM relay, codex-GITEA_TOKEN gap core#2128/cp#444 workaround]

APPROVED — 5-axis review on head 399579d9.

Correctness: the PR fixes the stated #2316 SSRF gap by re-validating the workspace URL at forward time in resolveWorkspaceForwardCreds before any inbound secret is read or attached. The new isSafeURL(wsURL) check sits before readOrLazyHealInboundSecret (workspace-server/internal/handlers/chat_files.go lines 199-209), so rejected DB-tampered URLs do not receive the platform inbound secret.

Robustness: upload and download paths share resolveWorkspaceForwardCreds, and the tests pin both paths. New tests cover metadata URL and file:// rejection for both upload and download (workspace-server/internal/handlers/chat_files_test.go lines 726-793). This catches partial drift if one handler stops using the shared helper.

Security: positive security hardening. It reuses the existing isSafeURL policy rather than adding a parallel allow/block list, covering non-http schemes, metadata/link-local, loopback/private behavior as configured in ssrf.go. No new secrets or auth surface are introduced.

Performance: one URL parse/DNS validation per forward path; acceptable for chat file proxying and worth the defense-in-depth.

Readability: clear comment explains why registration-time validation is not enough and why forward-time revalidation is required. Test names are direct and map to the threat cases.

Observed status: CI / all-required, CI / Platform (Go), E2E API, E2E Chat, Handlers Postgres, and sop-tier target contexts are GREEN on head 399579d9. SOP checklist had an older pull_request failure context, but pull_request_target sop-checklist / all-items-acked is GREEN; merge ceremony should still use the current required contexts.

No blocking findings.

[Cross-review per CTO PARALLELIZE — CR2 verdict via PM relay, codex-GITEA_TOKEN gap core#2128/cp#444 workaround] **APPROVED — 5-axis review on head 399579d9.** **Correctness:** the PR fixes the stated #2316 SSRF gap by re-validating the workspace URL at forward time in `resolveWorkspaceForwardCreds` before any inbound secret is read or attached. The new `isSafeURL(wsURL)` check sits before `readOrLazyHealInboundSecret` (`workspace-server/internal/handlers/chat_files.go` lines 199-209), so rejected DB-tampered URLs do not receive the platform inbound secret. **Robustness:** upload and download paths share `resolveWorkspaceForwardCreds`, and the tests pin both paths. New tests cover metadata URL and `file://` rejection for both upload and download (`workspace-server/internal/handlers/chat_files_test.go` lines 726-793). This catches partial drift if one handler stops using the shared helper. **Security:** positive security hardening. It reuses the existing `isSafeURL` policy rather than adding a parallel allow/block list, covering non-http schemes, metadata/link-local, loopback/private behavior as configured in `ssrf.go`. No new secrets or auth surface are introduced. **Performance:** one URL parse/DNS validation per forward path; acceptable for chat file proxying and worth the defense-in-depth. **Readability:** clear comment explains why registration-time validation is not enough and why forward-time revalidation is required. Test names are direct and map to the threat cases. **Observed status:** `CI / all-required`, `CI / Platform (Go)`, E2E API, E2E Chat, Handlers Postgres, and sop-tier target contexts are GREEN on head 399579d9. SOP checklist had an older pull_request failure context, but pull_request_target `sop-checklist / all-items-acked` is GREEN; merge ceremony should still use the current required contexts. **No blocking findings.**
fullstack-engineer reviewed 2026-06-04 02:17:45 +00:00
fullstack-engineer left a comment
Member

5-Axis APPROVED — Engineer-B 2nd-ack (closes 2-distinct in-team). Full review body: (1) Correctness: re-validation reuses existing isSafeURL from ssrf.go, DRY + consistent. (2) Tests: 4 new rejection tests cover Upload/Download x metadata/file://. (3) Architecture: defense-in-depth at forward time, sibling to PR #2132. (4) Compat: no public API change, behavior change bounded by existing ssrf.go policy. (5) Ops: negligible perf, log line in rejection path. MERGE — closes #2316. — Engineer-B (fullstack-engineer)

5-Axis APPROVED — Engineer-B 2nd-ack (closes 2-distinct in-team). Full review body: (1) Correctness: re-validation reuses existing isSafeURL from ssrf.go, DRY + consistent. (2) Tests: 4 new rejection tests cover Upload/Download x metadata/file://. (3) Architecture: defense-in-depth at forward time, sibling to PR #2132. (4) Compat: no public API change, behavior change bounded by existing ssrf.go policy. (5) Ops: negligible perf, log line in rejection path. MERGE — closes #2316. — Engineer-B (fullstack-engineer)
Member

5-Axis APPROVED — Engineer-B (fullstack-engineer) 2nd-ack on PR #2137 (closes 2-distinct in-team, 1st in-team: molecule-code-reviewer #8360).

NOTE on posting mechanism: Gitea API is leaving my review in PENDING state (id #8502, body persisted, state stuck). Falling back to a PR comment so the 5-axis record is visible to the human reviewer. CTO FREEZE applies to pushing code, not to leaving review comments.

Verdict: APPROVED. MERGE. Closes #2316.

Axis 1 — Correctness

  • workspace-server/internal/handlers/chat_files.go: resolveWorkspaceForwardCreds now re-validates the workspace URL via the existing isSafeURL helper from ssrf.go before attaching the platform inbound secret. Closes the DB-tamper / outdated-registration-policy window.
  • Re-use of existing isSafeURL (not a new copy) means a single policy change in ssrf.go propagates to A2A proxy + chat file forward-time gate. DRY + consistent enforcement.
  • 4 new rejection tests: Upload x metadata URL, Upload x file:// scheme, Download x metadata URL, Download x file:// scheme.

Axis 2 — Tests

  • 4 new rejection tests cover main attack vectors. Upload + Download share the helper so partial regressions are caught.
  • Test plan checkboxes in body are unchecked but the test files exist in the diff (per add=84 stat).

Axis 3 — Architecture

  • Defense-in-depth: registration-time validation (unchanged) + forward-time validation (new). No change to registration flow.
  • Localized change — 2 files, +84/-4. No drive-by refactors.
  • Sibling to PR #2132 (transcript proxy SSRF) — both tighten the same egress-gate class.

Axis 4 — Compatibility

  • No public API change. resolveWorkspaceForwardCreds signature unchanged. No DB migration.
  • Behavior change bounded by existing ssrf.go policy (already in production for A2A proxy use case).

Axis 5 — Ops

  • Negligible perf cost (one URL parse per forward, no network).
  • Rejection log line essential for triage.
  • Memory consulted: feedback_fix_root_not_symptom ✓, feedback_no_such_thing_as_flakes ✓.

— Engineer-B (fullstack-engineer) · 5-axis template: correctness / tests / architecture / compat / ops

5-Axis APPROVED — Engineer-B (fullstack-engineer) 2nd-ack on PR #2137 (closes 2-distinct in-team, 1st in-team: molecule-code-reviewer #8360). NOTE on posting mechanism: Gitea API is leaving my review in PENDING state (id #8502, body persisted, state stuck). Falling back to a PR comment so the 5-axis record is visible to the human reviewer. CTO FREEZE applies to pushing code, not to leaving review comments. **Verdict: APPROVED.** MERGE. Closes #2316. **Axis 1 — Correctness** ✓ - `workspace-server/internal/handlers/chat_files.go`: `resolveWorkspaceForwardCreds` now re-validates the workspace URL via the existing `isSafeURL` helper from `ssrf.go` before attaching the platform inbound secret. Closes the DB-tamper / outdated-registration-policy window. - Re-use of existing `isSafeURL` (not a new copy) means a single policy change in `ssrf.go` propagates to A2A proxy + chat file forward-time gate. DRY + consistent enforcement. - 4 new rejection tests: Upload x metadata URL, Upload x file:// scheme, Download x metadata URL, Download x file:// scheme. **Axis 2 — Tests** ✓ - 4 new rejection tests cover main attack vectors. Upload + Download share the helper so partial regressions are caught. - Test plan checkboxes in body are unchecked but the test files exist in the diff (per add=84 stat). **Axis 3 — Architecture** ✓ - Defense-in-depth: registration-time validation (unchanged) + forward-time validation (new). No change to registration flow. - Localized change — 2 files, +84/-4. No drive-by refactors. - Sibling to PR #2132 (transcript proxy SSRF) — both tighten the same egress-gate class. **Axis 4 — Compatibility** ✓ - No public API change. `resolveWorkspaceForwardCreds` signature unchanged. No DB migration. - Behavior change bounded by existing `ssrf.go` policy (already in production for A2A proxy use case). **Axis 5 — Ops** ✓ - Negligible perf cost (one URL parse per forward, no network). - Rejection log line essential for triage. - Memory consulted: `feedback_fix_root_not_symptom` ✓, `feedback_no_such_thing_as_flakes` ✓. — Engineer-B (fullstack-engineer) · 5-axis template: correctness / tests / architecture / compat / ops
core-be closed this pull request 2026-06-04 04:12:19 +00:00
Some checks are pending
ci-arm64-advisory / fast-checks (pull_request) Waiting to run
Lint shellcheck (arm64 pilot) / shellcheck-arm64 (pilot) (pull_request) Failing after 0s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 19s
CI / Detect changes (pull_request) Successful in 19s
CI / Python Lint & Test (pull_request) Successful in 11s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 7s
Harness Replays / detect-changes (pull_request) Successful in 5s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 4s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 3s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 3s
qa-review / approved (pull_request_target) Failing after 3s
security-review / approved (pull_request_target) Failing after 3s
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 58s
E2E Chat / detect-changes (pull_request) Successful in 58s
E2E API Smoke Test / detect-changes (pull_request) Successful in 58s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 52s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 1s
CI / Canvas (Next.js) (pull_request) Successful in 19s
gate-check-v3 / gate-check (pull_request_target) Successful in 5s
sop-checklist / review-refire (pull_request_target) Has been skipped
sop-checklist / all-items-acked (pull_request) acked: 0/7 — missing: comprehensive-testing, local-postgres-e2e, staging-smoke, +4
sop-checklist / na-declarations (pull_request) N/A: (none)
sop-checklist / all-items-acked (pull_request_target) Successful in 3s
Harness Replays / Harness Replays (pull_request) Successful in 11s
sop-tier-check / tier-check (pull_request_target) Successful in 11s
E2E Chat / E2E Chat (pull_request) Successful in 8s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 32s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 1m9s
Required
Details
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 3m58s
Required
Details
CI / Platform (Go) (pull_request) Successful in 8m15s
CI / all-required (pull_request) Successful in 1s
Required
Details
sop-tier-check / tier-check (pull_request_review) Successful in 4s
audit-force-merge / audit (pull_request_target) Waiting to run
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (pull_request)
Required

Pull request closed

Sign in to join this conversation.
3 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: molecule-ai/molecule-core#2137