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:
Anton Panasenko
2026-08-17 04:17:45 +00:00
committed by copyberry
parent 3b4569a920
commit 89e297729e
6 changed files with 102 additions and 2 deletions
@@ -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(()));
+1 -1
View File
@@ -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";
+1 -1
View File
@@ -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");