Skip to content

fix(security): authorize MCP subagent IDs, oauth workspace, credential admin demotion#4551

Merged
waleedlatif1 merged 2 commits into
stagingfrom
fix/security-copilot-authz
May 11, 2026
Merged

fix(security): authorize MCP subagent IDs, oauth workspace, credential admin demotion#4551
waleedlatif1 merged 2 commits into
stagingfrom
fix/security-copilot-authz

Conversation

@waleedlatif1
Copy link
Copy Markdown
Collaborator

Summary

  • HIGH: Cross-tenant env var leak via MCP subagenthandleSubagentToolCall and handleDirectToolCall were passing user-supplied workflowId/workspaceId directly downstream without authorization. Now authorizes both via authorizeWorkflowByWorkspacePermission (for workflow IDs) and ensureWorkspaceAccess (for workspace-only IDs). resolvedWorkspaceId is always derived from the server-trusted workflow record, not the request body.
  • HIGH: Cross-workspace OAuth credential injectionexecuteOAuthGetAuthLink used context.workspaceId (ultimately user-supplied via MCP args) without verifying the caller is a member. Added ensureWorkspaceAccess(workspaceId, userId, 'write') before generating the OAuth link or writing pendingCredentialDraft.
  • HIGH_BUG: Last-admin demotion lockoutPOST /api/credentials/[id]/members was blindly updating role without checking if it would leave zero admins. Wrapped the update in a transaction that counts active admins and returns 400 Cannot demote the last admin when the count would drop to zero (mirrors the identical guard already in the DELETE handler).
  • MEDIUM: GET existence oracleGET /api/credentials/[id]/members returned 200 { members: [] } for missing credentials but 403 for existing inaccessible ones, leaking existence. Now returns uniform 404 for both cases.

Follow-up (out of scope for this PR)

Several other copilot handlers (jobs.ts, function-execute.ts, materialize-file.ts, management/manage-mcp-tool.ts, management/manage-skill.ts, management/manage-custom-tool.ts) use context.workspaceId without verifying membership — filed as a follow-up sweep. The MCP boundary fix in this PR closes the most exploitable ingress path.

Type of Change

  • Bug fix

Testing

Tested manually

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

…l admin demotion

- handleSubagentToolCall and handleDirectToolCall now authorize user-supplied
  workflowId/workspaceId via authorizeWorkflowByWorkspacePermission /
  ensureWorkspaceAccess before forwarding downstream; resolvedWorkspaceId is
  derived from the authorized workflow record instead of trusted from the body
- executeOAuthGetAuthLink verifies caller membership (write level) on the
  target workspaceId before generating the OAuth link or writing
  pendingCredentialDraft, closing the cross-workspace credential injection path
- POST /api/credentials/[id]/members wraps role updates in a transaction that
  counts active admins and rejects demotion of the last admin (mirrors the
  existing DELETE guard in the same file)
- GET /api/credentials/[id]/members returns uniform 404 for both missing and
  inaccessible credentials to remove the existence oracle
@vercel
Copy link
Copy Markdown

vercel Bot commented May 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped May 11, 2026 1:44am

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented May 11, 2026

PR Summary

High Risk
High risk because it changes authorization/tenant-boundary enforcement for MCP tool routing and OAuth credential link generation, and adds transactional locking to credential member role updates; mistakes could block legitimate access or leave gaps.

Overview
Closes multiple cross-tenant access paths by authorizing user-supplied workflowId/workspaceId in MCP copilot direct/subagent tool calls and only forwarding server-resolved workspace context downstream.

Hardens credential membership management by preventing demotion/removal of the last active admin via transactional row locks, and reduces credential existence leakage by returning uniform 404 for missing or inaccessible credentials.

Adds a workspace membership check before generating OAuth auth links (oauth_get_auth_link) to prevent cross-workspace credential draft creation, and includes a minor regen of MCP tool runtime schema map key syntax.

