fix: avoid shell-escaping special characters except quotes (#7242)

This commit is contained in:
Lifei Zhou
2026-02-16 20:10:08 +11:00
committed by GitHub
parent 4a5210e20e
commit b9c3ce214f
3 changed files with 92 additions and 6 deletions
@@ -2,6 +2,7 @@ import ExtensionItem from './ExtensionItem';
import builtInExtensionsData from '../../../../built-in-extensions.json';
import { ExtensionConfig } from '../../../../api';
import { FixedExtensionEntry } from '../../../ConfigContext';
import { combineCmdAndArgs } from '../utils';
interface ExtensionListProps {
extensions: FixedExtensionEntry[];
@@ -137,7 +138,7 @@ export function getSubtitle(config: ExtensionConfig) {
default:
return {
description: config.description || null,
command: 'cmd' in config ? [config.cmd, ...config.args].join(' ') : null,
command: 'cmd' in config ? combineCmdAndArgs(config.cmd, config.args) : null,
};
}
}
@@ -156,6 +156,85 @@ describe('Extension Utils', () => {
headers: [],
});
});
it('should not escape @ in command args', () => {
const extension: FixedExtensionEntry = {
type: 'stdio',
name: 'context7',
description: 'Context7 MCP',
cmd: 'npx',
args: ['-y', '@upstash/context7-mcp'],
enabled: true,
};
const formData = extensionToFormData(extension);
expect(formData.cmd).toBe('npx -y @upstash/context7-mcp');
});
it('should quote args with spaces', () => {
const extension: FixedExtensionEntry = {
type: 'stdio',
name: 'java-app',
description: 'Java app',
cmd: '/Applications/IntelliJ IDEA.app/Contents/jbr/Contents/Home/bin/java',
args: ['-classpath', '/path/with spaces/lib.jar', 'Main'],
enabled: true,
};
const formData = extensionToFormData(extension);
expect(formData.cmd).toBe(
'"/Applications/IntelliJ IDEA.app/Contents/jbr/Contents/Home/bin/java" -classpath "/path/with spaces/lib.jar" Main'
);
});
it('should roundtrip command with @ through form data', () => {
const extension: FixedExtensionEntry = {
type: 'stdio',
name: 'context7',
description: 'Context7 MCP',
cmd: 'npx',
args: ['-y', '@upstash/context7-mcp'],
enabled: true,
};
const formData = extensionToFormData(extension);
const { cmd, args } = splitCmdAndArgs(formData.cmd || '');
expect(cmd).toBe('npx');
expect(args).toEqual(['-y', '@upstash/context7-mcp']);
});
it('should roundtrip command with spaces through form data', () => {
const extension: FixedExtensionEntry = {
type: 'stdio',
name: 'java-app',
description: 'Java app',
cmd: '/Applications/IntelliJ IDEA.app/Contents/jbr/Contents/Home/bin/java',
args: ['-classpath', '/path/with spaces/lib.jar', 'Main'],
enabled: true,
};
const formData = extensionToFormData(extension);
const { cmd, args } = splitCmdAndArgs(formData.cmd || '');
expect(cmd).toBe('/Applications/IntelliJ IDEA.app/Contents/jbr/Contents/Home/bin/java');
expect(args).toEqual(['-classpath', '/path/with spaces/lib.jar', 'Main']);
});
it('should roundtrip args with double quotes and spaces through form data', () => {
const extension: FixedExtensionEntry = {
type: 'stdio',
name: 'test',
description: 'test',
cmd: 'node',
args: ['/My "Project"/bin/run'],
enabled: true,
};
const formData = extensionToFormData(extension);
expect(formData.cmd).toBe('node \'/My "Project"/bin/run\'');
const { cmd, args } = splitCmdAndArgs(formData.cmd || '');
expect(cmd).toBe('node');
expect(args).toEqual(['/My "Project"/bin/run']);
});
});
describe('createExtensionConfig', () => {
@@ -1,6 +1,6 @@
import type { FixedExtensionEntry } from '../../ConfigContext';
import type { ExtensionConfig } from '../../../api/types.gen';
import { parse as parseShellQuote, quote as quoteShell } from 'shell-quote';
import { parse as parseShellQuote } from 'shell-quote';
// Default extension timeout in seconds
// TODO: keep in sync with rust better
@@ -100,11 +100,11 @@ export function extensionToFormData(extension: FixedExtensionEntry): ExtensionFo
description: extension.description || '',
type:
extension.type === 'frontend' ||
extension.type === 'inline_python' ||
extension.type === 'platform'
extension.type === 'inline_python' ||
extension.type === 'platform'
? 'stdio'
: extension.type,
cmd: extension.type === 'stdio' ? quoteShell([extension.cmd, ...extension.args]) : undefined,
cmd: extension.type === 'stdio' ? combineCmdAndArgs(extension.cmd, extension.args) : undefined,
endpoint:
extension.type === 'streamable_http' || extension.type === 'sse'
? (extension.uri ?? undefined)
@@ -187,7 +187,13 @@ export function splitCmdAndArgs(str: string): { cmd: string; args: string[] } {
}
export function combineCmdAndArgs(cmd: string, args: string[]): string {
return quoteShell([cmd, ...args]);
return [cmd, ...args]
.map((a) => {
if (!a.includes(' ')) return a;
if (a.includes('"')) return `'${a}'`;
return `"${a}"`;
})
.join(' ');
}
export function extractCommand(link: string): string {