mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 16:17:02 +00:00
fix(security): constrain image fallback hook to workspace (#1232)
This commit is contained in:
1 parent
349db830df
commit
40300c286e
3 files changed
+106
-5
No files matched your search
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user