mirror of
https://github.com/openai/codex.git
synced 2026-11-05 08:28:29 +00:00
Reject oversized reviewed terminal input (#41159)
## Why Reviewing a shortened `write_stdin` action while sending the complete input could allow unreviewed bytes to reach an escalated terminal. ## What changed - Require the fully formatted action and approval reason to fit within 8,000 bytes. - Reject truncated or oversized actions before requesting approval or writing any bytes to the terminal. - Document the reviewed stdin size limit in the app-server protocol. ## Testing Add an integration test that uses JSON escaping to push a formatted action over the limit and verifies that it is neither offered for approval nor executed, while a subsequent valid write still succeeds. GitOrigin-RevId: f6c96a78a65a136366c8119a7d242dfc2ec05fe6
This commit is contained in:
1 parent
453a9bcc69
commit
bce96bcb43
6 files changed
+147
-4
No files matched your search
@@ -170,6 +170,7 @@ mod turn_input_submission;
|
||||
mod turn_state;
|
||||
mod unified_exec;
|
||||
mod unified_exec_process_events;
|
||||
mod unified_exec_stdin_review_size;
|
||||
#[cfg(unix)]
|
||||
mod unified_exec_zsh_fork_approvals;
|
||||
mod unstable_features_warning;
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
//! Stdin approval must cover the complete input before any bytes reach a terminal.
|
||||
|
||||
use anyhow::Result;
|
||||
use codex_core::TurnInputRequest;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::approvals::ExecApprovalKind;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::ThreadSettingsOverrides;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_function_call;
|
||||
use core_test_support::responses::mount_sse_sequence;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::skip_if_sandbox;
|
||||
use core_test_support::skip_if_target_windows;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn oversized_stdin_is_rejected_before_approval_or_execution() -> Result<()> {
|
||||
skip_if_target_windows!(Ok(()), "uses a POSIX interactive shell");
|
||||
skip_if_no_network!(Ok(()));
|
||||
skip_if_sandbox!(Ok(()));
|
||||
let server = start_mock_server().await;
|
||||
let mut builder = test_codex().with_config(|config| {
|
||||
config
|
||||
.features
|
||||
.enable(Feature::WriteStdinApproval)
|
||||
.expect("enable stdin approvals");
|
||||
});
|
||||
let test = builder.build_with_auto_env(&server).await?;
|
||||
// The raw input fits in 8KB, but its JSON escapes do not. None of the
|
||||
// rejected input, including the trailing assignment, may reach the shell.
|
||||
let oversized = format!("#{}\nREJECTED=1\n", "\"".repeat(4_100));
|
||||
let mut sequence = Vec::new();
|
||||
for (id, tool, args) in [
|
||||
(
|
||||
"open",
|
||||
"exec_command",
|
||||
json!({"cmd":"/bin/bash --noprofile --norc", "tty":true, "yield_time_ms":200, "sandbox_permissions":"require_escalated"}),
|
||||
),
|
||||
(
|
||||
"oversized",
|
||||
"write_stdin",
|
||||
json!({"session_id":1000, "chars":oversized, "yield_time_ms":1000}),
|
||||
),
|
||||
(
|
||||
"allowed",
|
||||
"write_stdin",
|
||||
json!({"session_id":1000, "chars":"test -z \"${REJECTED-}\" && printf 'STDIN_%s_OK\\n' REVIEW; exit\n", "yield_time_ms":1000}),
|
||||
),
|
||||
] {
|
||||
sequence.push(sse(vec![
|
||||
ev_function_call(id, tool, &args.to_string()),
|
||||
ev_completed(id),
|
||||
]));
|
||||
}
|
||||
sequence.push(sse(vec![ev_completed("done")]));
|
||||
let responses = mount_sse_sequence(&server, sequence).await;
|
||||
test.codex
|
||||
.start_or_steer_turn(
|
||||
TurnInputRequest::user_input(vec![UserInput::Text {
|
||||
text: "send terminal input".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
}])
|
||||
.with_thread_settings(ThreadSettingsOverrides {
|
||||
approval_policy: Some(AskForApproval::OnRequest),
|
||||
sandbox_policy: Some(SandboxPolicy::ReadOnly {
|
||||
network_access: false,
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.await?;
|
||||
let mut approvals = Vec::new();
|
||||
loop {
|
||||
match wait_for_event(&test.codex, |_| true).await {
|
||||
EventMsg::ExecApprovalRequest(request) => {
|
||||
let id = request.effective_approval_id();
|
||||
approvals.push((request.kind, id.clone()));
|
||||
test.codex
|
||||
.submit(Op::ExecApproval {
|
||||
id,
|
||||
turn_id: Some(request.turn_id),
|
||||
decision: ReviewDecision::Approved,
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
EventMsg::TurnComplete(_) => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
assert_eq!(
|
||||
approvals,
|
||||
vec![
|
||||
(ExecApprovalKind::Command, "open".to_string()),
|
||||
(ExecApprovalKind::WriteStdin, "allowed".to_string()),
|
||||
]
|
||||
);
|
||||
let requests = responses.requests();
|
||||
let last = requests.last().expect("final model request");
|
||||
let rejected = last.function_call_output("oversized");
|
||||
assert!(
|
||||
rejected.to_string().contains("too large to review safely"),
|
||||
"{rejected}"
|
||||
);
|
||||
let allowed = last.function_call_output("allowed");
|
||||
assert!(allowed.to_string().contains("STDIN_REVIEW_OK"), "{allowed}");
|
||||
test.codex.submit(Op::Shutdown).await?;
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::ShutdownComplete)
|
||||
})
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user