Skip to content

[security] fix(workflows): enforce subworkflow access checks#93

Merged
mbakgun merged 1 commit into
heymrun:mainfrom
Hinotoi-agent:fix/subworkflow-access-checks
May 10, 2026
Merged

[security] fix(workflows): enforce subworkflow access checks#93
mbakgun merged 1 commit into
heymrun:mainfrom
Hinotoi-agent:fix/subworkflow-access-checks

Conversation

@Hinotoi-agent
Copy link
Copy Markdown
Contributor

Summary

This PR fixes a sub-workflow authorization gap in workflow execution. The root workflow execution path validated the workflow being invoked, but referenced execute-node and agent sub-workflow targets were loaded by UUID only and cached for execution without checking whether the actor was allowed to access each referenced workflow.

The patch threads an actor user id through referenced-workflow collection, checks access for every referenced workflow before caching it, and applies the same boundary across direct workflow execution, streaming execution, dashboard assistant execution, portal execution, MCP workflow execution, and background trigger executions.

Security issues covered

Issue Severity Affected surface Fix
Cross-workflow sub-workflow authorization bypass High Execute nodes and agent subWorkflowIds that reference another workflow by UUID Require actor access before referenced workflows enter the execution cache

Before this PR

  • The execution endpoint validated access to the root workflow only.
  • collect_referenced_workflows() loaded execute-node targets with select(Workflow).where(Workflow.id == uuid.UUID(target_id)).
  • Agent subWorkflowIds used the same UUID-only lookup.
  • Referenced workflows were cached and then executed by the workflow executor without a per-target access check.
  • Unauthorized referenced workflows were not distinguished from authorized workflows before being added to the cache.

After this PR

  • collect_referenced_workflows() accepts an actor_user_id for access decisions.
  • Every referenced execute-node or agent sub-workflow target must pass user_has_workflow_access() before it is cached.
  • Unauthorized referenced workflows fail closed with 403 Forbidden.
  • Execution entry points pass the user or owner context that will execute the workflow.
  • Regression tests cover both execute-node and agent sub-workflow references to workflows the actor cannot access.

Why this matters

Workflow UUIDs are authorization-sensitive. If a user can create and execute their own workflow, and referenced workflows are loaded by UUID only, the user can make their workflow point at another user's workflow once they learn or obtain its id.

That can expose target workflow outputs and can also trigger workflow nodes with owner-intended side effects through an attacker-controlled root execution path.

Attack flow

  1. Attacker has an account and can create/execute a workflow they own.
  2. Attacker learns or obtains a victim workflow UUID.
  3. Attacker creates either:
    • an execute node with data.executeWorkflowId set to the victim UUID, or
    • an agent node with data.subWorkflowIds containing the victim UUID.
  4. Attacker runs their own workflow.
  5. The root workflow access check passes because the attacker owns the root workflow.
  6. The referenced victim workflow is loaded by UUID and cached for execution.
  7. The executor runs the referenced workflow and returns/stores sub-workflow outputs in the attacker-triggered execution path.

Affected code

  • backend/app/api/workflows.py
    • collect_referenced_workflows()
    • execute_workflow_endpoint()
    • execute_workflow_stream()
  • Other execution entry points that build workflow caches:
    • backend/app/api/ai_assistant.py
    • backend/app/api/portal.py
    • backend/app/api/mcp.py
    • backend/app/api/mcp_servers.py
    • backend/app/api/slack.py
    • backend/app/api/telegram.py
    • backend/app/services/cron_scheduler.py
    • backend/app/services/imap_trigger_service.py
    • backend/app/services/rabbitmq_consumer.py
    • backend/app/services/websocket_trigger_service.py

Root cause

  • Root workflow authorization was treated as sufficient for execution.
  • Referenced workflows crossed from user-controlled workflow JSON into privileged execution cache lookup without a per-target authorization predicate.
  • The workflow cache trusted UUID references instead of reusing the existing owner/share/team-share access model for each target.

CVSS assessment

  • CVSS v3.1: 7.1 High
  • Vector: CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:L
  • Rationale: exploitation is network-reachable for an authenticated low-privilege user and requires no user interaction. The UUID knowledge prerequisite raises attack complexity. Successful exploitation can disclose referenced workflow outputs and can trigger workflow side effects, depending on the victim workflow's nodes.

