001: Docker Host Flag Precedence Bypass
Date: 2026-04-17
Severity: Medium
Impact: Docker API endpoint hijack
Subsystem: container
Final review by: gpt-5.4, high
Summary
stellar container accepts --docker-host as an explicit override, but its TCP/HTTP connection path drops that resolved value and calls Bollard's environment-driven default helper instead. When both --docker-host and DOCKER_HOST are set, start, stop, and logs can be silently redirected to the environment's Docker API endpoint rather than the operator's command-line target.
Root Cause
Args::connect_to_docker() correctly resolves the effective host into a local host variable, but the TCP/HTTP match arm uses Docker::connect_with_http_defaults() rather than Docker::connect_with_http(&h, ...). In Bollard 0.20.2, connect_with_http_defaults() re-reads DOCKER_HOST from the environment, so the explicit CLI flag's precedence is lost after argument parsing.
Reproduction
During normal operation, a user can invoke any stellar container subcommand with an explicit TCP --docker-host while inheriting a different DOCKER_HOST value from their shell, wrapper script, CI job, or other launch environment. The CLI then sends Docker API traffic to the environment-selected endpoint, even though its fallback warning still prints the explicit flag value.
Affected Code
stellar-cli/cmd/soroban-cli/src/commands/container/shared.rs:52-105 — resolves host, then ignores it for TCP/HTTP by calling Docker::connect_with_http_defaults()
stellar-cli/cmd/soroban-cli/src/commands/container/start.rs:88-92 — container start uses the shared connection helper
stellar-cli/cmd/soroban-cli/src/commands/container/stop.rs:39 — container stop uses the shared connection helper
stellar-cli/cmd/soroban-cli/src/commands/container/logs.rs:34 — container logs uses the shared connection helper
/root/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bollard-0.20.2/src/docker.rs:567-590 — the default HTTP helper re-reads DOCKER_HOST
PoC
- Target test file:
stellar-cli/poc/docker-host-flag-bypass
- Test name: docker-host-flag-bypass
- Test language: bash
- How to run:
- Run
cargo build from the repo root.
- Copy the script below to
poc/docker-host-flag-bypass.
- Run:
bash poc/docker-host-flag-bypass
Test Body
#!/usr/bin/env bash
set -uo pipefail
STELLAR="$(pwd)/target/debug/stellar"
FLAG_PORT=19999
ENVVAR_PORT=29999
echo "=== PoC: Docker Host Flag Precedence Bypass ==="
echo "DOCKER_HOST env var: tcp://127.0.0.1:$ENVVAR_PORT"
echo "--docker-host flag: tcp://127.0.0.1:$FLAG_PORT"
echo ""
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"; kill $FLAG_PID $ENVVAR_PID 2>/dev/null' EXIT
python3 -c "
import socket, sys
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('127.0.0.1', int(sys.argv[1])))
s.listen(1)
s.settimeout(10)
try:
conn, addr = s.accept()
data = conn.recv(4096)
with open(sys.argv[2], 'wb') as f:
f.write(data)
conn.close()
except socket.timeout:
pass
s.close()
" $FLAG_PORT "$TMPDIR/flag_conn.log" &
FLAG_PID=$!
python3 -c "
import socket, sys
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(('127.0.0.1', int(sys.argv[1])))
s.listen(1)
s.settimeout(10)
try:
conn, addr = s.accept()
data = conn.recv(4096)
with open(sys.argv[2], 'wb') as f:
f.write(data)
conn.close()
except socket.timeout:
pass
s.close()
" $ENVVAR_PORT "$TMPDIR/envvar_conn.log" &
ENVVAR_PID=$!
sleep 1
echo "Running: DOCKER_HOST=tcp://127.0.0.1:$ENVVAR_PORT stellar container start local --docker-host tcp://127.0.0.1:$FLAG_PORT"
echo ""
DOCKER_HOST="tcp://127.0.0.1:$ENVVAR_PORT" \
"$STELLAR" container start local --docker-host "tcp://127.0.0.1:$FLAG_PORT" 2>&1 || true
sleep 2
echo ""
echo "=== Connection Results ==="
FLAG_DATA=""
ENVVAR_DATA=""
if [ -f "$TMPDIR/flag_conn.log" ] && [ -s "$TMPDIR/flag_conn.log" ]; then
FLAG_DATA=$(cat "$TMPDIR/flag_conn.log" | head -c 200)
echo "Flag port ($FLAG_PORT) received data: YES"
echo " Data: $FLAG_DATA"
else
echo "Flag port ($FLAG_PORT) received data: NO"
fi
if [ -f "$TMPDIR/envvar_conn.log" ] && [ -s "$TMPDIR/envvar_conn.log" ]; then
ENVVAR_DATA=$(cat "$TMPDIR/envvar_conn.log" | head -c 200)
echo "Env var port ($ENVVAR_PORT) received data: YES"
echo " Data: $ENVVAR_DATA"
else
echo "Env var port ($ENVVAR_PORT) received data: NO"
fi
echo ""
if [ -n "$ENVVAR_DATA" ] && [ -z "$FLAG_DATA" ]; then
echo "=== FINDING CONFIRMED ==="
echo "The CLI connected to port $ENVVAR_PORT (DOCKER_HOST env var) instead of"
echo "port $FLAG_PORT (--docker-host flag). The explicit flag is ignored for TCP."
exit 0
elif [ -n "$FLAG_DATA" ] && [ -z "$ENVVAR_DATA" ]; then
echo "=== FINDING NOT CONFIRMED ==="
echo "The CLI correctly used the --docker-host flag (port $FLAG_PORT)."
exit 1
elif [ -n "$FLAG_DATA" ] && [ -n "$ENVVAR_DATA" ]; then
echo "=== BOTH PORTS CONTACTED ==="
echo "Both ports received connections."
exit 2
else
echo "=== INCONCLUSIVE ==="
echo "Neither port received a connection."
exit 2
fi
Expected vs Actual Behavior
- Expected: A TCP/HTTP
--docker-host flag should override any inherited DOCKER_HOST value, so Docker API traffic goes only to the operator-selected endpoint.
- Actual: The CLI sends Docker API traffic to the
DOCKER_HOST environment endpoint while user-facing diagnostics still refer to the explicit flag value.
Adversarial Review
- Exercises claimed bug: YES — the reproduced request hit only the environment-selected listener (
127.0.0.1:29999) and not the flag-selected listener (127.0.0.1:19999).
- Realistic preconditions: YES — inherited environment variables are common in shells, wrapper scripts, and CI; the PoC uses only the public CLI surface.
- Bug vs by-design: BUG — the help text describes
--docker-host as an override, and the Unix/named-pipe branches already honor the resolved explicit value.
- Final severity: Medium — this can silently redirect container lifecycle actions to an attacker-chosen Docker API endpoint, but it still requires control of the launched process environment.
- In scope: YES — no privileged machine access is required.
- Test correctness: CORRECT — separate listeners on distinct ports make the destination unambiguous, and the observed
GET /version request is the real Docker connectivity probe from check_docker_connection().
- Alternative explanations: NONE
- Novelty: NOVEL
Suggested Fix
Replace Docker::connect_with_http_defaults() with Docker::connect_with_http(&h, DEFAULT_TIMEOUT, API_DEFAULT_VERSION) in the TCP/HTTP branch so the already-resolved host value is passed through consistently.
001: Docker Host Flag Precedence Bypass
Date: 2026-04-17
Severity: Medium
Impact: Docker API endpoint hijack
Subsystem: container
Final review by: gpt-5.4, high
Summary
stellar containeraccepts--docker-hostas an explicit override, but its TCP/HTTP connection path drops that resolved value and calls Bollard's environment-driven default helper instead. When both--docker-hostandDOCKER_HOSTare set,start,stop, andlogscan be silently redirected to the environment's Docker API endpoint rather than the operator's command-line target.Root Cause
Args::connect_to_docker()correctly resolves the effective host into a localhostvariable, but the TCP/HTTP match arm usesDocker::connect_with_http_defaults()rather thanDocker::connect_with_http(&h, ...). In Bollard 0.20.2,connect_with_http_defaults()re-readsDOCKER_HOSTfrom the environment, so the explicit CLI flag's precedence is lost after argument parsing.Reproduction
During normal operation, a user can invoke any
stellar containersubcommand with an explicit TCP--docker-hostwhile inheriting a differentDOCKER_HOSTvalue from their shell, wrapper script, CI job, or other launch environment. The CLI then sends Docker API traffic to the environment-selected endpoint, even though its fallback warning still prints the explicit flag value.Affected Code
stellar-cli/cmd/soroban-cli/src/commands/container/shared.rs:52-105— resolveshost, then ignores it for TCP/HTTP by callingDocker::connect_with_http_defaults()stellar-cli/cmd/soroban-cli/src/commands/container/start.rs:88-92—container startuses the shared connection helperstellar-cli/cmd/soroban-cli/src/commands/container/stop.rs:39—container stopuses the shared connection helperstellar-cli/cmd/soroban-cli/src/commands/container/logs.rs:34—container logsuses the shared connection helper/root/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/bollard-0.20.2/src/docker.rs:567-590— the default HTTP helper re-readsDOCKER_HOSTPoC
stellar-cli/poc/docker-host-flag-bypasscargo buildfrom the repo root.poc/docker-host-flag-bypass.bash poc/docker-host-flag-bypassTest Body
Expected vs Actual Behavior
--docker-hostflag should override any inheritedDOCKER_HOSTvalue, so Docker API traffic goes only to the operator-selected endpoint.DOCKER_HOSTenvironment endpoint while user-facing diagnostics still refer to the explicit flag value.Adversarial Review
127.0.0.1:29999) and not the flag-selected listener (127.0.0.1:19999).--docker-hostas an override, and the Unix/named-pipe branches already honor the resolved explicit value.GET /versionrequest is the real Docker connectivity probe fromcheck_docker_connection().Suggested Fix
Replace
Docker::connect_with_http_defaults()withDocker::connect_with_http(&h, DEFAULT_TIMEOUT, API_DEFAULT_VERSION)in the TCP/HTTP branch so the already-resolvedhostvalue is passed through consistently.