fix(security): constrain image fallback hook to workspace (#1232)

This commit is contained in:
Kai (Tam Nhu) Tran authored and GitHub committed 2026-05-12 18:13:32 -04:00
1 parent 349db830df
commit 40300c286e
3 files changed
+106 -5

No files matched your search

+54 -2
View File
@@ -47,7 +47,10 @@ function enqueueResponses(...responses: MockResponse[]): void {
queuedResponses = responses;
}
function invokeHook(env: Record<string, string> = {}): Promise<HookResult> {
function invokeHook(
env: Record<string, string> = {},
options: { filePath?: string; cwd?: string } = {}
): Promise<HookResult> {
return new Promise((resolve, reject) => {
const child = spawn('node', [HOOK_PATH], {
env: {
@@ -68,6 +71,7 @@ function invokeHook(env: Record<string, string> = {}): Promise<HookResult> {
...env,
},
stdio: ['pipe', 'pipe', 'pipe'],
cwd: options.cwd ?? TEST_DIR,
});
let stdout = '';
@@ -98,7 +102,7 @@ function invokeHook(env: Record<string, string> = {}): Promise<HookResult> {
child.stdin.end(
JSON.stringify({
tool_name: 'Read',
tool_input: { file_path: TEST_PNG_PATH },
tool_input: { file_path: options.filePath ?? TEST_PNG_PATH },
})
);
});
@@ -198,6 +202,54 @@ describe('image analyzer hook regression coverage', () => {
expect((requests[0].body as { model: string }).model).toBe('gpt-5.1-codex-mini');
});
it('does not analyze files outside the current workspace', async () => {
const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-image-hook-outside-'));
const outsidePath = path.join(outsideDir, 'sensitive-outside-workspace.png');
createTestPng(outsidePath);
try {
const result = await invokeHook({}, { filePath: outsidePath });
expect(result.code).toBe(0);
expect(requests).toHaveLength(0);
} finally {
fs.rmSync(outsideDir, { recursive: true, force: true });
}
});
it.if(process.platform === 'win32')(
'analyzes Windows-style relative paths inside the current workspace',
async () => {
const nestedDir = path.join(TEST_DIR, 'windows-style-fixture-dir');
const nestedPath = path.join(nestedDir, 'inside-workspace.png');
fs.mkdirSync(nestedDir, { recursive: true });
createTestPng(nestedPath);
const result = await invokeHook({}, { filePath: path.win32.relative(TEST_DIR, nestedPath) });
expect(result.code).toBe(2);
expect(requests).toHaveLength(1);
}
);
it('does not analyze workspace symlinks that resolve outside the current workspace', async () => {
const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-image-hook-symlink-'));
const outsidePath = path.join(outsideDir, 'sensitive-symlink-target.png');
const linkPath = path.join(TEST_DIR, 'linked-sensitive.png');
createTestPng(outsidePath);
try {
fs.symlinkSync(outsidePath, linkPath);
const result = await invokeHook({}, { filePath: linkPath });
expect(result.code).toBe(0);
expect(requests).toHaveLength(0);
} finally {
fs.rmSync(linkPath, { force: true });
fs.rmSync(outsideDir, { recursive: true, force: true });
}
});
it('skips analysis before contacting CLIProxy when the current provider has no mapped vision model', async () => {
const result = await invokeHook({
CCS_CURRENT_PROVIDER: 'unknown-provider',