fix(cli): escape npm script names in shell autocompletion to prevent command injection

This commit is contained in:
MD. MOHIBUR RAHMAN
2026-02-24 05:04:27 +06:00
parent 39b393abfb
commit c1d7bc4c8c
2 changed files with 30 additions and 1 deletions
@@ -44,7 +44,9 @@ describe('npmProvider', () => {
expect(result.exclusive).toBe(true);
expect(result.suggestions).toHaveLength(2);
expect(result.suggestions[0].label).toBe('build');
expect(result.suggestions[0].value).toBe('build');
expect(result.suggestions[1].label).toBe('build:dev');
expect(result.suggestions[1].value).toBe('build:dev');
expect(fs.readFile).toHaveBeenCalledWith(
expect.stringContaining('package.json'),
@@ -52,6 +54,32 @@ describe('npmProvider', () => {
);
});
it('escapes script names with shell metacharacters', async () => {
const mockPackageJson = {
scripts: {
'build(prod)': 'tsc',
'test:watch': 'vitest',
},
};
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(mockPackageJson));
const result = await npmProvider.getCompletions(
['npm', 'run', 'bu'],
2,
'/tmp',
);
expect(result.exclusive).toBe(true);
expect(result.suggestions).toHaveLength(1);
expect(result.suggestions[0].label).toBe('build(prod)');
// Windows does not escape spaces/parens in cmds by default in our function, but Unix does.
const isWin = process.platform === 'win32';
expect(result.suggestions[0].value).toBe(
isWin ? 'build(prod)' : 'build\\(prod\\)',
);
});
it('handles missing package.json gracefully', async () => {
vi.mocked(fs.readFile).mockRejectedValue(new Error('ENOENT'));
@@ -7,6 +7,7 @@
import * as fs from 'node:fs/promises';
import * as path from 'node:path';
import type { ShellCompletionProvider, CompletionResult } from './types.js';
import { escapeShellPath } from '../useShellCompletion.js';
const NPM_SUBCOMMANDS = [
'build',
@@ -64,7 +65,7 @@ export const npmProvider: ShellCompletionProvider = {
.filter((s) => s.startsWith(partial))
.map((s) => ({
label: s,
value: s,
value: escapeShellPath(s),
description: 'npm script',
})),
exclusive: true,