Reviewed by Cursor Bugbot for commit c0d9961. Configure here.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 11, 2026

Greptile Summary

This PR closes three high-severity authorization gaps in the copilot/MCP layer and credential-member API. No new features are introduced — all changes are security fixes.

  • MCP subagent/direct tool authorization: handleSubagentToolCall and handleDirectToolCall now run authorizeWorkflowByWorkspacePermission (workflow path) or ensureWorkspaceAccess (workspace-only path) before forwarding IDs downstream; resolvedWorkspaceId is always sourced from the DB-trusted workflow record.
  • OAuth workspace guard: executeOAuthGetAuthLink now calls ensureWorkspaceAccess(..., 'write') before generating an OAuth link or writing pendingCredentialDraft, closing cross-workspace credential injection.
  • Last-admin demotion lockout: POST /api/credentials/[id]/members wraps the role update in a transaction with SELECT … FOR UPDATE on both the member row and the admin count, mirroring the guard already present in the DELETE handler. GET now returns a uniform 404 for both missing and inaccessible credentials.

Confidence Score: 5/5

All changed paths correctly fence user-supplied IDs behind server-side authorization checks before any DB writes or downstream forwarding.

The MCP route now derives workspaceId from the DB record rather than trusting user input; the OAuth handler guards its workspace write with an explicit membership check; and the credential demotion path correctly serializes concurrent role changes with row-level locks inside a transaction. The cosmetic key-literal cleanup in tool-schemas-v1.ts carries no risk. No unguarded code paths remain in the changed files.

No files require special attention; all security fixes follow through consistently within their respective files.

Important Files Changed

Filename Overview
apps/sim/app/api/credentials/[id]/members/route.ts GET existence oracle fixed (uniform 404); POST demotion guard wrapped in transaction with FOR UPDATE on both the member row and admin count; DELETE gains FOR UPDATE on the admin count SELECT.
apps/sim/app/api/mcp/copilot/route.ts handleDirectToolCall and handleSubagentToolCall now authorize user-supplied workflowId/workspaceId before forwarding; resolvedWorkspaceId is always derived from the DB record, never from raw user input.
apps/sim/lib/copilot/tools/handlers/oauth.ts Added ensureWorkspaceAccess('write') guard before generating OAuth links; also guards against missing workspaceId/userId in context before calling.
apps/sim/lib/copilot/generated/tool-schemas-v1.ts Cosmetic-only change: computed property keys like ['agent'] simplified to bare identifier keys (agent). No behavioral change.

Reviews (2): Last reviewed commit: "fix(security): address PR review — activ..." | Re-trigger Greptile

Comment thread apps/sim/app/api/credentials/[id]/members/route.ts
Comment thread apps/sim/app/api/mcp/copilot/route.ts
Comment thread apps/sim/app/api/credentials/[id]/members/route.ts
…cks, workspaceId propagation

- credentials/members POST: add `current.status === 'active'` check to the
  last-admin demotion guard so re-inviting a revoked admin as a non-admin role
  no longer incorrectly hits the "Cannot demote the last admin" path
- credentials/members POST+DELETE: add `.for('update')` to the active-admin
  count SELECT inside both transactions to serialize concurrent demotions and
  eliminate the admin-count TOCTOU race under Postgres READ COMMITTED
- credentials/members POST: also lock the member row itself with `.for('update')`
  so the role+status read and the subsequent UPDATE are atomic
- mcp/copilot handleDirectToolCall: thread the DB-verified workspaceId from the
  authorization result into prepareExecutionContext instead of relying on
  user-supplied args
- oauth handler: fix error message to mention both workspaceId and userId when
  either is missing from the execution context
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit c0d9961. Configure here.

@waleedlatif1 waleedlatif1 merged commit d895e0e into staging May 11, 2026
14 checks passed
@waleedlatif1 waleedlatif1 deleted the fix/security-copilot-authz branch May 11, 2026 17:08
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.

1 participant