fix: address SSH command injection risks in e2e cloud drivers (#2447)

Add defense-in-depth validation across all e2e cloud driver scripts:

- Validate IP addresses match IPv4 format before use in SSH commands
  (aws, digitalocean, gcp, hetzner)
- Validate SSH username contains only safe characters (gcp)
- Validate resource IDs are numeric before interpolating into API URLs
  (digitalocean droplet IDs, hetzner server IDs)
- URL-encode app name in Hetzner API query parameter to prevent
  query parameter injection
- Validate numeric env vars (INPUT_TEST_TIMEOUT, PROVISION_TIMEOUT,
  INSTALL_WAIT) that get interpolated into remote command strings

Fixes #2432, #2433, #2434, #2435, #2442

Agent: security-auditor

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
A 2026-03-10 09:27:47 -07:00 committed by GitHub
parent 0380ad33f9
commit 3724bb8ba4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 59 additions and 1 deletions

View file

@ -136,6 +136,13 @@ _aws_exec() {
log_err "Could not resolve IP for instance ${app}"
return 1
fi
# Validate IP looks like an IPv4 address (defense-in-depth against API/file tampering)
if ! printf '%s' "${_AWS_INSTANCE_IP}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then
log_err "Invalid IP address for instance ${app}: ${_AWS_INSTANCE_IP}"
_AWS_INSTANCE_IP=""
_AWS_INSTANCE_APP=""
return 1
fi
fi
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \

View file

@ -149,6 +149,12 @@ _digitalocean_exec() {
return 1
fi
# Validate IP looks like an IPv4 address (defense-in-depth against file tampering)
if ! printf '%s' "${ip}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then
log_err "Invalid IP address in ${ip_file}: ${ip}"
return 1
fi
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o ConnectTimeout=10 -o LogLevel=ERROR -o BatchMode=yes \
"root@${ip}" "${cmd}"
@ -183,6 +189,9 @@ _digitalocean_teardown() {
return 0
fi
# Validate droplet ID is numeric (defense-in-depth against metadata tampering)
case "${droplet_id}" in ''|*[!0-9]*) log_warn "Non-numeric droplet ID: ${droplet_id}"; untrack_app "${app}"; return 0 ;; esac
# Retry DELETE up to 3 times with --max-time to prevent hangs
local attempt=0
local delete_accepted=0
@ -281,6 +290,9 @@ _digitalocean_cleanup_stale() {
local droplet_name
droplet_name=$(printf '%s' "${line}" | cut -d' ' -f2)
# Validate droplet ID is numeric before using it in API URL
case "${droplet_id}" in ''|*[!0-9]*) log_warn "Skipping ${line} — non-numeric droplet ID"; skipped=$((skipped + 1)); continue ;; esac
# Extract timestamp from name: e2e-AGENT-TIMESTAMP
# The timestamp is the last dash-separated segment
local ts

View file

@ -126,6 +126,12 @@ _gcp_exec() {
local cmd="$2"
local ssh_user="${GCP_SSH_USER:-$(whoami)}"
# Validate SSH user contains only safe characters (defense-in-depth)
if ! printf '%s' "${ssh_user}" | grep -qE '^[a-zA-Z0-9._-]+$'; then
log_err "Invalid SSH user for instance ${app}: ${ssh_user}"
return 1
fi
# Resolve instance IP (cached per app)
if [ "${_GCP_INSTANCE_APP}" != "${app}" ] || [ -z "${_GCP_INSTANCE_IP}" ]; then
# Try reading from the IP file first (written by _gcp_provision_verify)
@ -143,6 +149,13 @@ _gcp_exec() {
log_err "Could not resolve IP for instance ${app}"
return 1
fi
# Validate IP looks like an IPv4 address (defense-in-depth against API/file tampering)
if ! printf '%s' "${_GCP_INSTANCE_IP}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then
log_err "Invalid IP address for instance ${app}: ${_GCP_INSTANCE_IP}"
_GCP_INSTANCE_IP=""
_GCP_INSTANCE_APP=""
return 1
fi
fi
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \

View file

@ -54,10 +54,14 @@ _hetzner_provision_verify() {
local app="$1"
local log_dir="$2"
# URL-encode the app name to prevent query parameter injection
local encoded_app
encoded_app=$(jq -rn --arg v "${app}" '$v|@uri')
local response
response=$(curl -sf \
-H "Authorization: Bearer ${HCLOUD_TOKEN}" \
"${_HETZNER_API}/servers?name=${app}" 2>/dev/null || true)
"${_HETZNER_API}/servers?name=${encoded_app}" 2>/dev/null || true)
if [ -z "${response}" ]; then
log_err "Failed to query Hetzner API for server ${app}"
@ -120,6 +124,17 @@ _hetzner_exec() {
local ip
ip=$(cat "${ip_file}")
if [ -z "${ip}" ]; then
log_err "Empty IP in ${ip_file}"
return 1
fi
# Validate IP looks like an IPv4 address (defense-in-depth against file tampering)
if ! printf '%s' "${ip}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then
log_err "Invalid IP address in ${ip_file}: ${ip}"
return 1
fi
ssh -o StrictHostKeyChecking=no \
-o UserKnownHostsFile=/dev/null \
-o LogLevel=ERROR \
@ -153,6 +168,9 @@ _hetzner_teardown() {
return 0
fi
# Validate server ID is numeric (defense-in-depth against metadata tampering)
case "${server_id}" in ''|*[!0-9]*) log_warn "Non-numeric server ID: ${server_id}"; untrack_app "${app}"; return 0 ;; esac
log_step "Deleting Hetzner server ${app} (id=${server_id})"
local http_code
@ -220,6 +238,9 @@ _hetzner_cleanup_stale() {
local server_name
server_name=$(printf '%s' "${entry}" | cut -d: -f2-)
# Validate server ID is numeric before using it in API URL
case "${server_id}" in ''|*[!0-9]*) log_warn "Skipping ${entry} — non-numeric server ID"; skipped=$((skipped + 1)); continue ;; esac
# Extract timestamp from name: e2e-AGENT-TIMESTAMP
local ts
ts=$(printf '%s' "${server_name}" | sed 's/.*-//')

View file

@ -9,6 +9,11 @@ ALL_AGENTS="claude openclaw zeroclaw codex opencode kilocode hermes junie"
PROVISION_TIMEOUT="${PROVISION_TIMEOUT:-720}"
INSTALL_WAIT="${INSTALL_WAIT:-600}"
INPUT_TEST_TIMEOUT="${INPUT_TEST_TIMEOUT:-120}"
# Validate numeric env vars that get interpolated into remote command strings.
# A non-numeric value here could lead to shell injection via SSH commands.
case "${PROVISION_TIMEOUT}" in ''|*[!0-9]*) PROVISION_TIMEOUT=720 ;; esac
case "${INSTALL_WAIT}" in ''|*[!0-9]*) INSTALL_WAIT=600 ;; esac
case "${INPUT_TEST_TIMEOUT}" in ''|*[!0-9]*) INPUT_TEST_TIMEOUT=120 ;; esac
# Active cloud (set by load_cloud_driver)
ACTIVE_CLOUD=""