mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-11 03:13:12 +00:00
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:
1 parent
c351261fe4
commit
db44845857
2 files changed
+74
-1
No files matched your search
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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, '');
|
||||
|
||||
Reference in new issue
Block a user