Skip to content

finding: GET /automation/:name/runs/:runId/screen still discloses record-derived values to any authenticated caller who knows a run id #7968

Description

@hotlong

Observation-class finding, filed unassigned with no pm:queue — for triage to grade and route. Named residual of #7900 (PR pending), deliberately not decided inside that card because closing it needs a different mechanism from the one #7900's maintainer ruling names.

What #7900 closed, and what it left

The 2026-08-12 ruling on #7900 converged the /automation run-state reads on the sys_automation_run object-read grant. GET /:name/runs/:runId and GET /:name/runs now require it. The ruling also asked for the rest of the domain to be audited against the same rule, with a stated reason for anything that stays authenticated-only.

GET /:name/runs/:runId/screen is the one route the audit left authenticated-only, and the reason is on the route in packages/runtime/src/domains/automation.ts:

The residual, measured

The route is not inert with respect to record data. AutomationEngine.getSuspendedScreen(runId) returns (await this.loadSuspendedRun(runId))?.screen — the ScreenSpec persisted with the suspended run — and that spec is built with values interpolated against the live flow variables at suspend time (packages/services/service-automation/src/builtin/screen-nodes.ts: interpolate(cfg.defaults, variables, context) and, per field, interpolate(f.defaultValue, variables, context)).

So a flow whose screen prefills from the triggering record (defaultValue: '{record.email}', a defaults map over {record.*}) writes those values into the spec, and the route answers them to any authenticated caller who knows the run id — there is no check anywhere on this path that the run belongs to, or was paused for, the caller. Same shape as the #7900 exposure, one route over and a narrower payload: interpolated screen defaults rather than the whole variable map.

Not claimed: that this is a regression (it is true of origin/main before #7900 as well), or that it exposes any credential class (the #7900 analysis of connector credentials applies here unchanged).

What closing it would take

Not the sys_automation_run grant — that is the mechanism this route was audited out of, for the reasons above. The shape that fits is the one the resume path already has: a per-run authority read gate, i.e. "may this caller see the screen this run is paused on?", answered from the suspension's own resumeAuthority / assignee state rather than from an object permission. That is a design call about the screen-flow runtime (ADR-0019 territory), not an application of the #7900 ruling, which is why it is a separate card.

Possible cheaper interim, if triage wants one before that design lands: scope the route to the run's own trigger identity (ExecutionLogEntry.trigger.userId) with the sys_automation_run grant as the operator override — narrow, decidable, and it does not refuse the end user. Recorded as an option, not a recommendation; the authority model is the real answer.

Evidence

Measured 2026-08-12 while implementing #7900, on origin/main base 4b5702a, reading packages/runtime/src/domains/automation.ts, packages/services/service-automation/src/engine.ts (getSuspendedScreen) and packages/services/service-automation/src/builtin/screen-nodes.ts.

Source

Split out of #7900 — the implementing dev's named residual, recorded in the route's own comment and in that card's PR audit table.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions