diff --git a/src/cli.ts b/src/cli.ts index da09f5e..abda237 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -24,6 +24,7 @@ import { detectEditors, configureMcp, ApiError, + buildMacClipboardArgs, type Model, type AspectRatio, type Style, @@ -123,7 +124,7 @@ async function copyToClipboard(imagePath: string): Promise { const { execFileSync } = await import('node:child_process') try { if (process.platform === 'darwin') { - execFileSync('osascript', ['-e', `set the clipboard to (read (POSIX file "${imagePath}") as TIFF picture)`]) + execFileSync('osascript', buildMacClipboardArgs(imagePath)) } else { execFileSync('xclip', ['-selection', 'clipboard', '-t', 'image/png', '-i', imagePath]) } diff --git a/src/core/clipboard.test.ts b/src/core/clipboard.test.ts new file mode 100644 index 0000000..949ba65 --- /dev/null +++ b/src/core/clipboard.test.ts @@ -0,0 +1,12 @@ +import { describe, it, expect } from 'vitest' +import { buildMacClipboardArgs } from './clipboard.js' + +describe('buildMacClipboardArgs', () => { + it('passes image path as argv, not interpolated script', () => { + const maliciousPath = 'safe" ) & do shell script "touch /tmp/pwned" & ("' + const args = buildMacClipboardArgs(maliciousPath) + + expect(args.at(-1)).toBe(maliciousPath) + expect(args.slice(0, -1).join(' ')).not.toContain(maliciousPath) + }) +}) diff --git a/src/core/clipboard.ts b/src/core/clipboard.ts new file mode 100644 index 0000000..083b001 --- /dev/null +++ b/src/core/clipboard.ts @@ -0,0 +1,13 @@ +export function buildMacClipboardArgs(imagePath: string): string[] { + return [ + '-e', + 'on run argv', + '-e', + 'set imagePath to POSIX file (item 1 of argv)', + '-e', + 'set the clipboard to (read imagePath as TIFF picture)', + '-e', + 'end run', + imagePath, + ] +} diff --git a/src/core/index.ts b/src/core/index.ts index 10d0bdd..c1d5d55 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -37,3 +37,4 @@ export { ApiError } from './types.js' export { timeAgo } from './utils.js' export { initiateDeviceAuth, pollForToken } from './device-auth.js' export { detectEditors, configureMcp, type EditorInfo } from './mcp-config.js' +export { buildMacClipboardArgs } from './clipboard.js'