fix(codex): sanitize config override probes (#1535)

Sanitizes ccsxp support-probe config overrides to avoid env leak; parity with the real Codex launch path.
This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-06-15 23:12:46 -04:00
1 parent c351261fe4
commit db44845857
2 files changed
+74 -1

No files matched your search

+23 -1
View File
@@ -1,11 +1,19 @@
import * as fs from 'fs';
import * as childProcess from 'child_process';
import { expandPath } from '../utils/helpers';
import { escapeShellArg, getWindowsEscapedCommandShell } from '../utils/shell-executor';
import {
escapeShellArg,
getWindowsEscapedCommandShell,
stripAnthropicEnv,
stripBrowserEnv,
stripCodexSessionEnv,
} from '../utils/shell-executor';
import type { TargetBinaryInfo } from './target-adapter';
const CODEX_CONFIG_OVERRIDE_FEATURE = 'config-overrides';
const CODEX_CONFIG_OVERRIDE_PROBE_ARGS = ['-c', 'model="gpt-5"', '--version'];
const CCSXP_CLIPROXY_SHORTCUT_ENV = 'CCSXP_CLIPROXY_SHORTCUT';
const CODEX_RUNTIME_ENV_KEY = 'CCS_CODEX_API_KEY';
function buildWindowsCodexCandidates(matches: string[]): string[] {
const shellCandidates = matches.filter((entry) => /\.(exe|cmd|bat|ps1)$/i.test(entry));
@@ -32,11 +40,22 @@ function buildWindowsCodexCandidates(matches: string[]): string[] {
return [...new Set([...prioritized, ...bareCandidates])];
}
function buildCodexProbeEnv(): NodeJS.ProcessEnv {
const env: NodeJS.ProcessEnv = {
...stripBrowserEnv(stripCodexSessionEnv(stripAnthropicEnv(process.env))),
};
delete env[CCSXP_CLIPROXY_SHORTCUT_ENV];
delete env[CODEX_RUNTIME_ENV_KEY];
return env;
}
function runCodexProbe(codexPath: string, args: string[]): string | undefined {
const isWindows = process.platform === 'win32';
const isPowerShellScript = isWindows && /\.ps1$/i.test(codexPath);
const needsShell = isWindows && /\.(cmd|bat)$/i.test(codexPath);
const env = buildCodexProbeEnv();
try {
if (isPowerShellScript) {
return childProcess.execFileSync(
@@ -44,6 +63,7 @@ function runCodexProbe(codexPath: string, args: string[]): string | undefined {
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', codexPath, ...args],
{
encoding: 'utf8',
env,
stdio: ['ignore', 'pipe', 'ignore'],
timeout: 5000,
windowsHide: true,
@@ -55,6 +75,7 @@ function runCodexProbe(codexPath: string, args: string[]): string | undefined {
const cmdString = [codexPath, ...args].map(escapeShellArg).join(' ');
const result = childProcess.spawnSync(cmdString, {
encoding: 'utf8',
env,
stdio: ['ignore', 'pipe', 'ignore'],
timeout: 5000,
windowsHide: true,
@@ -65,6 +86,7 @@ function runCodexProbe(codexPath: string, args: string[]): string | undefined {
return childProcess.execFileSync(codexPath, args, {
encoding: 'utf8',
env,
stdio: ['ignore', 'pipe', 'ignore'],
timeout: 5000,
});
+51
View File
@@ -6,16 +6,29 @@ import * as path from 'path';
import { detectCodexCli, getCodexBinaryInfo } from '../../../src/targets/codex-detector';
const CCSXP_CLIPROXY_SHORTCUT_ENV = 'CCSXP_CLIPROXY_SHORTCUT';
describe('codex-detector', () => {
let tmpDir: string;
let originalPath: string | undefined;
let originalCodexPath: string | undefined;
let originalProbeEnv: Record<string, string | undefined>;
const probeEnvKeys = [
'ANTHROPIC_AUTH_TOKEN',
'ANTHROPIC_API_KEY',
'CCS_BROWSER_DEVTOOLS_URL',
'CODEX_THREAD_ID',
'CCS_CODEX_API_KEY',
CCSXP_CLIPROXY_SHORTCUT_ENV,
'CCS_SAFE_VALUE',
];
const originalPlatform = process.platform;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-codex-detector-test-'));
originalPath = process.env.PATH;
originalCodexPath = process.env.CCS_CODEX_PATH;
originalProbeEnv = Object.fromEntries(probeEnvKeys.map((key) => [key, process.env[key]]));
process.env.PATH = '';
});
@@ -28,6 +41,11 @@ describe('codex-detector', () => {
if (originalCodexPath !== undefined) process.env.CCS_CODEX_PATH = originalCodexPath;
else delete process.env.CCS_CODEX_PATH;
for (const key of probeEnvKeys) {
if (originalProbeEnv[key] !== undefined) process.env[key] = originalProbeEnv[key];
else delete process.env[key];
}
fs.rmSync(tmpDir, { recursive: true, force: true });
});
@@ -121,6 +139,39 @@ describe('codex-detector', () => {
execSyncSpy.mockRestore();
});
it('runs Codex feature probes with the same sensitive env stripping used for Codex launches', () => {
const fakeCodex = path.join(tmpDir, 'codex');
fs.writeFileSync(fakeCodex, '');
process.env.CCS_CODEX_PATH = fakeCodex;
process.env.ANTHROPIC_AUTH_TOKEN = 'secret-auth-token';
process.env.ANTHROPIC_API_KEY = 'secret-api-key';
process.env.CCS_BROWSER_DEVTOOLS_URL = 'ws://127.0.0.1:9222/devtools/browser/secret';
process.env.CODEX_THREAD_ID = 'thread-secret';
process.env.CCS_CODEX_API_KEY = 'runtime-secret';
process.env[CCSXP_CLIPROXY_SHORTCUT_ENV] = '1';
process.env.CCS_SAFE_VALUE = 'safe-value';
const execFileSyncSpy = spyOn(childProcess, 'execFileSync').mockImplementation(() => {
return 'codex-cli 0.119.0-alpha.1';
});
const info = getCodexBinaryInfo();
expect(info?.features).toContain('config-overrides');
const probeOptions = execFileSyncSpy.mock.calls
.map((call) => call[2] as { env?: NodeJS.ProcessEnv } | undefined)
.find((options) => options?.env);
expect(probeOptions?.env?.ANTHROPIC_AUTH_TOKEN).toBeUndefined();
expect(probeOptions?.env?.ANTHROPIC_API_KEY).toBeUndefined();
expect(probeOptions?.env?.CCS_BROWSER_DEVTOOLS_URL).toBeUndefined();
expect(probeOptions?.env?.CODEX_THREAD_ID).toBeUndefined();
expect(probeOptions?.env?.CCS_CODEX_API_KEY).toBeUndefined();
expect(probeOptions?.env?.[CCSXP_CLIPROXY_SHORTCUT_ENV]).toBeUndefined();
expect(probeOptions?.env?.CCS_SAFE_VALUE).toBe('safe-value');
execFileSyncSpy.mockRestore();
});
it('falls back to a direct -c probe when help text omits the config flag', () => {
const fakeCodex = path.join(tmpDir, 'codex');
fs.writeFileSync(fakeCodex, '');