mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 14:10:37 -07:00
137 lines
4.0 KiB
TypeScript
137 lines
4.0 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { describe, expect, it, beforeEach, afterEach } from 'vitest';
|
|
import { TestRig, InteractiveRun } from './test-helper.js';
|
|
import * as fs from 'node:fs';
|
|
import * as os from 'node:os';
|
|
import {
|
|
writeFileSync,
|
|
mkdirSync,
|
|
symlinkSync,
|
|
readFileSync,
|
|
unlinkSync,
|
|
} from 'node:fs';
|
|
import { join, dirname } from 'node:path';
|
|
import { GEMINI_DIR } from '@google/gemini-cli-core';
|
|
import * as pty from '@lydell/node-pty';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const BUNDLE_PATH = join(__dirname, '..', 'bundle/gemini.js');
|
|
|
|
const extension = `{
|
|
"name": "test-symlink-extension",
|
|
"version": "0.0.1"
|
|
}`;
|
|
|
|
const otherExtension = `{
|
|
"name": "malicious-extension",
|
|
"version": "6.6.6"
|
|
}`;
|
|
|
|
describe('extension symlink install spoofing protection', () => {
|
|
let rig: TestRig;
|
|
|
|
beforeEach(() => {
|
|
rig = new TestRig();
|
|
});
|
|
|
|
afterEach(async () => await rig.cleanup());
|
|
|
|
it('canonicalizes the trust path and prevents symlink spoofing', async () => {
|
|
// Enable folder trust for this test
|
|
rig.setup('symlink spoofing test', {
|
|
settings: {
|
|
security: {
|
|
folderTrust: {
|
|
enabled: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const realExtPath = join(rig.testDir!, 'real-extension');
|
|
mkdirSync(realExtPath);
|
|
writeFileSync(join(realExtPath, 'gemini-extension.json'), extension);
|
|
|
|
const maliciousExtPath = join(
|
|
os.tmpdir(),
|
|
`malicious-extension-${Date.now()}`,
|
|
);
|
|
mkdirSync(maliciousExtPath);
|
|
writeFileSync(
|
|
join(maliciousExtPath, 'gemini-extension.json'),
|
|
otherExtension,
|
|
);
|
|
|
|
const symlinkPath = join(rig.testDir!, 'symlink-extension');
|
|
symlinkSync(realExtPath, symlinkPath);
|
|
|
|
// Function to run a command with a PTY to avoid headless mode
|
|
const runPty = (args: string[]) => {
|
|
const ptyProcess = pty.spawn(process.execPath, [BUNDLE_PATH, ...args], {
|
|
name: 'xterm-color',
|
|
cols: 80,
|
|
rows: 80,
|
|
cwd: rig.testDir!,
|
|
env: {
|
|
...process.env,
|
|
GEMINI_CLI_HOME: rig.homeDir!,
|
|
GEMINI_CLI_INTEGRATION_TEST: 'true',
|
|
GEMINI_PTY_INFO: 'node-pty',
|
|
},
|
|
});
|
|
return new InteractiveRun(ptyProcess);
|
|
};
|
|
|
|
// 1. Install via symlink, trust it
|
|
const run1 = runPty(['extensions', 'install', symlinkPath]);
|
|
await run1.expectText('Do you want to trust this folder', 30000);
|
|
await run1.type('y\r');
|
|
await run1.expectText('trust this workspace', 30000);
|
|
await run1.type('y\r');
|
|
await run1.expectText('Do you want to continue', 30000);
|
|
await run1.type('y\r');
|
|
await run1.expectText('installed successfully', 30000);
|
|
await run1.kill();
|
|
|
|
// 2. Verify trustedFolders.json contains the REAL path, not the symlink path
|
|
const trustedFoldersPath = join(
|
|
rig.homeDir!,
|
|
GEMINI_DIR,
|
|
'trustedFolders.json',
|
|
);
|
|
// Wait for file to be written
|
|
let attempts = 0;
|
|
while (!fs.existsSync(trustedFoldersPath) && attempts < 50) {
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
attempts++;
|
|
}
|
|
|
|
const trustedFolders = JSON.parse(
|
|
readFileSync(trustedFoldersPath, 'utf-8'),
|
|
);
|
|
const trustedPaths = Object.keys(trustedFolders);
|
|
const canonicalRealExtPath = fs.realpathSync(realExtPath);
|
|
|
|
expect(trustedPaths).toContain(canonicalRealExtPath);
|
|
expect(trustedPaths).not.toContain(symlinkPath);
|
|
|
|
// 3. Swap the symlink to point to the malicious extension
|
|
unlinkSync(symlinkPath);
|
|
symlinkSync(maliciousExtPath, symlinkPath);
|
|
|
|
// 4. Try to install again via the same symlink path.
|
|
// It should NOT be trusted because the real path changed.
|
|
const run2 = runPty(['extensions', 'install', symlinkPath]);
|
|
await run2.expectText('Do you want to trust this folder', 30000);
|
|
await run2.type('n\r');
|
|
await run2.expectText('Installation aborted', 30000);
|
|
await run2.kill();
|
|
}, 60000);
|
|
});
|