fix(flows): close two authorization boundaries in flow-run tools - #5287
Conversation
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 42 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (7)
Comment |
|
Review follow-up: no code change, but I corrected this PR's own description, which overstated what the ownership check buys. The summary implied the The real boundary is The in-code comments were already honest about this; the PR summary was the part that drifted. Fixed there rather than left to a reader to discover — overstating a security fix is its own kind of bug. |
There was a problem hiding this comment.
graycyrus has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
…2, T-M3)
T-M2: flow_memory_remember trusted the model-supplied flow_id whenever a
call had no trusted Workflow run origin (every chat/orchestrator turn) —
a prompt-injected chat turn could poison another flow's private dedup
memory, since the tool has no external_effect and never parks for
approval. Now refuses outright outside a trusted workflow run instead of
falling back to the arg. Corrected the tools/ops.rs registration comment
that claimed this was already impossible.
T-M3: cancel_flow_run cancelled an arbitrary run_id with no ownership
check and external_effect() == false, so it never parked and was only
hidden on two of several reachable paths. It now requires the caller to
name the owning flow_id, refuses on mismatch against the run's actual
flow_id, and reports external_effect() == true so the approval gate
parks it wherever one exists — mirroring resume_flow_run's existing
{ flow_id, run_id } ownership shape rather than inventing a new one.
Both flows_build hide-lists are deliberately left UNCHANGED. Hardening
the tool makes it safe to unhide on the copilot path, but unhiding is a
capability expansion — it would newly let an authoring turn tear down a
run the *user* started — and that product decision has not been taken.
A security fix should close a hole, not open a door as a side effect;
the entry can be dropped whenever that call is made, and the hardening
here is what makes doing so safe. It stays hidden on the headless path
regardless, since a Cli-trusted origin auto-allows external_effect tools.
No caller depended on the removed chat-side flow_memory_remember
fallback (grepped for callers before removing it); tinyflows's own
memory-node adapter already used the fail-closed pattern this mirrors.
Adds an ApprovalGate integration test proving the now-external-effect
cancel_flow_run genuinely parks and times out to Deny rather than
auto-executing. 554 flows tests pass.
9e59c8c to
60ffd47
Compare
There was a problem hiding this comment.
graycyrus has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.
Summary
flow_memory_rememberno longer trusts a model-suppliedflow_idoutside a workflow run — a prompt-injected chat turn could poison another flow's private dedup memory.cancel_flow_runnow reportsexternal_effect() == trueso it parks for approval instead of cancelling any run unapproved — that is the real boundary — plus an ownership check as defence-in-depth (see the honest scoping note below).tools/ops.rsthat asserted an invariant the code did not hold.Problem
T-M2 — cross-flow memory poisoning from chat. Inside a run,
FlowMemoryRememberToolresolves the namespace-governing flow id from the trustedTrustedAutomation { Workflow }turn origin and ignores the arg — airtight. But whentrusted_flow_id()returnsNone, which is every chat/orchestrator turn, it fell back to the model-suppliedflow_idand wrote into that flow's namespace.The registration comment in
tools/ops.rsclaimed "there is no code path by which either tool can address a namespace other than the calling flow's own." That was true only inside runs.Concretely: a prompt-injected chat turn calls
flow_memory_remember { flow_id: "<digest-flow-id>", key: "sent_item_42", … }and the digest flow's dedup memory now says item 42 was already sent, so it silently skips it forever. The tool has noexternal_effect, so this never parked for approval.T-M3 —
cancel_flow_runhad no ownership check. It cancelled an arbitraryrun_id(its own doc comment said so), reportedexternal_effect() == falseso the approval gate never parked it, and was hidden only on the twoflows_buildpaths — not on orchestrator delegation or main chat, which also carry the tool. Injected content could therefore cancel any in-flight or approval-parked automation.Solution
T-M2 — outside a trusted workflow run the write is refused outright rather than routed to an arg that cannot be distinguished from an attacker's. There is no legitimate chat-side use case for writing another flow's private namespace, and refusing is the only fail-closed option that does not require inventing an ownership proof. The registration comment is corrected to describe the real invariant.
flow_memory_recallis untouched — it stays read-only, and itsscope: "flows"cross-flow read is deliberate.A caller audit found nothing depending on the removed fallback;
tinyflows's own memory-node adapter already used this fail-closed pattern.T-M3 — two changes, of unequal weight:
external_effect() == true— soApprovalSecurityMiddlewareparks it wherever a gate exists. This is the actual authorization boundary. Pinned by anApprovalGateintegration test proving the call parks and TTL-denies rather than auto-executing.{ flow_id, run_id }, resolves the run's actualflow_id, and refuses on mismatch, mirroringresume_flow_run/flows_resume's existing shape rather than inventing a new one.Honest scoping: the ownership check narrows the attack, it does not close it
Review raised this and it is worth stating plainly rather than leaving the summary to imply otherwise.
list_flowstakes no arguments, isPermissionLevel::Noneandexternal_effect() == false;list_flow_runsreturnsflow_idinline for every run. Chaining the two reconstructs the fullrun_id → flow_idmap, and both are available on exactly the surfacescancel_flow_runis. So a caller that can reachcancel_flow_runat all can simply look up the correctflow_idfirst — the ownership check costs an attacker one extra tool round-trip, nothing more.It is still worth having: it stops accidental cross-flow cancellation, such as a model reusing a stale or hallucinated
run_id. But the approval-gate parking is what actually stops an unapproved cancel, and the same defeatable pattern already exists unchanged inresume_flow_run, so this PR mirrors precedent rather than introducing a new false sense of security. The in-code comments were already accurate on this point; this section brings the PR summary in line with them.Deliberate non-change reviewers should weigh
Both
flows_buildhide-lists are left unchanged. Hardening the tool makes it safe to unhide on the copilot path — but unhiding is a capability expansion, not a security fix: it would newly let an authoring turn tear down a run the user started. That product decision has not been taken, and hardening is not a reason to take it implicitly. A user can still cancel from the Runs rail. The entry can be dropped whenever you decide you want it; this PR is what makes doing so safe. It stays hidden on the headless path regardless, since aCli-trusted origin auto-allowsexternal_effecttools.Submission Checklist
ApprovalGatepark are all covered;cargo test --lib openhuman::flows= 554 passed, 0 failedN/A: security hardening of existing tools, no feature rows added/removed/renamed## Related—N/A: no matrix feature rows affectedN/A: agent-tool surface onlyCloses #NNN—N/A: found by code review, no tracking issue filed yetImpact
cancel_flow_run's schema gains a requiredflow_id. Any caller passing onlyrun_idnow gets a clear parameter error.flow_memory_remembercalled outside a run now errors instead of writing.ApprovalGatetest proves the now-external-effectcancel_flow_rungenuinely parks and times out toDenyrather than auto-executing.Cargo.toml/Cargo.lockchange.Related
N/Acancel_flow_runon the copilot path (now safe, deliberately deferred). The stale-approval-after-graph-swap gap (a run parked on the old graph config resuming against a rewritten node) is a separate stacked PR.src/openhuman/flows/ops.rs+ops_tests.rswith the resume-lifecycle PR, andbuilder_tools.rs+builder_tools_tests.rswith the contract-drift PR. Merge the resume-lifecycle PR first, then this, then contract-drift.AI Authored PR Metadata (required for Codex/Linear PRs)
Linear Issue
Commit & Branch
fix/flows-authorization-boundaries9e59c8ccaValidation Run
pnpm --filter openhuman-app format:check— N/A, no frontend files changedpnpm typecheck— N/A, no TypeScript changedGGML_NATIVE=OFF cargo test --lib openhuman::flows→ 554 passed, 0 failedGGML_NATIVE=OFF cargo check --manifest-path Cargo.tomlcleanapp/src-tauriuntouchedValidation Blocked
command:N/Aerror:N/Aimpact:N/ABehavior Changes
Parity Contract
flow_memory_rememberpath is byte-for-byte unchanged and still pinned byremember_ignores_mismatched_flow_id_arg_inside_trusted_workflow_run;flow_memory_recallis untouched.flows_buildhide-lists are unchanged and still pinned by their existing contents tests, so no path silently gained a tool.Duplicate / Superseded PR Handling