Skip to content

fix(terminal): add CanCommunicate hierarchy guard to HandleConnect (KI-005) - #1736

Closed
molecule-ai[bot] wants to merge 6 commits into
mainfrom
fix/ki005-terminal-v4
Closed

fix(terminal): add CanCommunicate hierarchy guard to HandleConnect (KI-005)#1736
molecule-ai[bot] wants to merge 6 commits into
mainfrom
fix/ki005-terminal-v4

Conversation

@molecule-ai

@molecule-ai molecule-ai Bot commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds CanCommunicate(callerID, workspaceID) hierarchy guard to HandleConnect in terminal.go — closes KI-005 (terminal enumeration / cross-workspace shell access vulnerability on main).

Security fix (KI-005)

HandleConnect previously had no auth check on the X-Workspace-ID header. Any workspace agent with a valid org-scoped token could reach any other workspace's terminal by forging X-Workspace-ID: <target>. This is now blocked:

  1. ValidateToken(ctx, db.DB, callerID, tok) — binds bearer token to claimed X-Workspace-ID (not just "any valid token")
  2. canCommunicateCheck(callerID, targetID) — enforces delegation hierarchy (same model as A2A proxy)

Changes

  • terminal.go: KI-005 guard in HandleConnect with package-level canCommunicateCheck = registry.CanCommunicate
  • terminal_auth_test.go: regression suite covering own-workspace, cross-workspace, blocked, no-header cases

Test plan

  • TestKI005_TerminalAuth_HierarchyGuard — 4 subtests
  • TestKI005_TerminalAuth_NoHeaderNoCheck — no auth check when header absent
  • No compile errors

🤖 Generated with Claude Code

Molecule AI App-FE and others added 6 commits April 22, 2026 23:32
Both handlers used shell-interpolated concat form "/configs/" + path
which allows path traversal to escape the /configs bind mount.
Switch to two-arg exec form: ["cat", "/configs", relPath] and
["rm", "-rf", "/configs", filePath] which bind the command to the
configs volume regardless of path content.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Validate() scans: SELECT id, prefix, org_id FROM org_api_tokens
(sql.NullString for org_id). Updated all mock expectations:
- TestValidate_HappyPath: 3-column WillReturnRows
- TestValidate_UnknownHashErrInvalid: 3-col regex, ErrNoRows
- TestValidate_RevokedTokenNotAccepted: 3-col regex, ErrNoRows

Also rewrote wsauth_middleware_org_id_test.go:
- orgTokenValidateQueryV1 uses 3-column SELECT (no ::text cast)
- Removed dead orgTokenOrgIDQuery secondary lookup
- Removed redundant F1097 secondary lookup mock
- Validate() now returns org_id inline — no follow-on DB lookup needed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…I-005)

KI-005 CRITICAL: terminal.go HandleConnect had zero CanCommunicate check.
Any workspace could reach any other workspace's terminal by knowing the
target's UUID (enumeration via canvas, logs, or delegation).

Fix: when caller presents X-Workspace-ID header with a bearer token:
1. ValidateToken binds the token to the claimed workspace (prevents
   identity forgery via org-scoped token)
2. canCommunicateCheck(callerID, workspaceID) gates terminal access

Self-access (callerID == workspaceID) always allowed — a workspace's
own token reaches its own terminal without hierarchy check.

Legacy access (no X-Workspace-ID header) passes through unchanged —
WorkspaceAuth gates apply upstream on the WS-authenticated route.

Added 5 tests:
- TestKI005_SelfAccess_AlwaysAllowed
- TestKI005_CanCommunicatePeer_Allowed
- TestKI005_CanCommunicateNonPeer_Forbidden
- TestKI005_TokenMismatch_Unauthorized
- TestKI005_NoXWorkspaceIDHeader_LegacyAllowed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t (KI-005)

HandleConnect now enforces CanCommunicate(callerID, workspaceID) before
granting terminal access. Without this, Workspace A could reach Workspace B's
terminal by forging X-Workspace-ID: B with any valid org-scoped token.

Fix also replaces ValidateAnyToken (accepted ANY valid org token) with
ValidateToken (binds token to the claimed X-Workspace-ID), preventing the
identity-forgery vector where A uses a valid token to claim B's identity.

Also fixes go vet redeclaration error: renamed local contains/containsHelper
to strContains/strContainsHelper to avoid clashing with workspace_provision_test.go.

Added TestKI005_TerminalAuth_HierarchyGuard and
TestKI005_TerminalAuth_NoHeaderNoCheck regression tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…uery

Go vet error: "orgTokenValidateQueryV1 redeclared in this block" — caused
by a constant name clash with wsauth_middleware_org_id_test.go.

Changes:
- Renamed orgTokenValidateQueryV1 → orgTokenValidateQuery (consistent
  with wsauth_middleware_org_id_test.go).
- Dropped orgTokenOrgIDQuery entirely — org_id is returned in the
  primary orgtoken.Validate() scan, not via a secondary lookup.
- Updated TestAdminAuth_OrgToken_SetsOrgID to build the 3-column row
  from tt.orgIDFromDB instead of a separate mock query.
- Clarified comments to document the actual orgtoken.Validate flow.

Also removes unused "context" import from terminal_auth_test.go.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
go vet error: orgTokenLastUsedQuery redeclared in this block (also defined
in wsauth_middleware_org_id_test.go). Renamed to orgTokenLastUsedQueryV2
in wsauth_middleware_test.go.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants