From 56e1d10e5c333e7b629186208a6d77fdde5d881a Mon Sep 17 00:00:00 2001 From: Epinephrine Date: Sun, 18 Oct 2026 19:03:33 -0700 Subject: [PATCH] fix(codex): strip OpenSSL startup variables from updater child env --- src/codex/cli-update-plan.ts | 21 ++++++-- structure/codex-cli-update.md | 3 +- .../codex-cli-update-plan.test.ts | 49 +++++++++++++++++++ 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/src/codex/cli-update-plan.ts b/src/codex/cli-update-plan.ts index 190a1088001..f87c2f5de6b 100644 --- a/src/codex/cli-update-plan.ts +++ b/src/codex/cli-update-plan.ts @@ -213,9 +213,15 @@ function npmTarget(args: readonly string[], env?: NodeJS.ProcessEnv): NpmTarget * reverse: they are output paths, so ambient values would write Node artifacts to * arbitrary external directories on exit. ELF/Darwin loader controls (LD_* and * DYLD_*) are also removed: they can load code or redirect loader output before - * npm runs. This is a denylist for process-startup/output channels, not a claim - * that every ambient variable is allowlisted. Ordinary OS environment, proxy - * settings and the supported NODE_EXTRA_CA_CERTS extension remain available. + * npm runs. OpenSSL's startup inputs are removed with them: OPENSSL_CONF names + * a configuration file the crypto library reads at startup, OPENSSL_CONF_INCLUDE + * resolves relative includes inside that configuration, and OPENSSL_MODULES / + * OPENSSL_ENGINES name the directories its provider and engine search loads + * shared modules from. None of these is input the updater child needs, so + * ambient values stay out of the spawned npm. This is a denylist for + * process-startup/output channels, not a claim that every ambient variable is + * allowlisted. Ordinary OS environment, proxy settings and the supported + * NODE_EXTRA_CA_CERTS extension remain available. */ const NPM_ENV_DROP_KEYS: ReadonlySet = new Set([ "node_options", @@ -227,6 +233,15 @@ const NPM_ENV_DROP_KEYS: ReadonlySet = new Set([ // escaping the owned temp root regardless of the isolated --cache. "node_v8_coverage", "node_redirect_warnings", + // OpenSSL startup inputs: OPENSSL_CONF points the library at an ambient + // startup configuration file (OPENSSL_CONF_INCLUDE resolves that config's + // relative includes); OPENSSL_MODULES and OPENSSL_ENGINES point its + // provider/engine search at ambient directories that supply loadable + // shared modules. + "openssl_conf", + "openssl_conf_include", + "openssl_modules", + "openssl_engines", ]); function codexCliUpdateNpmEnv(base: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv { diff --git a/structure/codex-cli-update.md b/structure/codex-cli-update.md index 57b0bcf1758..bae97d671a3 100644 --- a/structure/codex-cli-update.md +++ b/structure/codex-cli-update.md @@ -11,7 +11,8 @@ The read-only CLI update plan binds npm executable selection to the inspected la Registry integrity requires a canonical base64 SHA-512 digest of exactly 64 bytes; malformed or truncated tokens refuse the target. Selection observation uses the runtime resolver with provenance's bounded no-follow, nonblocking state reader and local executable-path policy. Windows returns before invoking the resolver; its deferred inspection performs no candidate or configuration I/O. Its child environment drops npm configuration overrides, named Node startup/output controls, -and the `LD_*`/`DYLD_*` loader families; proxy settings and `NODE_EXTRA_CA_CERTS` remain supported. +the `LD_*`/`DYLD_*` loader families and the OpenSSL startup configuration and +provider/engine search variables; proxy settings and `NODE_EXTRA_CA_CERTS` remain supported. Distinct npm config files, cache and logs stay under the owned temporary root with best-effort cleanup; a cleanup error does not replace the computed plan/refusal. The POSIX update scan includes all readable users by default, retaining an explicitly injected UID scope. diff --git a/tests/codex-integration/codex-cli-update-plan.test.ts b/tests/codex-integration/codex-cli-update-plan.test.ts index f3e4c2eac9b..6d3b86fccc7 100644 --- a/tests/codex-integration/codex-cli-update-plan.test.ts +++ b/tests/codex-integration/codex-cli-update-plan.test.ts @@ -564,6 +564,55 @@ describe("registry configuration isolation", () => { } }); + test.each([ + "OPENSSL_CONF", "OPENSSL_CONF_INCLUDE", "OPENSSL_MODULES", "OPENSSL_ENGINES", + "openssl_conf", "openssl_modules", + ])("ambient OpenSSL startup control %s cannot reach the spawned npm", key => { + const previous = process.env[key]; + process.env[key] = join(tmpdir(), "ocx-ambient-openssl.cnf"); + try { + // Capture only: no child process, native provider or network is invoked. + const { calls, spawn } = capturingSpawn(RESOLVE_OUTPUTS); + expect(resolveCodexCliUpdateTarget("latest", spawn).kind).toBe("resolved"); + expect(calls.length).toBe(3); + for (const call of calls) { + const env = call.options.env as NodeJS.ProcessEnv; + expect(Object.keys(env).some(name => name.toLowerCase() === key.toLowerCase())).toBe(false); + } + } finally { + if (previous === undefined) delete process.env[key]; + else process.env[key] = previous; + } + }); + + test("supported proxy and CA channels still reach the spawned npm", () => { + const sentinels = { + HTTP_PROXY: "http://127.0.0.1:9/ocx-proxy-sentinel", + NO_PROXY: "registry.npmjs.org.invalid,127.0.0.1", + NODE_EXTRA_CA_CERTS: join(tmpdir(), "ocx-corporate-ca.pem"), + }; + const previous = Object.fromEntries(Object.keys(sentinels).map(key => [key, process.env[key]])); + try { + Object.assign(process.env, sentinels); + const { calls, spawn } = capturingSpawn(RESOLVE_OUTPUTS); + expect(resolveCodexCliUpdateTarget("latest", spawn).kind).toBe("resolved"); + for (const call of calls) { + const env = call.options.env as NodeJS.ProcessEnv; + for (const [key, value] of Object.entries(sentinels)) { + // Match case-insensitively: the child env is a plain object, so a + // Windows-normalized casing must not make a supported channel look absent. + const entry = Object.entries(env).find(([name]) => name.toLowerCase() === key.toLowerCase()); + expect(entry?.[1]).toBe(value); + } + } + } finally { + for (const key of Object.keys(sentinels)) { + if (previous[key] === undefined) delete process.env[key]; + else process.env[key] = previous[key]; + } + } + }); + test("real Node cannot write output sentinels outside the owned npm root", () => { const fixture = fs.realpathSync.native(mkdtempSync(join(tmpdir(), "ocx-update-node-output-"))); const sentinels = {