Skip to content

Commit 861d493

Browse files
committed
Embedded CLI runs refuse @path file arguments
readArgumentSource did a raw readFileSync on any @path — and the mothership runs the CLI in-process on the sim server with argv the model controls, so a prompt-injected --input @/etc/... read the SERVER's filesystem. Embedded runs (embedStore present) now refuse file and stdin sources with inline guidance; @@ literals and inline values are unchanged, and the installed CLI's behavior is untouched. Claude-Session: https://claude.ai/code/session_01CgaxNAaeD3taGdghbXn17w
1 parent fdd0e95 commit 861d493

2 files changed

Lines changed: 52 additions & 0 deletions

File tree

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { mkdtempSync, writeFileSync } from 'node:fs'
2+
import { tmpdir } from 'node:os'
3+
import { join } from 'node:path'
4+
import { describe, expect, it } from 'vitest'
5+
import { embedStore } from '../embed-context'
6+
import { readArgumentSource } from './request'
7+
8+
describe('file arguments in embedded runs', () => {
9+
it('refuses @path reads in-process, with inline guidance', () => {
10+
const ctx = {
11+
identity: { endpoint: 'http://x', apiKey: 'k' },
12+
stdout: [] as string[],
13+
stderr: [] as string[],
14+
}
15+
embedStore.run(ctx, () => {
16+
expect(() => readArgumentSource('@/etc/hostname', 'input')).toThrow(
17+
/not available in embedded runs.*inline/
18+
)
19+
})
20+
})
21+
22+
it('keeps @@ literal escape and inline values working embedded', () => {
23+
const ctx = {
24+
identity: { endpoint: 'http://x', apiKey: 'k' },
25+
stdout: [] as string[],
26+
stderr: [] as string[],
27+
}
28+
embedStore.run(ctx, () => {
29+
expect(readArgumentSource('@@literal', 'input').text).toBe('@literal')
30+
expect(readArgumentSource('{"a":1}', 'input').text).toBe('{"a":1}')
31+
})
32+
})
33+
34+
it('still reads files outside embedded runs', () => {
35+
const dir = mkdtempSync(join(tmpdir(), 'cli-args-'))
36+
const file = join(dir, 'v.json')
37+
writeFileSync(file, '{"ok":true}')
38+
expect(readArgumentSource(`@${file}`, 'input').text).toBe('{"ok":true}')
39+
})
40+
})

packages/sim-cli/src/runtime/request.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { existsSync, readFileSync, readSync } from 'node:fs'
22
import { CLI_CONTRACT } from '../contract/commands'
33
import type { CommandSpec, FlagSpec } from '../contract/types'
4+
import { embedStore } from '../embed-context'
45
import { V2_OPERATIONS, type V2OperationName } from '../generated/v2-api'
56
import { type QueryValue, SimApiError } from '../http/client'
67
import { camel, kebab } from './derive'
@@ -202,6 +203,17 @@ export function readArgumentSource(raw: string, flagName: string): { text: strin
202203
if (raw.startsWith('@@')) return { text: raw.slice(1), from: '' }
203204
if (!raw.startsWith('@')) return { text: raw, from: '' }
204205

206+
// An embedded run executes in-process on the hosting server, so a file path
207+
// here would read the SERVER's filesystem with argv the model controls.
208+
// There is no local file a caller could legitimately mean; the value must
209+
// arrive inline (or as @@-escaped literal text).
210+
if (embedStore.getStore()) {
211+
throw new SimApiError(
212+
`--${flagName} file arguments (@path) are not available in embedded runs — pass the JSON inline as a single argument`,
213+
0
214+
)
215+
}
216+
205217
const path = raw.slice(1)
206218
if (path === '-') {
207219
if (process.stdin.isTTY) {

0 commit comments

Comments
 (0)