fix: secure curl header args and provision.sh export whitelist (fixes #2464, fixes #2465) (#2471)

- Replace `-H "Authorization: Bearer ..."` curl args with temp curl config
  files (`-K`) in digitalocean.sh and hetzner.sh e2e drivers, keeping API
  tokens out of `ps` output
- Replace dangerous-var blocklist in provision.sh with a positive whitelist
  of allowed cloud_headless_env variable names

Agent: complexity-hunter

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
A 2026-03-10 17:54:32 -07:00 committed by GitHub
parent 58282f5727
commit e9f8d5ec2d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 56 additions and 32 deletions

View file

@ -16,6 +16,24 @@ _DO_API="https://api.digitalocean.com/v2"
_DO_DEFAULT_SIZE="s-2vcpu-2gb"
_DO_DEFAULT_REGION="nyc3"
# ---------------------------------------------------------------------------
# _do_curl_auth [curl-args...]
#
# Wrapper around curl that passes the DO_API_TOKEN via a temp config file
# instead of a command-line -H flag. This keeps the token out of `ps` output.
# All arguments are forwarded to curl.
# ---------------------------------------------------------------------------
_do_curl_auth() {
local _cfg
_cfg=$(mktemp)
chmod 600 "${_cfg}"
printf 'header = "Authorization: Bearer %s"\n' "${DO_API_TOKEN}" > "${_cfg}"
curl -K "${_cfg}" "$@"
local _rc=$?
rm -f "${_cfg}"
return "${_rc}"
}
# ---------------------------------------------------------------------------
# _digitalocean_validate_env
#
@ -29,8 +47,7 @@ _digitalocean_validate_env() {
return 1
fi
if ! curl -sf \
-H "Authorization: Bearer ${DO_API_TOKEN}" \
if ! _do_curl_auth -sf \
"${_DO_API}/account" >/dev/null 2>&1; then
log_err "DigitalOcean API authentication failed — check DO_API_TOKEN"
return 1
@ -70,8 +87,7 @@ _digitalocean_provision_verify() {
log_step "Checking for droplet ${app}..."
local droplets_json
droplets_json=$(curl -sf \
-H "Authorization: Bearer ${DO_API_TOKEN}" \
droplets_json=$(_do_curl_auth -sf \
-H "Content-Type: application/json" \
"${_DO_API}/droplets?per_page=200" 2>/dev/null || true)
@ -206,10 +222,9 @@ _digitalocean_teardown() {
attempt=$((attempt + 1))
local http_code
http_code=$(curl -s -o /dev/null -w '%{http_code}' \
http_code=$(_do_curl_auth -s -o /dev/null -w '%{http_code}' \
--max-time 30 \
-X DELETE \
-H "Authorization: Bearer ${DO_API_TOKEN}" \
-H "Content-Type: application/json" \
"${_DO_API}/droplets/${droplet_id}" 2>/dev/null || printf '000')
@ -232,9 +247,8 @@ _digitalocean_teardown() {
local poll_waited=0
while [ "${poll_waited}" -lt 60 ]; do
local check_code
check_code=$(curl -s -o /dev/null -w '%{http_code}' \
check_code=$(_do_curl_auth -s -o /dev/null -w '%{http_code}' \
--max-time 10 \
-H "Authorization: Bearer ${DO_API_TOKEN}" \
"${_DO_API}/droplets/${droplet_id}" 2>/dev/null || printf '000')
if [ "${check_code}" = "404" ]; then
@ -268,8 +282,7 @@ _digitalocean_cleanup_stale() {
local max_age=1800 # 30 minutes in seconds
local droplets_json
droplets_json=$(curl -sf \
-H "Authorization: Bearer ${DO_API_TOKEN}" \
droplets_json=$(_do_curl_auth -sf \
-H "Content-Type: application/json" \
"${_DO_API}/droplets?per_page=200" 2>/dev/null || true)
@ -318,9 +331,8 @@ _digitalocean_cleanup_stale() {
age_str=$(format_duration "${age}")
log_step "Destroying stale droplet ${droplet_name} (age: ${age_str})"
curl -sf -o /dev/null \
_do_curl_auth -sf -o /dev/null \
-X DELETE \
-H "Authorization: Bearer ${DO_API_TOKEN}" \
-H "Content-Type: application/json" \
"${_DO_API}/droplets/${droplet_id}" 2>/dev/null || log_warn "Failed to destroy ${droplet_name}"

View file

@ -7,6 +7,24 @@ set -eo pipefail
# ---------------------------------------------------------------------------
_HETZNER_API="https://api.hetzner.cloud/v1"
# ---------------------------------------------------------------------------
# _hetzner_curl_auth [curl-args...]
#
# Wrapper around curl that passes the HCLOUD_TOKEN via a temp config file
# instead of a command-line -H flag. This keeps the token out of `ps` output.
# All arguments are forwarded to curl.
# ---------------------------------------------------------------------------
_hetzner_curl_auth() {
local _cfg
_cfg=$(mktemp)
chmod 600 "${_cfg}"
printf 'header = "Authorization: Bearer %s"\n' "${HCLOUD_TOKEN}" > "${_cfg}"
curl -K "${_cfg}" "$@"
local _rc=$?
rm -f "${_cfg}"
return "${_rc}"
}
# ---------------------------------------------------------------------------
# _hetzner_validate_env
#
@ -19,8 +37,7 @@ _hetzner_validate_env() {
return 1
fi
if ! curl -sf \
-H "Authorization: Bearer ${HCLOUD_TOKEN}" \
if ! _hetzner_curl_auth -sf \
"${_HETZNER_API}/servers?per_page=1" >/dev/null 2>&1; then
log_err "Hetzner API credentials are invalid"
return 1
@ -59,8 +76,7 @@ _hetzner_provision_verify() {
encoded_app=$(jq -rn --arg v "${app}" '$v|@uri')
local response
response=$(curl -sf \
-H "Authorization: Bearer ${HCLOUD_TOKEN}" \
response=$(_hetzner_curl_auth -sf \
"${_HETZNER_API}/servers?name=${encoded_app}" 2>/dev/null || true)
if [ -z "${response}" ]; then
@ -181,9 +197,8 @@ _hetzner_teardown() {
log_step "Deleting Hetzner server ${app} (id=${server_id})"
local http_code
http_code=$(curl -s -o /dev/null -w '%{http_code}' \
http_code=$(_hetzner_curl_auth -s -o /dev/null -w '%{http_code}' \
-X DELETE \
-H "Authorization: Bearer ${HCLOUD_TOKEN}" \
"${_HETZNER_API}/servers/${server_id}" 2>/dev/null || printf '000')
if [ "${http_code}" = "200" ] || [ "${http_code}" = "204" ]; then
@ -209,8 +224,7 @@ _hetzner_cleanup_stale() {
local max_age=1800 # 30 minutes
local response
response=$(curl -sf \
-H "Authorization: Bearer ${HCLOUD_TOKEN}" \
response=$(_hetzner_curl_auth -sf \
"${_HETZNER_API}/servers?per_page=50" 2>/dev/null || true)
if [ -z "${response}" ]; then
@ -266,9 +280,8 @@ _hetzner_cleanup_stale() {
log_step "Destroying stale Hetzner server ${server_name} (id=${server_id}, age: ${age_str})"
local http_code
http_code=$(curl -s -o /dev/null -w '%{http_code}' \
http_code=$(_hetzner_curl_auth -s -o /dev/null -w '%{http_code}' \
-X DELETE \
-H "Authorization: Bearer ${HCLOUD_TOKEN}" \
"${_HETZNER_API}/servers/${server_id}" 2>/dev/null || printf '000')
if [ "${http_code}" = "200" ] || [ "${http_code}" = "204" ]; then

View file

@ -63,7 +63,10 @@ provision_agent() {
export OPENROUTER_API_KEY="${OPENROUTER_API_KEY}"
# Apply cloud-specific env vars (safe: only processes export VAR="VALUE" lines)
# Uses sed instead of BASH_REMATCH for macOS bash 3.2 compatibility
# Uses sed instead of BASH_REMATCH for macOS bash 3.2 compatibility.
# Positive whitelist: only variables actually emitted by cloud_headless_env
# functions are allowed. This prevents injection of arbitrary env vars.
_ALLOWED_HEADLESS_VARS=" LIGHTSAIL_SERVER_NAME AWS_DEFAULT_REGION LIGHTSAIL_BUNDLE DO_DROPLET_NAME DO_DROPLET_SIZE DO_REGION GCP_INSTANCE_NAME GCP_PROJECT GCP_ZONE GCP_MACHINE_TYPE HETZNER_SERVER_NAME HETZNER_SERVER_TYPE HETZNER_LOCATION "
while IFS= read -r _env_line; do
# Skip lines that don't look like export VAR="VALUE"
case "${_env_line}" in
@ -76,18 +79,14 @@ provision_agent() {
if [ -z "${_env_name}" ]; then
continue
fi
# Block dangerous system env vars that could enable privilege escalation
case "${_env_name}" in
PATH|LD_PRELOAD|LD_LIBRARY_PATH|HOME|SHELL|USER|IFS|ENV|BASH_ENV|CDPATH)
log_err "Blocked dangerous env var: ${_env_name}"
# Only allow whitelisted variable names (positive match)
case "${_ALLOWED_HEADLESS_VARS}" in
*" ${_env_name} "*) ;;
*)
log_err "Rejected unexpected env var from cloud_headless_env: ${_env_name}"
continue
;;
esac
# Validate env var name matches strict alphanumeric pattern
if ! printf '%s' "${_env_name}" | grep -qE '^[A-Za-z_][A-Za-z0-9_]*$'; then
log_err "Invalid env var name: ${_env_name}"
continue
fi
# Validate value against a safe character whitelist BEFORE export
if printf '%s' "${_env_val}" | grep -qE '[^A-Za-z0-9@%+=:,./_-]'; then
log_err "Invalid characters in env value for ${_env_name}"