mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-04-28 03:49:31 +00:00
- 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:
parent
58282f5727
commit
e9f8d5ec2d
3 changed files with 56 additions and 32 deletions
|
|
@ -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}"
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue