fix(codex): require final two-phase approval decisions#70751
fix(codex): require final two-phase approval decisions#70751steipete merged 2 commits intoopenclaw:mainfrom
Conversation
Greptile SummaryThis PR fixes a security bypass in the Codex app-server approval bridges where a Confidence Score: 5/5Safe to merge — the security fix is logically correct, all affected code paths are covered by updated and new tests, and no regressions are introduced. The core logic change is narrow and correct: approvalRequestExplicitlyUnavailable properly differentiates decision: null (explicit no-route signal) from all other decision values (which must now go through waitDecision). The prototype-chain edge case is explicitly tested. Existing tests were updated to reflect the new two-call contract, and two new regression tests were added. The shell script change is additive and defensive. No P0 or P1 issues found. No files require special attention. Reviews (3): Last reviewed commit: "ci: retrigger pull request checks" | Re-trigger Greptile |
0a05f15 to
9f94a53
Compare
|
Addressed in |
|
I am not applying the shorter local timeout suggestion here. The long wait is the intended human approval window and is already bounded by |
|
Temporarily closing/reopening to retrigger pull_request workflows after GitHub only dispatched label checks on the latest head SHA. |
28730eb to
a69edcd
Compare
a69edcd to
09959cc
Compare
Co-authored-by: Lucenx9 <185146821+Lucenx9@users.noreply.github.com>
Co-authored-by: Lucenx9 <185146821+Lucenx9@users.noreply.github.com>
a686eec to
b17f2ac
Compare
🔒 Aisle Security AnalysisWe found 1 potential security issue(s) in this PR:
1. 🟡 Potential application-layer DoS via forced two-phase approval wait (ignoring immediate decisions)
DescriptionThe app-server now always waits for the second-phase This can enable an application-layer DoS / resource exhaustion scenario:
While there is a gateway-side timeout, this change increases exposure by no longer honoring request-time decisions (even if present), meaning plugins that previously completed in one phase will now incur the full wait or timeout. Vulnerable code: const decision = approvalRequestExplicitlyUnavailable(requestResult)
? null
: await waitForPluginApprovalDecision({ approvalId, signal: params.signal });RecommendationAdd a bounded, explicit server-side timeout and/or concurrency controls around the wait, and consider using request-time decisions when safe/expected. Example (explicit timeout wrapping the wait): import { setTimeout as delay } from "node:timers/promises";
const decision = approvalRequestExplicitlyUnavailable(requestResult)
? null
: await Promise.race([
waitForPluginApprovalDecision({ approvalId, signal: params.signal }),
delay(30_000, null, { signal: params.signal }), // shorter app-server bound
]);Additionally:
Analyzed PR: #70751 at commit Last updated on: 2026-04-24T04:27:07Z |
|
Codex review follow-up: landed as Rebased after #70569 and kept the final two-phase approval decision as the authority while preserving the explicit no-route sentinel. Local proof on the rebased branch: |
Require the Codex app-server bridge to wait for the final two-phase approval decision, while preserving the explicit no-route sentinel behavior. Local gate on rebased branch: pnpm check:changed (20 files, 157 tests). Thanks @Lucenx9. Co-authored-by: Lucenx9 <185146821+Lucenx9@users.noreply.github.com>
Summary
decisionfield returned by the initialplugin.approval.requestcall.plugin.approval.requestis a two-phase registration step; granting from that response can bypass the finalplugin.approval.waitDecisionconfirmation path.plugin.approval.waitDecisionwhenever an approval route exists.decision: nullfrom the request response still fails closed immediately for the existing no-approval-route case; approval payload shape and granted permission mapping are unchanged.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
Root Cause (if applicable)
plugin.approval.requestas potentially final even though it always requeststwoPhase: true.allow-alwaysis ignored.decision: nullon the request response to report no approval route, which made the request response look decision-shaped.Regression Test Plan (if applicable)
extensions/codex/src/app-server/approval-bridge.test.ts,extensions/codex/src/app-server/elicitation-bridge.test.tsallow-alwaysis ignored and the finalwaitDecisionresult controls the outcome.decision: null, but not conflicting request-time decisions.User-visible / Behavior Changes
Codex approval prompts still look the same, but final approval decisions now always come from the two-phase wait endpoint when an approval route exists.
Diagram (if applicable)
Security Impact (required)
Yes/No) NoYes/No) NoYes/No) NoYes/No) YesYes/No) NoYes, explain risk + mitigation: This reduces the command/tool approval surface by preventing request-time approval decisions from granting Codex native command/file/permission approvals or MCP tool approval elicitations.Repro + Verification
Environment
Steps
{ id, status: "accepted", decision: "allow-always" }fromplugin.approval.request.{ id, decision: "deny" }fromplugin.approval.waitDecision.Expected
allow-alwaysand returns the denied final outcome.Actual
waitDecisioncontrols the outcome.Evidence
Human Verification (required)
What you personally verified (not just CI), and how:
allow-always; no-routedecision: nullstill fails closed without waiting.Review Conversations
Compatibility / Migration
Yes/No) YesYes/No) NoYes/No) NoRisks and Mitigations
requestPluginApprovalalways sendstwoPhase: true; tests assert the expected two-call contract.