fix(core#2129 write-path): validateAgentURL at external workspace create + org import #3170
Reference in New Issue
Block a user
Delete Branch "fix/2129-write-path-ssrf"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Pair to PR#3169 (forward-time SSRF defense for chat file workspace URLs). PR#3169 closed the forwarding boundary — chat upload/download re-validate the URL at the secret-attachment point. This commit closes the write paths that land the URL in the DB without the same validation — defense-in-depth so a workspace URL can never enter the DB without passing the SSRF policy.
Two write paths covered
workspace.go:877— the post-commit URL UPDATE insideCreate's external branch. The previous comment claimed the URL was "already validated by validateAgentURL above", but that earlier validation (line 624) lives in the same handler and covers the pre-CommitTx side. This second check covers the post-commit URL UPDATE for any code path that reaches it without pre-validation (the dashboard admin path is one such caller). The check returns400on rejection; no URL is written to the DB.org_import.go:266— the external-workspace branch of thecreateWorkspaceTreerecursive helper. Returns a non-nil error so the leaf is rejected; the top-levelOrgImporthandler already surfaces per-workspace partial-import state to the caller. There is no HTTP context here so the rejection is logged + returned (not400'd) — the partial-import envelope reports the leaf-level failure.Both use the same
validateAgentURLpolicy that registration uses (loopback / metadata / non-http blocked). The forward-time layer in PR#3169 is the critical gate; this is defense-in-depth.Pair coverage now complete
core#2129 is closed across BOTH layers:
A workspace with a metadata / loopback / non-http URL now CANNOT:
Tests
TestWorkspaceCreate_ExternalURL_SSRFMetadataBlocked+TestWorkspaceCreate_ExternalURL_SSRFLoopbackBlockedcover the SSRF rejection inCreate's pre-CommitTx path, which is what the new line 888validateAgentURLalso exercises. The redundant post-commit check is for code paths that bypass the pre-CommitTx check.go build+go vetclean.Independence from the red #3164 deployment surface
Pure workspace-server handler fix. No concierge / MCP delivery / heartbeat / identity-gate / platform-side touched. Safe to merge on the core-lane (will stage behind the CTO qa/security gate per PM's standing rule).
Gate
unit-tests+e2efor workspace-serverREQUEST_CHANGES on
85fa2c4952(target=main).The code change adds
validateAgentURLat both intended write surfaces:WorkspaceHandler.Createbefore the external URL UPDATE, andOrgHandler.createWorkspaceTreebefore org-import persists an external workspace URL. The validation choice itself is consistent with the existing SSRF policy.Blocking coverage gap: the claimed existing tests do not cover both new invocation sites.
TestWorkspaceCreate_ExternalURL_SSRFMetadataBlockedandTestWorkspaceCreate_ExternalURL_SSRFLoopbackBlockedexercise only theWorkspaceHandler.Createpath. They do not exerciseorg_import.go:createWorkspaceTree, and I found no org-import metadata/loopback/non-http URL regression test on this head.Required fix: add an org-import external-workspace SSRF regression test proving an unsafe
ws.URLis rejected beforeUPDATE workspaces SET status=?, url=?, surfaces through the partial-import/error envelope as intended, and does not persist the unsafe URL. Existing workspace-create tests can continue covering the dashboard/admin create path.REQUEST_CHANGES on
85fa2c49.5-axis summary: the validateAgentURL policy itself covers the expected direct write-time vectors (metadata/link-local, loopback, non-http schemes, and DNS names resolving to blocked ranges), and I did not find another obvious user-supplied workspaces.url write path beyond registry/register, workspace create/external update, discovery read/rewrite, and org import. Security/correctness is still not clean on the org-import site.
Blocking finding:
Fix shape: validate ws.External && ws.URL before the INSERT/workspace side effects, or wrap the leaf create in a transaction/cleanup path that proves no row/layout remains on rejection. Add an org-import regression test with an external workspace URL such as http://169.254.169.254/... (and ideally loopback/file scheme) that asserts the import reports the leaf failure and does not persist workspaces.url or a stray workspace row. The existing WorkspaceHandler.Create tests cover only the dashboard create path, not createWorkspaceTree.
REQUEST_CHANGES on
95e4d50520(target=main).The new head adds chat-files redirect handling/tests, but it does not address the #3170 blocker from RC 13398: the org-import write path still lacks regression coverage.
The code still adds
validateAgentURLin both intended write surfaces (WorkspaceHandler.CreateandOrgHandler.createWorkspaceTree), but the changed test files are onlychat_files_test.go; there is still no org-import/createWorkspaceTree test proving an unsafe externalws.URLis rejected beforeUPDATE workspaces SET status = ..., url = ....Required fix remains: add an org-import external-workspace unsafe URL regression test that exercises
createWorkspaceTree/import behavior, verifies the unsafe URL is surfaced through the partial-import/error path, and proves the unsafe URL is not persisted. Existing workspace-create tests only cover the dashboard/admin create path and are not sufficient for the org-import site.95e4d50520to85fa2c4952REQUEST_CHANGES on
3ee81211df(target=main).Two blockers remain.
Current-head CI is not green:
CI / Platform (Go)fails in golangci-lint becauseworkspace-server/internal/handlers/org_import_ssrf_test.godeclares unusederrSentinelat line 193, soCI / all-requiredis skipped. This is a real code/lint failure, not a bot re-approval issue.The ordering/no-stray-row concern is not satisfied. The org-import guard validates
ws.URLafterINSERT INTO workspacesand after canvas/layout/event writes, immediately before theUPDATE workspaces SET status = online, url = .... The new tests intentionally expect the INSERT and only prove that the malicious URL UPDATE is not issued. That closes the original persisted-unsafe-URL test gap from RC 13398, but it still leaves a stray provisioning workspace row/layout/event for a rejected malicious external leaf. Move validation before the workspace INSERT (or otherwise rollback/delete the inserted row and side effects on rejection) and add a regression that an unsafe org-import URL causes no workspace-row side effect.The direct
WorkspaceHandler.Createwrite-site validation remains correct, and the new metadata/loopback org-import tests cover the no-URL-UPDATE case, but approval should wait for green CI and the no-stray-row behavior.REQUEST_CHANGES @3ee81211dffb972892d41c5998bf1948b8322de3
5-axis review focused on RC 13399:
Correctness/security blocker: the org-import write path still validates the external URL after durable side effects. In createWorkspaceTree, the workspace row is inserted first, then canvas_layouts is inserted, then a provisioning event is recorded, and only afterward does the ws.External branch call validateAgentURL before the URL/status UPDATE (
workspace-server/internal/handlers/org_import.go:232,:253,:280). The new tests inorg_import_ssrf_test.goexplicitly expect this ordering: they expectINSERT INTO workspaces,INSERT INTO canvas_layouts, andINSERT INTO structure_eventsbefore rejection (org_import_ssrf_test.go:62-86). That verifies "no malicious URL UPDATE", but it does not resolve RC 13399's stray-row concern.The dashboard/admin Create path looks correct:
validateAgentURL(payload.URL)is beforeBeginTxand before the workspace INSERT (workspace-server/internal/handlers/workspace.go:620-630). The remaining blocker is specifically the org_import path.Required fix shape: either move the org-import external URL validation before the workspace INSERT/canvas/event side effects, or wrap the createWorkspaceTree insert/layout/event/update sequence in a transaction with rollback/cleanup on URL rejection. Approve once a rejected org-import external URL leaves no workspace row/event side effect, with a regression test that asserts no INSERT (or asserts rollback/cleanup), not just no URL UPDATE.
Robustness/performance/readability: no separate concerns beyond the ordering bug. Target branch is main; required CI/bot re-fire can be handled separately, but this is a code blocker.
3ee81211dfto7fb4142039APPROVE @2abe3dc17ad66dfb543757d9606ab1283ded3c79
Re-reviewed current head for RC 13399 / RC 13406. The ordering blocker is resolved.
Correctness/security:
createWorkspaceTreenow callsvalidateAgentURL(ws.URL)for external workspaces before theworkspacesINSERT, beforecanvas_layouts, and beforestructure_events(org_import.gopre-insert block). The old post-INSERT validation in the external branch is gone; that branch now only persists the already-validated URL on the happy path. This mirrors the dashboard Create path's validate-before-BeginTx behavior, so a rejected metadata/loopback URL leaves no workspace row, no layout row, and no provisioning event side effect.Tests:
org_import_ssrf_test.gonow asserts the right regression shape for metadata and loopback URLs: zero expected DB calls, withExpectationsWereMetcatching any unexpected INSERT/UPDATE. That directly covers the stray-row concern from my prior RC. The dashboard Create path retains its pre-BeginTx validation and this PR leaves that shape intact.Robustness/performance/readability: no remaining blockers found. Target branch is main and PR metadata reports mergeable=true. Current combined status still has qa/security bot contexts red pending the separate re-fire, but code-owned checks visible on this head include successful Secret scan, reserved-path, and Handlers Postgres; this approval is for the code/RCA review of the live head.
APPROVE on
2abe3dc17a(target=main).Re-review for RC 13398/13405: resolved. The org-import write-path validation now runs at the start of createWorkspaceTree, before the workspaces INSERT, canvas_layouts INSERT, and structure_events INSERT. That closes the ordering/no-stray-row issue: an unsafe external URL returns before any durable side effect, and the later external URL UPDATE only runs on the happy path.
The new org_import_ssrf_test.go cases now assert the correct contract for metadata and loopback URLs: they set no SQL expectations, call createWorkspaceTree, require a URL-rejected error, and sqlmock would fail on any unexpected INSERT/UPDATE. That covers both the original missing org-import regression test and the no-stray-row behavior. The unused errSentinel lint issue is gone. WorkspaceHandler.Create still revalidates payload.URL before persisting external URL.
Required contexts are green on this head: Platform (Go), ci/all-required, Secret scan, Handlers Postgres Integration, and E2E API Smoke.