Skip to content

ScheduleFlow missing CheckIfCanStartFlow authorization check (privilege escalation) #1160

@Alearner12

Description

@Alearner12

Summary

The ScheduleFlow method in ApiCallRouterWithApprovalChecks is missing the admin_access_checker.CheckIfCanStartFlow() authorization check, allowing non-admin users to schedule admin-restricted flows (ExecutePythonHack, LaunchBinary, UpdateClient). The parallel CreateFlow method correctly enforces this check.

Root Cause

File: grr/server/grr_response_server/gui/api_call_router_with_approval_checks.py

CreateFlow (line 542) enforces three authorization checks:

def CreateFlow(self, args, context=None):
    self.approval_checker.CheckClientAccess(context, args.client_id)        # ✅
    self.admin_access_checker.CheckIfCanStartFlow(                          # ✅ enforces RESTRICTED_FLOWS
        context.username, args.flow.name or args.flow.runner_args.flow_name
    )
    self.mitigation_flows_access_checker.CheckIfHasAccessToFlow(            # ✅
        context.username, args.flow.name or args.flow.runner_args.flow_name
    )
    return self.delegate.CreateFlow(args, context=context)

ScheduleFlow (line 696) enforces only one:

def ScheduleFlow(self, args, context=None):
    self.mitigation_flows_access_checker.CheckIfHasAccessToFlow(            # ✅
        context.username, args.flow.name or args.flow.runner_args.flow_name
    )
    return self.delegate.ScheduleFlow(args, context=context)                # ❌ No CheckIfCanStartFlow
                                                                            # ❌ No CheckClientAccess

CheckIfCanStartFlow (in access_controller.py, line 46) is the sole enforcement point for RESTRICTED_FLOWS — flows that grant arbitrary code execution on endpoints.

For comparison, CreateHunt (line 988) also correctly enforces CheckIfCanStartFlow.

Impact

A non-admin user can:

  1. Schedule ExecutePythonHack via POST /api/v2/clients/<client_id>/scheduled-flows
  2. Request routine client access approval (the approval UI does not display pending scheduled flows)
  3. When an admin grants the approval, GrantClientApproval (user.py:986) automatically calls flow.StartScheduledFlows(), which calls StartFlow() — the execution layer has no authorization checks by design (auth is enforced at the router layer)
  4. The restricted flow executes with SYSTEM/root privileges on the target endpoint

This is a privilege escalation from standard user to admin-equivalent execution — bypassing the authorization boundary that RESTRICTED_FLOWS exists to enforce.

Proposed Fix

Add the missing checks to ScheduleFlow:

def ScheduleFlow(self, args, context=None):
    self.approval_checker.CheckClientAccess(context, args.client_id)        # ADD
    self.admin_access_checker.CheckIfCanStartFlow(                          # ADD
        context.username, args.flow.name or args.flow.runner_args.flow_name # ADD
    )                                                                       # ADD
    self.mitigation_flows_access_checker.CheckIfHasAccessToFlow(
        context.username, args.flow.name or args.flow.runner_args.flow_name
    )
    return self.delegate.ScheduleFlow(args, context=context)

I'm happy to submit a PR with the fix and corresponding test updates.

Reproduction

Requires ApiCallRouterWithApprovalChecks (the production router). The default Docker config uses ApiCallRouterWithoutChecks which performs no auth checks.

# docker_config_files/server/grr.server.yaml
API.DefaultRouter: ApiCallRouterWithApprovalChecks
AdminUI.webauth_manager: BasicWebAuthManager
# Create admin and non-admin users
docker exec grr-admin-ui grr_config_updater --config /configs/server/grr.server.yaml add_user admin --password admin123 --admin true
docker exec grr-admin-ui grr_config_updater --config /configs/server/grr.server.yaml add_user analyst --password analyst123 --admin false

# Get CSRF token
CSRF=$(curl -s -v -u analyst:analyst123 http://localhost:8000/ 2>&1 | grep "Set-Cookie: csrftoken=" | sed 's/.*csrftoken=//;s/;.*//')

# Test 1: CreateFlow blocks non-admin (expected — working correctly)
curl -s -u analyst:analyst123 -H "x-csrftoken: $CSRF" -b "csrftoken=$CSRF" \
  -H "Content-Type: application/json" \
  -X POST "http://localhost:8000/api/v2/clients/C.1234567890abcdef/flows" \
  -d '{"flow":{"name":"ExecutePythonHack","args":{}}}'
# Result: "Access denied by ACL"

# Test 2: ScheduleFlow does NOT block non-admin (bug)
curl -s -u analyst:analyst123 -H "x-csrftoken: $CSRF" -b "csrftoken=$CSRF" \
  -H "Content-Type: application/json" \
  -X POST "http://localhost:8000/api/v2/clients/C.1234567890abcdef/scheduled-flows" \
  -d '{"flow":{"name":"ExecutePythonHack","args":{}}}'
# Result: Passes router, reaches database layer (FK error on unenrolled client — NOT an auth denial)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions