Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ on:
pull_request:
branches: [main]

permissions:
contents: read
Comment thread
cameroncooke marked this conversation as resolved.

Comment thread
cameroncooke marked this conversation as resolved.
jobs:
build-and-test:
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/sentry.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ on:
tags:
- 'v*'

permissions:
contents: read

jobs:
release:
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
- Fixed `stop_mac_app` app-name targeting so it no longer terminates unrelated processes whose command lines contain the app name, and reject unsafe process IDs before execution ([#306](https://github.com/getsentry/XcodeBuildMCP/issues/306)).
- Fixed incremental `xcodemake` builds when DerivedData uses an absolute path by updating the pinned wrapper and delegating Makefile reuse to it so its argument and freshness checks are always applied ([#466](https://github.com/getsentry/XcodeBuildMCP/issues/466)).
- Fixed the scheduled Warden sweep to authenticate through OpenRouter and track the current v0 action release ([#483](https://github.com/getsentry/XcodeBuildMCP/issues/483)).
- Fixed shell-mode command execution so environment-derived arguments never become shell source, and restricted CI and Sentry release workflows to read-only repository access.

## [2.6.2]

Expand Down
62 changes: 62 additions & 0 deletions src/utils/__tests__/command.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,69 @@
import { chmod, mkdtemp, rm, writeFile } from 'fs/promises';
import { tmpdir } from 'os';
import { join } from 'path';
import { describe, expect, it } from 'vitest';
import { __getRealCommandExecutor } from '../command.ts';

describe('defaultExecutor', () => {
it('passes arguments literally when shell execution is requested', async () => {
const executor = __getRealCommandExecutor();
const argumentsWithMetacharacters = [
'$(printf injected)',
'`printf injected`',
'value; printf injected',
'$HOME',
"single'quote",
];

const result = await executor(
['/usr/bin/printf', '%s\n', ...argumentsWithMetacharacters],
'Shell Argument Test',
true,
);

expect(result).toMatchObject({
success: true,
exitCode: 0,
output: `${argumentsWithMetacharacters.join('\n')}\n`,
});
});

it('treats a leading-dash executable as a command name when shell execution is requested', async () => {
const executableDirectory = await mkdtemp(join(tmpdir(), 'xcodebuildmcp-command-'));
const executablePath = join(executableDirectory, '-c');
await writeFile(executablePath, '#!/bin/sh\nprintf "%s\\n" "$@"\n', 'utf8');
await chmod(executablePath, 0o700);

try {
const executor = __getRealCommandExecutor();
const result = await executor(['-c', 'literal argument'], 'Shell Executable Test', true, {
env: { PATH: executableDirectory },
});

expect(result).toMatchObject({
success: true,
exitCode: 0,
output: 'literal argument\n',
});
} finally {
await rm(executableDirectory, { recursive: true, force: true });
}
});

it('returns an exit response when a shell-mode executable is missing', async () => {
const executor = __getRealCommandExecutor();
const result = await executor(
['xcodebuildmcp-command-that-does-not-exist'],
'Missing Shell Executable Test',
true,
);

expect(result).toMatchObject({
success: false,
exitCode: 127,
});
});

it('settles after exit even when child close is delayed', async () => {
const executor = __getRealCommandExecutor();
const startedAt = Date.now();
Expand Down
17 changes: 8 additions & 9 deletions src/utils/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,15 @@
opts?: CommandExecOptions,
detached: boolean = false,
): Promise<CommandResponse> {
let escapedCommand = command;
if (useShell) {
const commandString = command.map((arg) => shellEscapeArg(arg)).join(' ');
let executable = command[0];
let args = command.slice(1);

escapedCommand = ['/bin/sh', '-c', commandString];
if (useShell) {
executable = '/usr/bin/env';
args = ['--', ...command];
}
Comment thread
cameroncooke marked this conversation as resolved.

return new Promise((resolve, reject) => {
let executable = escapedCommand[0];
let args = escapedCommand.slice(1);

if (!useShell && executable === 'xcodebuild') {
const xcrunPath = '/usr/bin/xcrun';
if (existsSync(xcrunPath)) {
Expand All @@ -37,8 +35,9 @@
}
}

const displayCommand =
useShell && escapedCommand.length === 3 ? escapedCommand[2] : [executable, ...args].join(' ');
const displayCommand = useShell
? command.map((arg) => shellEscapeArg(arg)).join(' ')
: [executable, ...args].join(' ');
log('debug', `Executing ${logPrefix ?? ''} command: ${displayCommand}`);

const emitTranscript = transcriptEmitterStorage.getStore();
Expand Down Expand Up @@ -68,7 +67,7 @@
};

const childProcess = spawn(executable, args, spawnOpts);

Check warning

Code scanning / CodeQL

Shell command built from environment values Medium

This shell command depends on an uncontrolled
absolute path
.
This shell command depends on an uncontrolled
absolute path
.
This shell command depends on an uncontrolled
absolute path
.
This shell command depends on an uncontrolled
absolute path
.
This shell command depends on an uncontrolled
absolute path
.
This shell command depends on an uncontrolled
absolute path
.
This shell command depends on an uncontrolled
absolute path
.
This shell command depends on an uncontrolled
file name
.
This shell command depends on an uncontrolled
absolute path
.
This shell command depends on an uncontrolled
file name
.
This shell command depends on an uncontrolled
absolute path
.
This shell command depends on an uncontrolled
absolute path
.
This shell command depends on an uncontrolled
absolute path
.
This shell command depends on an uncontrolled
absolute path
.
This shell command depends on an uncontrolled
file name
.
This shell command depends on an uncontrolled
absolute path
.
This shell command depends on an uncontrolled
absolute path
.
This shell command depends on an uncontrolled
absolute path
.
This shell command depends on an uncontrolled absolute path.
This shell command depends on an uncontrolled absolute path.
This shell command depends on an uncontrolled absolute path.
This shell command depends on an uncontrolled absolute path.
This shell command depends on an uncontrolled absolute path.
This shell command depends on an uncontrolled absolute path.
This shell command depends on an uncontrolled file name.
This shell command depends on an uncontrolled absolute path.
let stdout = '';
let stderr = '';

Expand Down
Loading