Skip to content

fix(security): close cross-tenant IDOR gaps in OAuth credential and execution auth#4549

Open
waleedlatif1 wants to merge 7 commits intostagingfrom
fix/security-idor-auth
Open

fix(security): close cross-tenant IDOR gaps in OAuth credential and execution auth#4549
waleedlatif1 wants to merge 7 commits intostagingfrom
fix/security-idor-auth

Conversation

@waleedlatif1
Copy link
Copy Markdown
Collaborator

Summary

  • Several tool routes (gmail/label, onedrive/files, onedrive/folder, outlook/folders, wealthbox/item, auth/oauth/wealthbox/item) called resolveOAuthAccountId then only checked workspace permissions when workspaceId was set — the legacy account-ID fallback path returned no workspaceId, silently skipping all ownership validation. Any authenticated user could supply a raw account.id to access another tenant's OAuth credentials
  • Replace all instances with authorizeCredentialUse, which enforces ownership on every credential type including the legacy fallback path
  • Add authorizeCredentialUse ownership gate before resolveVertexCredential in providers/route.ts (Vertex AI credential path had the same gap)
  • Add verifyFileAccess check in wordpress/upload before downloadFileFromStorage — file key was user-supplied with no ownership verification
  • Add optional workflowId filter to all PauseResumeManager methods (enqueueOrStartResume, beginPausedCancellation, etc.) — previously looked up paused executions by executionId only, allowing any authenticated user to cancel or resume another tenant's paused workflow execution; update all call sites (cancel, resume, poll routes)

Type of Change

  • Bug fix

Testing

Tested manually. authorizeCredentialUse backwards-compatibility verified: legacy account-ID callers who own their credentials still work (ownership enforced); HITL manager workflowId param is optional so internal self-calls without it are unaffected; verifyFileAccess confirmed to exist and match usage.

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)

…orization

Routes that called resolveOAuthAccountId followed by a conditional
workspace permission check (only run when workspaceId was set) silently
skipped all ownership validation on the legacy account-ID fallback path.
Any authenticated user could supply a raw account.id to access another
tenant's OAuth credentials.

- Replace resolveOAuthAccountId + conditional perm check with
  authorizeCredentialUse in: auth/oauth/wealthbox/item, tools/gmail/label,
  tools/onedrive/files, tools/onedrive/folder, tools/outlook/folders,
  tools/wealthbox/item (routes 1, 3-7)
- Add authorizeCredentialUse ownership gate before resolveVertexCredential
  in providers/route.ts (route 2)
- Add verifyFileAccess check on the user-supplied file key before
  downloadFileFromStorage in tools/wordpress/upload (route 8)
- Add workflowId param to PauseResumeManager methods
  (enqueueOrStartResume, beginPausedCancellation, completePausedCancellation,
  blockQueuedResumesForCancellation, clearPausedCancellationIntent,
  getPausedCancellationStatus, processQueuedResumes) and filter all
  pausedExecutions lookups by workflowId so callers cannot act on another
  tenant's paused execution by supplying a foreign executionId (route 9)
- Update all call sites (cancel, resume, poll routes) to pass workflowId
@vercel
Copy link
Copy Markdown

vercel Bot commented May 10, 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 3:36am

Request Review

@cursor
Copy link
Copy Markdown

cursor Bot commented May 10, 2026

PR Summary

High Risk
High risk because it changes authorization/credential access paths and pause/resume/cancel execution flows, which are security-critical and could block legitimate requests if the new checks are misapplied.

Overview
Closes cross-tenant IDOR gaps by replacing per-route resolveOAuthAccountId + ad-hoc checks with centralized authorizeCredentialUse gating in multiple OAuth-backed tool routes (Gmail, OneDrive, Outlook, Wealthbox) and the Wealthbox OAuth item endpoint.

Adds an explicit credential-ownership check before resolving Vertex credentials in providers requests, and adds verifyFileAccess enforcement to wordpress/upload before downloading from storage.

Hardens HITL pause/resume/cancel by threading workflowId through PauseResumeManager queue/cancellation APIs and updating resume, poll, and cancel routes to scope paused-execution lookups by (executionId, workflowId); also normalizes error handling to use toError() in a couple of routes.

Reviewed by Cursor Bugbot for commit 86f3a53. Configure here.

Comment thread apps/sim/app/api/tools/wordpress/upload/route.ts
Comment thread apps/sim/lib/workflows/executor/human-in-the-loop-manager.ts
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 10, 2026

Greptile Summary

This PR closes cross-tenant IDOR vulnerabilities in OAuth credential access and paused-execution management by centralizing auth enforcement in authorizeCredentialUse and adding workflowId scoping to all PauseResumeManager database operations.

  • Credential access: Replaces the ad-hoc resolveOAuthAccountId + conditional workspace-permission pattern in 6 routes with authorizeCredentialUse, which enforces ownership on every credential type including the legacy account-ID fallback path. The credential-access.ts changes also remove the now-redundant if (actingUserId) guards since actingUserId is always defined after the top-level auth assertion.
  • HITL cancellation/resume: Adds workflowId to every PauseResumeManager method and threads it through every external API call site and all internal call sites within the manager itself.
  • WordPress upload: Gates downloadFileFromStorage behind verifyFileAccess when a storage key is present, with a hard failure when userId is unavailable.

Confidence Score: 5/5

Safe to merge — all credential and execution paths now enforce ownership on every code branch, with no identified regressions.

All call sites of processQueuedResumes and the cancellation methods have been updated to pass workflowId, including the previously flagged internal sites inside startResumeExecution, persistPauseResult, and clearPausedCancellationIntent. The authorizeCredentialUse refactor is consistent across all six tool routes and the Vertex provider path. The credential-access.ts guard removal is safe because actingUserId is always defined after the top-level auth assertion. No new gaps were found.

No files require special attention. The HITL manager and credential-access module are the highest-impact files and both look correct.

Important Files Changed

Filename Overview
apps/sim/lib/auth/credential-access.ts Removes dead if (actingUserId) guards so membership and workspace-permission checks are now unconditional on all credential paths — closes the legacy account-ID IDOR gap.
apps/sim/lib/workflows/executor/human-in-the-loop-manager.ts Adds required workflowId parameter to all six paused-execution DB methods and threads it through every internal call site.
apps/sim/app/api/workflows/[id]/executions/[executionId]/cancel/route.ts Passes workflowId to all PauseResumeManager cancellation calls; replaces untyped error.message with toError(error).message.
apps/sim/app/api/resume/[workflowId]/[executionId]/[contextId]/route.ts Forwards workflowId to enqueueOrStartResume and the error-recovery processQueuedResumes call.
apps/sim/app/api/tools/wordpress/upload/route.ts Adds verifyFileAccess gate before downloadFileFromStorage when a storage key is present.
apps/sim/app/api/providers/route.ts Adds authorizeCredentialUse gate before resolveVertexCredential, closing the Vertex AI credential IDOR gap.

Reviews (5): Last reviewed commit: "fix(security): thread workflowId through..." | Re-trigger Greptile

Comment thread apps/sim/app/api/tools/onedrive/folder/route.ts Outdated
Comment thread apps/sim/app/api/tools/wordpress/upload/route.ts Outdated
…ocessQueuedResumes, fix log level

- Fail closed in WordPress upload when userFile.key is present but authResult.userId is absent, preventing silent bypass of ownership check via JWT fallback path
- Thread workflowId into processQueuedResumes in the async resume error-recovery path and in pause-persistence.ts to close residual cross-tenant gap
- Change logger.error to logger.warn for credential access denial in OneDrive folder route to match all other routes in this PR
@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 94bc2e2. Configure here.

…l sites

Closes residual cross-tenant IDOR gap where processQueuedResumes was called
without a workflowId scope in persistPauseResult, startResumeExecution (success
and error paths), and clearPausedCancellationIntent. workflowId was already in
scope at each site — this wires it through to the existing optional parameter.
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

Follow-up fix (fd234ac): threaded workflowId through the remaining 4 processQueuedResumes call sites in human-in-the-loop-manager.ts that were still missing it — persistPauseResult (line 267), startResumeExecution success path (line 507), startResumeExecution error path (line 535), and clearPausedCancellationIntent (line 1692). In all cases workflowId was already in scope; this just wires it through to the existing optional parameter. All 4 call sites now pass tenant scope — no remaining gaps.

…caught errors

- catch (error: any) → catch (error) + toError(error).message in resume and cancel routes
- Remove what-not-why inline comments from wordpress upload and onedrive/files routes
- Remove redundant debug-only item breakdown log and the file-IDs log in onedrive/files
- Trim extraneous DAG-edge comments from updateSnapshotAfterResume in HITL manager
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

Quality pass complete (0d4c9c6):

  • catch (error: any)catch (error) + toError(error).message in both the resume route and cancel route — no more type escape hatch on caught errors
  • WordPress upload: removed 4 what-not-why inline comments (Process file, Use provided filename, Upload to WordPress using multipart, Add optional metadata)
  • OneDrive files: collapsed the debug item-breakdown reduce+log and the file-IDs log into a single structured info log; consolidated the URL-branch comments into one truthful line; simplified the .filter() call
  • HITL manager (updateSnapshotAfterResume): removed 3 redundant DAG-edge comments

All 4 original review threads were already resolved before this pass. Lint is clean.

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

Comment thread apps/sim/app/api/tools/outlook/folders/route.ts Outdated
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

Comment thread apps/sim/lib/workflows/executor/human-in-the-loop-manager.ts
All 7 method signatures (processQueuedResumes, enqueueOrStartResume,
beginPausedCancellation, completePausedCancellation, blockQueuedResumesForCancellation,
clearPausedCancellationIntent, getPausedCancellationStatus) previously accepted
workflowId as optional. Every call site already supplies it — making it required
closes the vulnerability at the type level so future callers cannot accidentally
omit tenant scoping and silently fall back to an unscoped DB query.
…alls and remove dead branches in credential-access
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 86f3a53. Configure here.

.where(
and(
eq(pausedExecutions.executionId, executionId),
...(workflowId ? [eq(pausedExecutions.workflowId, workflowId)] : []),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Required workflowId silently skipped when empty string passed

Medium Severity

Several PauseResumeManager methods declare workflowId as a required string parameter but internally use falsy checks (workflowId ? ... : ... or ...(workflowId ? [...] : [])) to conditionally apply the filter. Since empty string "" is falsy in JavaScript, an empty-string workflowId silently degrades to the old executionId-only lookup, bypassing the cross-tenant scoping this PR intends to enforce. The type system won't catch this because "" satisfies string. Methods affected include beginPausedCancellation, completePausedCancellation, blockQueuedResumesForCancellation, clearPausedCancellationIntent, processQueuedResumes, and enqueueOrStartResume.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 86f3a53. Configure here.

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