mirror of
https://github.com/openai/codex.git
synced 2026-11-12 21:20:34 +00:00
Prevent Noise auth tokens from reaching child processes (#38941)
## Why `CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN` is an execution-server credential and must not be exposed to model-reachable commands or command hooks, including when shell environment policy explicitly sets a case variant of the name. ## What changed - Add `CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN` to the shared list of non-inheritable environment variables. - Keep the environment variable constant in `codex-protocol` so the exec server and environment scrubber use the same definition. - Verify case-insensitive removal after policy overrides for shell commands and command hooks. GitOrigin-RevId: 759b224b6a7fb4f56f7b1a6d94ffbb0b188c658f
This commit is contained in:
committed by
copyberry
parent
3b4569a920
commit
89e297729e
@@ -2,6 +2,8 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::shell_environment::CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR;
|
||||
use core_test_support::TestTargetOs;
|
||||
use core_test_support::assert_regex_match;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
@@ -11,9 +13,11 @@ use core_test_support::responses::mount_sse_sequence;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::skip_if_host_windows;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::skip_if_wine_exec;
|
||||
use core_test_support::test_codex::TestCodexBuilder;
|
||||
use core_test_support::test_codex::TestCodexHarness;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::test_target_os;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use test_case::test_case;
|
||||
@@ -126,6 +130,41 @@ async fn shell_command_works() -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn shell_command_does_not_expose_configured_noise_auth_token() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
skip_if_wine_exec!(Ok(()), "shell_command is unavailable for Wine executors");
|
||||
|
||||
let builder = test_codex().with_model("gpt-5.4").with_config(|config| {
|
||||
config.permissions.shell_environment_policy.r#set.insert(
|
||||
CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR.to_string(),
|
||||
"configured-noise-token".to_string(),
|
||||
);
|
||||
config.permissions.shell_environment_policy.r#set.insert(
|
||||
CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR.to_ascii_lowercase(),
|
||||
"case-variant-noise-token".to_string(),
|
||||
);
|
||||
});
|
||||
let harness = TestCodexHarness::with_auto_env_builder(builder).await?;
|
||||
let command = match test_target_os() {
|
||||
TestTargetOs::Linux | TestTargetOs::MacOs => {
|
||||
"if [ -n \"${CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN:-}\" ] || [ -n \"${codex_exec_server_noise_auth_token:-}\" ]; then echo leaked; else echo unset; fi"
|
||||
}
|
||||
TestTargetOs::Windows => {
|
||||
"if ($env:CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN) { Write-Output leaked } else { Write-Output unset }"
|
||||
}
|
||||
};
|
||||
let call_id = "shell-command-noise-auth-token";
|
||||
mount_shell_responses(&harness, call_id, command, /*login*/ None).await;
|
||||
harness
|
||||
.submit("check the remote execution auth token")
|
||||
.await?;
|
||||
|
||||
assert_shell_command_output(&harness.function_call_stdout(call_id).await, "unset")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn shell_command_rejects_justification_without_sandbox_permissions() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
@@ -9,6 +9,7 @@ use codex_http_client::HttpClientFactory;
|
||||
use codex_http_client::OutboundProxyPolicy;
|
||||
use codex_protocol::capabilities::CapabilityRootLocation;
|
||||
use codex_protocol::capabilities::SelectedCapabilityRoot;
|
||||
use codex_protocol::shell_environment::CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR;
|
||||
|
||||
use crate::CapabilityRootsDiscoverParams;
|
||||
use crate::CapabilityRootsDiscoverResponse;
|
||||
@@ -50,7 +51,6 @@ pub const CODEX_EXEC_SERVER_NOISE_REGISTRY_URL_ENV_VAR: &str =
|
||||
"CODEX_EXEC_SERVER_NOISE_REGISTRY_URL";
|
||||
pub const CODEX_EXEC_SERVER_NOISE_ENVIRONMENT_ID_ENV_VAR: &str =
|
||||
"CODEX_EXEC_SERVER_NOISE_ENVIRONMENT_ID";
|
||||
pub const CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR: &str = "CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN";
|
||||
pub const CODEX_EXEC_SERVER_NOISE_CHATGPT_ACCOUNT_ID_ENV_VAR: &str =
|
||||
"CODEX_EXEC_SERVER_NOISE_CHATGPT_ACCOUNT_ID";
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ pub use codex_file_system::WalkEntryKind;
|
||||
pub use codex_file_system::WalkError;
|
||||
pub use codex_file_system::WalkOptions;
|
||||
pub use codex_file_system::WalkOutcome;
|
||||
pub use environment::CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR;
|
||||
pub use codex_protocol::shell_environment::CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR;
|
||||
pub use environment::CODEX_EXEC_SERVER_NOISE_CHATGPT_ACCOUNT_ID_ENV_VAR;
|
||||
pub use environment::CODEX_EXEC_SERVER_NOISE_ENVIRONMENT_ID_ENV_VAR;
|
||||
pub use environment::CODEX_EXEC_SERVER_NOISE_REGISTRY_URL_ENV_VAR;
|
||||
|
||||
@@ -12,6 +12,7 @@ use codex_protocol::protocol::HookOutputEntry;
|
||||
use codex_protocol::protocol::HookOutputEntryKind;
|
||||
use codex_protocol::protocol::HookRunStatus;
|
||||
use codex_protocol::protocol::HookSource;
|
||||
use codex_protocol::shell_environment::CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
@@ -113,6 +114,48 @@ async fn fast_exiting_hook_preserves_stdout_when_stdin_is_not_consumed() {
|
||||
assert_eq!(result.error, None);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn command_hook_does_not_expose_configured_noise_auth_token() {
|
||||
let temp = tempdir().expect("create temp dir");
|
||||
let source_path = AbsolutePathBuf::try_from(temp.path().join("hooks.json"))
|
||||
.expect("absolute hook configuration path");
|
||||
let command = if cfg!(windows) { "set" } else { "env" };
|
||||
let env = HashMap::from([
|
||||
(
|
||||
CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR.to_ascii_lowercase(),
|
||||
"configured-noise-token".to_string(),
|
||||
),
|
||||
("CODEX_HOOK_SAFE_ENV".to_string(), "visible".to_string()),
|
||||
]);
|
||||
let handler = ConfiguredHandler {
|
||||
event_name: HookEventName::SessionStart,
|
||||
matcher: None,
|
||||
timeout_sec: 10,
|
||||
status_message: None,
|
||||
additional_context_limit: Default::default(),
|
||||
source_path,
|
||||
source: HookSource::User,
|
||||
display_order: 0,
|
||||
kind: ConfiguredHandlerKind::Command {
|
||||
command: command.to_string(),
|
||||
r#async: false,
|
||||
env: env.clone(),
|
||||
},
|
||||
};
|
||||
let (runtime, _result_receiver) = runtime();
|
||||
|
||||
let result = run_command(&runtime, &handler, command, &env, "{}", temp.path()).await;
|
||||
|
||||
assert_eq!(result.exit_code, Some(0), "stderr: {}", result.stderr);
|
||||
assert!(result.stdout.contains("CODEX_HOOK_SAFE_ENV=visible"));
|
||||
assert!(!result.stdout.lines().any(|line| {
|
||||
line.split_once('=').is_some_and(|(name, _)| {
|
||||
name.eq_ignore_ascii_case(CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR)
|
||||
})
|
||||
}));
|
||||
assert_eq!(result.error, None);
|
||||
}
|
||||
|
||||
const ASYNC_HOOK_TEST_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
fn runtime() -> (CommandHookRuntime, Receiver<HookCompletedEvent>) {
|
||||
|
||||
@@ -5,12 +5,14 @@ use std::collections::HashMap;
|
||||
|
||||
pub const CODEX_SESSION_ID_ENV_VAR: &str = "CODEX_SESSION_ID";
|
||||
pub const CODEX_THREAD_ID_ENV_VAR: &str = "CODEX_THREAD_ID";
|
||||
pub const CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR: &str = "CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN";
|
||||
pub const OPENAI_FEDERATION_RULE_ID_ENV_VAR: &str = "OPENAI_FEDERATION_RULE_ID";
|
||||
pub const OPENAI_IDENTITY_TOKEN_FILE_ENV_VAR: &str = "OPENAI_IDENTITY_TOKEN_FILE";
|
||||
pub const OPENAI_WORKLOAD_IDENTITY_CONTEXT_ENV_VAR: &str = "OPENAI_WORKLOAD_IDENTITY_CONTEXT";
|
||||
|
||||
/// Environment variables that model-reachable child processes must not inherit.
|
||||
pub const NON_INHERITABLE_ENV_VARS: &[&str] = &[
|
||||
CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR,
|
||||
OPENAI_FEDERATION_RULE_ID_ENV_VAR,
|
||||
OPENAI_IDENTITY_TOKEN_FILE_ENV_VAR,
|
||||
OPENAI_WORKLOAD_IDENTITY_CONTEXT_ENV_VAR,
|
||||
|
||||
@@ -21,6 +21,10 @@ fn non_inheritable_environment_is_removed_after_policy_overrides() {
|
||||
"OPENAI_WORKLOAD_IDENTITY_CONTEXT".to_string(),
|
||||
r#"{"instance_id":"box-one"}"#.to_string(),
|
||||
),
|
||||
(
|
||||
"codex_exec_server_noise_auth_token".to_string(),
|
||||
"inherited-noise-token".to_string(),
|
||||
),
|
||||
];
|
||||
let policy = ShellEnvironmentPolicy {
|
||||
inherit: ShellEnvironmentPolicyInherit::All,
|
||||
@@ -31,6 +35,10 @@ fn non_inheritable_environment_is_removed_after_policy_overrides() {
|
||||
"OpenAI_Identity_Token_File".to_string(),
|
||||
"/run/identity-token".to_string(),
|
||||
),
|
||||
(
|
||||
"Codex_Exec_Server_Noise_Auth_Token".to_string(),
|
||||
"configured-noise-token".to_string(),
|
||||
),
|
||||
]),
|
||||
..Default::default()
|
||||
};
|
||||
@@ -52,6 +60,10 @@ fn command_scrubber_removes_names_from_real_child_environment() {
|
||||
"OpenAI_Workload_Identity_Context",
|
||||
r#"{"instance_id":"box-one"}"#,
|
||||
)
|
||||
.env(
|
||||
"Codex_Exec_Server_Noise_Auth_Token",
|
||||
"inherited-noise-token",
|
||||
)
|
||||
.output()
|
||||
.expect("run inherited-environment test process");
|
||||
assert!(
|
||||
@@ -66,6 +78,10 @@ fn command_scrubber_removes_names_from_real_child_environment() {
|
||||
let mut command = environment_command();
|
||||
command
|
||||
.env("openai_identity_token_file", "/run/identity-token")
|
||||
.env(
|
||||
CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR,
|
||||
"configured-noise-token",
|
||||
)
|
||||
.env("SAFE", "value");
|
||||
scrub_non_inheritable_env_vars(&mut command);
|
||||
let output = command.output().expect("read child environment");
|
||||
|
||||
Reference in New Issue
Block a user