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:
- Schedule
ExecutePythonHack via POST /api/v2/clients/<client_id>/scheduled-flows
- Request routine client access approval (the approval UI does not display pending scheduled flows)
- 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)
- 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)
Summary
The
ScheduleFlowmethod inApiCallRouterWithApprovalChecksis missing theadmin_access_checker.CheckIfCanStartFlow()authorization check, allowing non-admin users to schedule admin-restricted flows (ExecutePythonHack,LaunchBinary,UpdateClient). The parallelCreateFlowmethod correctly enforces this check.Root Cause
File:
grr/server/grr_response_server/gui/api_call_router_with_approval_checks.pyCreateFlow(line 542) enforces three authorization checks:ScheduleFlow(line 696) enforces only one:CheckIfCanStartFlow(inaccess_controller.py, line 46) is the sole enforcement point forRESTRICTED_FLOWS— flows that grant arbitrary code execution on endpoints.For comparison,
CreateHunt(line 988) also correctly enforcesCheckIfCanStartFlow.Impact
A non-admin user can:
ExecutePythonHackviaPOST /api/v2/clients/<client_id>/scheduled-flowsGrantClientApproval(user.py:986) automatically callsflow.StartScheduledFlows(), which callsStartFlow()— the execution layer has no authorization checks by design (auth is enforced at the router layer)This is a privilege escalation from standard user to admin-equivalent execution — bypassing the authorization boundary that
RESTRICTED_FLOWSexists to enforce.Proposed Fix
Add the missing checks to
ScheduleFlow:I'm happy to submit a PR with the fix and corresponding test updates.
Reproduction
Requires
ApiCallRouterWithApprovalChecks(the production router). The default Docker config usesApiCallRouterWithoutCheckswhich performs no auth checks.