Safe reproduction steps

On vulnerable code:

  1. Create user A and user B.
  2. As user B, create a workflow that returns a recognizable output.
  3. As user A, create a separate workflow with an execute node whose executeWorkflowId is user B's workflow id, or an agent node whose subWorkflowIds contains that id.
  4. Execute user A's workflow.
  5. Observe that the referenced user B workflow is loaded into the cache and can be executed without user A having owner/share/team-share access.

This PR adds focused unit coverage for both execute-node and agent sub-workflow reference rejection.

Expected vulnerable behavior

A user who can execute their own workflow can cause a referenced workflow owned by another user to be loaded and executed by UUID, even when the user has no access to that referenced workflow.

Changes in this PR

  • Add _add_referenced_workflow_to_cache() to centralize referenced-workflow loading and access enforcement.
  • Add actor_user_id to collect_referenced_workflows().
  • Reuse user_has_workflow_access() for referenced workflow authorization.
  • Fail closed with 403 Forbidden when a referenced workflow exists but is not accessible to the actor.
  • Pass the appropriate actor/owner id from all workflow-cache-building execution paths.
  • Add regression tests for execute-node and agent sub-workflow unauthorized references.

Files changed

Category Files What changed
Authorization hardening backend/app/api/workflows.py Adds referenced-workflow access checks and passes actor ids from primary execution endpoints
Execution entry points backend/app/api/ai_assistant.py, backend/app/api/portal.py, backend/app/api/mcp.py, backend/app/api/mcp_servers.py, backend/app/api/slack.py, backend/app/api/telegram.py, trigger services Pass actor/owner context when collecting referenced workflows
Tests backend/tests/test_workflow_execution_api.py Adds execute-node and agent sub-workflow authorization regression tests

Maintainer impact

This preserves normal sub-workflow execution for workflows the actor owns or can access through existing individual/team share mechanisms. The behavior change is limited to references that point to workflows the actor is not allowed to access.

If a deployment intentionally relies on owner-run background triggers, those paths pass the workflow owner as the actor so owner-owned sub-workflow composition continues to work.

Fix rationale

The referenced workflow cache is the boundary immediately before sub-workflow execution. Enforcing access there prevents both execute-node and agent sub-workflow references from bypassing the root workflow authorization check, and it keeps the fix centralized instead of relying on every caller to pre-filter node JSON.

Type of change

  • Security fix
  • Bug fix
  • Tests added or updated
  • Documentation update
  • Breaking change

Test plan

Commands run:

cd backend
ENCRYPTION_KEY=<64-byte-test-key> uv run pytest tests/test_workflow_execution_api.py::CollectReferencedWorkflowsAccessTests -q
ENCRYPTION_KEY=<64-byte-test-key> uv run pytest tests/test_workflow_execution_api.py tests/test_mcp_workflow_traces.py tests/test_cancellation_by_trigger.py tests/test_cron_scheduler.py tests/test_websocket_trigger_service.py tests/test_imap_trigger_service.py -q
ENCRYPTION_KEY=<64-byte-test-key> uv run ruff check app/api/workflows.py app/api/ai_assistant.py app/api/portal.py app/api/telegram.py app/api/slack.py app/api/mcp_servers.py app/api/mcp.py app/services/cron_scheduler.py app/services/websocket_trigger_service.py app/services/imap_trigger_service.py app/services/rabbitmq_consumer.py tests/test_workflow_execution_api.py
ENCRYPTION_KEY=<64-byte-test-key> ./run_tests.sh

Result:

  • New focused authorization tests: 3 passed
  • Related workflow/MCP/trigger tests: 60 passed
  • Backend test suite: 741 passed
  • Ruff check passed for touched files

Note: ./check.sh could not complete in this local environment because bun is not installed (./check.sh: line 10: bun: command not found).

Disclosure notes

This PR is intentionally bounded to sub-workflow authorization for referenced workflows. It does not claim broader changes to workflow UUID secrecy, sharing semantics, or node-level sandboxing.

@mbakgun mbakgun merged commit 3ae3ef6 into heymrun:main May 10, 2026
1 check passed
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.

2 participants