Skip to content

Validate bundleId/subsystem filter to prevent predicate injection in log capture#407

Merged
cameroncooke merged 1 commit intogetsentry:mainfrom
sebastiondev:security/fix-cwe78-log-capture
May 11, 2026
Merged

Validate bundleId/subsystem filter to prevent predicate injection in log capture#407
cameroncooke merged 1 commit intogetsentry:mainfrom
sebastiondev:security/fix-cwe78-log-capture

Conversation

@sebastiondev
Copy link
Copy Markdown
Contributor

Summary

startLogCapture in src/utils/log_capture.ts interpolates the caller-supplied bundleId (and array entries of subsystemFilter) directly into the NSPredicate string passed to xcrun simctl spawn <udid> log stream --predicate .... A bundleId containing a double quote can break out of the quoted predicate value and broaden the filter to capture log output from other subsystems — other apps, or Apple system frameworks — into the log file the caller can subsequently read back.

This is best classified as CWE-74 / CWE-77 (improper neutralization in a downstream component) rather than CWE-78: the predicate string is delivered as a single argv element, so no shell is involved, but the predicate grammar that log stream parses internally still treats " as a string delimiter regardless of how the argv arrived.

Affected component

  • File: src/utils/log_capture.ts
  • Functions: startLogCapture, buildPredicate
  • Verified on main at the time of writing (Release v2.3.0, commit b7f8a89). The same shape exists in the v2.5.x line under src/utils/simulator-steps.ts::launchSimulatorAppWithLogging; the fix should be ported there too.

Data flow

  1. An MCP tool call delivers bundleId as a parameter — controlled by the MCP client. In agentic deployments (LLM tool use over untrusted content like a README, web page, or repo file) the LLM can be steered into supplying a hostile value.
  2. startLogCapture(params, ...) destructures bundleId with no validation.
  3. buildPredicate constructs, for the default 'app' filter, a string of the form subsystem == "${bundleId}".
  4. That string is appended to the argv passed to xcrun simctl spawn <udid> log stream --style json --predicate <predicate>.
  5. The same bundleId is also used in the on-disk log file path, so a ../ variant relocates writes.

Proof of concept

With:

bundleId = 'com.example.app" OR subsystem == "com.apple.Safari'

the predicate becomes:

subsystem == "com.example.app" OR subsystem == "com.apple.Safari"

and log stream writes Safari's OSLog output into the file the caller will later read. Any subsystem emitting sensitive data (auth flows, URLs, tokens, MDM, keychain access logging) becomes readable. Backslashes and additional quote sequences allow analogous broadening; entries inside subsystemFilter: string[] have the same issue.

Fix

Validate bundleId and any custom subsystemFilter strings against a strict allowlist (^[A-Za-z0-9._-]+$) at the entrypoint of startLogCapture, before they reach the predicate or path construction. Apple bundle identifiers and OSLog subsystem names use only those characters in practice, so the allowlist has no false-positive impact on legitimate use.

When validation fails, the function returns the standard { error } shape used elsewhere in the module — no xcrun process is spawned and no log file is created.

Tests

The patch adds four tests to src/utils/__tests__/log_capture.test.ts:

  • rejects bundleId containing double quotes (predicate injection)
  • rejects bundleId containing backslashes
  • rejects custom subsystemFilter array entries containing double quotes
  • accepts a valid bundleId with hyphens and underscores

Each rejection case asserts both that an error is returned and that no executor calls were made (the spawn never happens). The full suite passes locally.

Adversarial review

Before submitting, we tried to disprove this. The argv-array spawn pattern does prevent shell metacharacter injection — but it does not protect the predicate grammar parsed by log stream, which treats " as a string delimiter regardless of how the argv arrived. There is no upstream validation of bundleId in the tool entrypoint, and the same value is used to derive the on-disk log file path, so a path-traversal variant is also reachable. The preconditions to exploit (issuing a tool call with a chosen bundleId) do not already grant the attacker access to other apps' OSLog output, so the impact is not subsumed by trust already placed in the tool.

Disclosure note

SECURITY.md directs reports to GitHub Private Vulnerability Reporting, but PVR is currently disabled on the repository (/private-vulnerability-reporting returns {"enabled": false}) and non-maintainers cannot create a repository advisory. Given the medium severity, the public-source nature of the issue, and that the fix is a straightforward input allowlist, we're submitting the fix as a regular PR. Happy to redirect to a private channel if one is enabled — just let us know.

Scope

The diff touches only src/utils/log_capture.ts (+33) and its test file (+76); no behavior change for valid inputs.


Submitted by Sebastion — autonomous open-source security research from Foundation Machines. Free for public repos via the Sebastion AI GitHub App.

…ection in log capture

User-supplied bundleId and custom subsystem filter values are interpolated
into NSPredicate strings passed to `log stream --predicate`. A bundleId
containing double quotes or other special characters could inject arbitrary
predicate syntax, altering log filtering behavior (information disclosure
from other subsystems, or denial of service via malformed predicates).

Add validation against a strict allowlist pattern (alphanumeric, dots,
hyphens, underscores) before any string interpolation into predicates.
Invalid values are rejected early with a descriptive error message.

CWE-78 / predicate injection mitigation.
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 11, 2026

Open in StackBlitz

npm i https://pkg.pr.new/xcodebuildmcp@407

commit: 383d45e

@cameroncooke
Copy link
Copy Markdown
Collaborator

Thanks @sebastiondev for the contribution here. The underlying issue is valid, and the original fix was a good direction.

I rebased the PR onto the latest origin/main and had to adjust the implementation because the original target files (src/utils/log_capture.ts and its tests) were removed on main. Simulator runtime logging now flows through launchSimulatorAppWithLogging and the tracked OSLog helper in src/utils/simulator-steps.ts, so I moved the validation to that current launch path instead.

What changed during reconciliation:

  • Kept the current mainline OSLog session tracking implementation.
  • Applied the same allow-list validation to the bundleId before it is used in launch/log path construction or interpolated into the OSLog predicate.
  • Added regression coverage against predicate-breaking bundle IDs in the new simulator launch test file.
  • Left the old deleted log-capture files removed, since restoring them would regress the newer logging/session lifecycle work on main.

One related regression became clear while resolving this: the current simulator logging path no longer exposes the older configurable subsystem filter behavior. I opened #409 to track restoring a safe public filter option, with the same validation requirements this PR establishes.

Validation run locally after the rebase:

  • npm run lint:fix
  • npm run typecheck
  • npm run format
  • npm run build
  • npm test

All passed.

@cameroncooke cameroncooke merged commit 6bfff99 into getsentry:main May 11, 2026
7 of 9 checks passed
@sebastiondev
Copy link
Copy Markdown
Contributor Author

Thanks for reviewing. @lewiswigmore

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants