diff --git a/src/commands/env-command.ts b/src/commands/env-command.ts index fac4f2fb..a29754fa 100644 --- a/src/commands/env-command.ts +++ b/src/commands/env-command.ts @@ -30,17 +30,18 @@ export function detectShell(flag?: string): ShellType { return 'bash'; } -/** Format a single env var export for the target shell */ +/** Format a single env var export for the target shell (single-quoted to prevent injection) */ export function formatExportLine(shell: ShellType, key: string, value: string): string { - // Escape double quotes in value - const escaped = value.replace(/"/g, '\\"'); switch (shell) { case 'fish': - return `set -gx ${key} "${escaped}"`; + // Fish: single quotes prevent expansion; escape embedded single quotes with \' + return `set -gx ${key} '${value.replace(/'/g, "\\'")}'`; case 'powershell': - return `$env:${key} = "${escaped}"`; + // PowerShell: single quotes prevent expansion; escape embedded single quotes with '' + return `$env:${key} = '${value.replace(/'/g, "''")}'`; default: - return `export ${key}="${escaped}"`; + // Bash/zsh: single quotes prevent all expansion; handle embedded single quotes + return `export ${key}='${value.replace(/'/g, "'\\''")}'`; } } @@ -59,7 +60,7 @@ export function transformToOpenAI(envVars: Record): Record a.startsWith(`--${flag}=`)); - if (eqMatch) return eqMatch.split('=')[1]; + if (eqMatch) return eqMatch.split('=').slice(1).join('='); // --flag value style const idx = args.indexOf(`--${flag}`); if (idx >= 0 && idx + 1 < args.length && !args[idx + 1].startsWith('-')) { @@ -220,6 +221,14 @@ export async function handleEnvCommand(args: string[]): Promise { break; } + // Guard: format transformation may filter out all vars + if (Object.keys(output).filter((k) => output[k]).length === 0) { + console.error( + warn(`No ${format}-format vars found for profile '${profile}'. Try --format raw`) + ); + process.exit(1); + } + // Output shell-formatted exports to stdout for (const [key, value] of Object.entries(output)) { if (value) { diff --git a/tests/unit/commands/env-command.test.ts b/tests/unit/commands/env-command.test.ts index 898714e9..efcae254 100644 --- a/tests/unit/commands/env-command.test.ts +++ b/tests/unit/commands/env-command.test.ts @@ -3,13 +3,12 @@ * * Tests pure utility functions: detectShell, formatExportLine, transformToOpenAI */ -import { describe, it, expect, beforeEach, afterEach } from 'bun:test'; +import { describe, it, expect, afterEach } from 'bun:test'; import { detectShell, formatExportLine, transformToOpenAI } from '../../../src/commands/env-command'; describe('env-command', () => { describe('detectShell', () => { const originalShell = process.env['SHELL']; - const originalPlatform = process.platform; afterEach(() => { if (originalShell !== undefined) { @@ -59,32 +58,38 @@ describe('env-command', () => { describe('formatExportLine', () => { it('formats bash export', () => { - expect(formatExportLine('bash', 'API_KEY', 'sk-123')).toBe('export API_KEY="sk-123"'); + expect(formatExportLine('bash', 'API_KEY', 'sk-123')).toBe("export API_KEY='sk-123'"); }); it('formats fish export', () => { - expect(formatExportLine('fish', 'API_KEY', 'sk-123')).toBe('set -gx API_KEY "sk-123"'); + expect(formatExportLine('fish', 'API_KEY', 'sk-123')).toBe("set -gx API_KEY 'sk-123'"); }); it('formats powershell export', () => { expect(formatExportLine('powershell', 'API_KEY', 'sk-123')).toBe( - '$env:API_KEY = "sk-123"' + "$env:API_KEY = 'sk-123'" ); }); - it('escapes double quotes in values', () => { - expect(formatExportLine('bash', 'VAL', 'has "quotes"')).toBe( - 'export VAL="has \\"quotes\\""' + it('escapes single quotes in values', () => { + expect(formatExportLine('bash', 'VAL', "it's here")).toBe( + "export VAL='it'\\''s here'" ); }); it('handles empty values', () => { - expect(formatExportLine('bash', 'EMPTY', '')).toBe('export EMPTY=""'); + expect(formatExportLine('bash', 'EMPTY', '')).toBe("export EMPTY=''"); }); it('handles URLs with special characters', () => { const url = 'http://127.0.0.1:8317/api/provider/gemini'; - expect(formatExportLine('bash', 'BASE_URL', url)).toBe(`export BASE_URL="${url}"`); + expect(formatExportLine('bash', 'BASE_URL', url)).toBe(`export BASE_URL='${url}'`); + }); + + it('prevents shell injection with $() in values', () => { + expect(formatExportLine('bash', 'TOKEN', 'safe$(whoami)')).toBe( + "export TOKEN='safe$(whoami)'" + ); }); });