mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-12 14:14:47 +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 fs from 'fs';
|
||||||
import * as childProcess from 'child_process';
|
import * as childProcess from 'child_process';
|
||||||
import { expandPath } from '../utils/helpers';
|
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';
|
import type { TargetBinaryInfo } from './target-adapter';
|
||||||
|
|
||||||
const CODEX_CONFIG_OVERRIDE_FEATURE = 'config-overrides';
|
const CODEX_CONFIG_OVERRIDE_FEATURE = 'config-overrides';
|
||||||
const CODEX_CONFIG_OVERRIDE_PROBE_ARGS = ['-c', 'model="gpt-5"', '--version'];
|
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[] {
|
function buildWindowsCodexCandidates(matches: string[]): string[] {
|
||||||
const shellCandidates = matches.filter((entry) => /\.(exe|cmd|bat|ps1)$/i.test(entry));
|
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])];
|
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 {
|
function runCodexProbe(codexPath: string, args: string[]): string | undefined {
|
||||||
const isWindows = process.platform === 'win32';
|
const isWindows = process.platform === 'win32';
|
||||||
const isPowerShellScript = isWindows && /\.ps1$/i.test(codexPath);
|
const isPowerShellScript = isWindows && /\.ps1$/i.test(codexPath);
|
||||||
const needsShell = isWindows && /\.(cmd|bat)$/i.test(codexPath);
|
const needsShell = isWindows && /\.(cmd|bat)$/i.test(codexPath);
|
||||||
|
|
||||||
|
const env = buildCodexProbeEnv();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isPowerShellScript) {
|
if (isPowerShellScript) {
|
||||||
return childProcess.execFileSync(
|
return childProcess.execFileSync(
|
||||||
@@ -44,6 +63,7 @@ function runCodexProbe(codexPath: string, args: string[]): string | undefined {
|
|||||||
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', codexPath, ...args],
|
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', codexPath, ...args],
|
||||||
{
|
{
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
|
env,
|
||||||
stdio: ['ignore', 'pipe', 'ignore'],
|
stdio: ['ignore', 'pipe', 'ignore'],
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
@@ -55,6 +75,7 @@ function runCodexProbe(codexPath: string, args: string[]): string | undefined {
|
|||||||
const cmdString = [codexPath, ...args].map(escapeShellArg).join(' ');
|
const cmdString = [codexPath, ...args].map(escapeShellArg).join(' ');
|
||||||
const result = childProcess.spawnSync(cmdString, {
|
const result = childProcess.spawnSync(cmdString, {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
|
env,
|
||||||
stdio: ['ignore', 'pipe', 'ignore'],
|
stdio: ['ignore', 'pipe', 'ignore'],
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
windowsHide: true,
|
windowsHide: true,
|
||||||
@@ -65,6 +86,7 @@ function runCodexProbe(codexPath: string, args: string[]): string | undefined {
|
|||||||
|
|
||||||
return childProcess.execFileSync(codexPath, args, {
|
return childProcess.execFileSync(codexPath, args, {
|
||||||
encoding: 'utf8',
|
encoding: 'utf8',
|
||||||
|
env,
|
||||||
stdio: ['ignore', 'pipe', 'ignore'],
|
stdio: ['ignore', 'pipe', 'ignore'],
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,16 +6,29 @@ import * as path from 'path';
|
|||||||
|
|
||||||
import { detectCodexCli, getCodexBinaryInfo } from '../../../src/targets/codex-detector';
|
import { detectCodexCli, getCodexBinaryInfo } from '../../../src/targets/codex-detector';
|
||||||
|
|
||||||
|
const CCSXP_CLIPROXY_SHORTCUT_ENV = 'CCSXP_CLIPROXY_SHORTCUT';
|
||||||
|
|
||||||
describe('codex-detector', () => {
|
describe('codex-detector', () => {
|
||||||
let tmpDir: string;
|
let tmpDir: string;
|
||||||
let originalPath: string | undefined;
|
let originalPath: string | undefined;
|
||||||
let originalCodexPath: 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;
|
const originalPlatform = process.platform;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-codex-detector-test-'));
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-codex-detector-test-'));
|
||||||
originalPath = process.env.PATH;
|
originalPath = process.env.PATH;
|
||||||
originalCodexPath = process.env.CCS_CODEX_PATH;
|
originalCodexPath = process.env.CCS_CODEX_PATH;
|
||||||
|
originalProbeEnv = Object.fromEntries(probeEnvKeys.map((key) => [key, process.env[key]]));
|
||||||
process.env.PATH = '';
|
process.env.PATH = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -28,6 +41,11 @@ describe('codex-detector', () => {
|
|||||||
if (originalCodexPath !== undefined) process.env.CCS_CODEX_PATH = originalCodexPath;
|
if (originalCodexPath !== undefined) process.env.CCS_CODEX_PATH = originalCodexPath;
|
||||||
else delete process.env.CCS_CODEX_PATH;
|
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 });
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -121,6 +139,39 @@ describe('codex-detector', () => {
|
|||||||
execSyncSpy.mockRestore();
|
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', () => {
|
it('falls back to a direct -c probe when help text omits the config flag', () => {
|
||||||
const fakeCodex = path.join(tmpDir, 'codex');
|
const fakeCodex = path.join(tmpDir, 'codex');
|
||||||
fs.writeFileSync(fakeCodex, '');
|
fs.writeFileSync(fakeCodex, '');
|
||||||
|
|||||||
Reference in new issue
Block a user