Skip to content

Commit e9f8d5e

Browse files
la14-1louisgvclaude
authored
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>
1 parent 58282f5 commit e9f8d5e

3 files changed

Lines changed: 56 additions & 32 deletions

File tree

sh/e2e/lib/clouds/digitalocean.sh

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,24 @@ _DO_API="https://api.digitalocean.com/v2"
1616
_DO_DEFAULT_SIZE="s-2vcpu-2gb"
1717
_DO_DEFAULT_REGION="nyc3"
1818

19+
# ---------------------------------------------------------------------------
20+
# _do_curl_auth [curl-args...]
21+
#
22+
# Wrapper around curl that passes the DO_API_TOKEN via a temp config file
23+
# instead of a command-line -H flag. This keeps the token out of `ps` output.
24+
# All arguments are forwarded to curl.
25+
# ---------------------------------------------------------------------------
26+
_do_curl_auth() {
27+
local _cfg
28+
_cfg=$(mktemp)
29+
chmod 600 "${_cfg}"
30+
printf 'header = "Authorization: Bearer %s"\n' "${DO_API_TOKEN}" > "${_cfg}"
31+
curl -K "${_cfg}" "$@"
32+
local _rc=$?
33+
rm -f "${_cfg}"
34+
return "${_rc}"
35+
}
36+
1937
# ---------------------------------------------------------------------------
2038
# _digitalocean_validate_env
2139
#
@@ -29,8 +47,7 @@ _digitalocean_validate_env() {
2947
return 1
3048
fi
3149

32-
if ! curl -sf \
33-
-H "Authorization: Bearer ${DO_API_TOKEN}" \
50+
if ! _do_curl_auth -sf \
3451
"${_DO_API}/account" >/dev/null 2>&1; then
3552
log_err "DigitalOcean API authentication failed — check DO_API_TOKEN"
3653
return 1
@@ -70,8 +87,7 @@ _digitalocean_provision_verify() {
7087
log_step "Checking for droplet ${app}..."
7188

7289
local droplets_json
73-
droplets_json=$(curl -sf \
74-
-H "Authorization: Bearer ${DO_API_TOKEN}" \
90+
droplets_json=$(_do_curl_auth -sf \
7591
-H "Content-Type: application/json" \
7692
"${_DO_API}/droplets?per_page=200" 2>/dev/null || true)
7793

@@ -206,10 +222,9 @@ _digitalocean_teardown() {
206222
attempt=$((attempt + 1))
207223

208224
local http_code
209-
http_code=$(curl -s -o /dev/null -w '%{http_code}' \
225+
http_code=$(_do_curl_auth -s -o /dev/null -w '%{http_code}' \
210226
--max-time 30 \
211227
-X DELETE \
212-
-H "Authorization: Bearer ${DO_API_TOKEN}" \
213228
-H "Content-Type: application/json" \
214229
"${_DO_API}/droplets/${droplet_id}" 2>/dev/null || printf '000')
215230

@@ -232,9 +247,8 @@ _digitalocean_teardown() {
232247
local poll_waited=0
233248
while [ "${poll_waited}" -lt 60 ]; do
234249
local check_code
235-
check_code=$(curl -s -o /dev/null -w '%{http_code}' \
250+
check_code=$(_do_curl_auth -s -o /dev/null -w '%{http_code}' \
236251
--max-time 10 \
237-
-H "Authorization: Bearer ${DO_API_TOKEN}" \
238252
"${_DO_API}/droplets/${droplet_id}" 2>/dev/null || printf '000')
239253

240254
if [ "${check_code}" = "404" ]; then
@@ -268,8 +282,7 @@ _digitalocean_cleanup_stale() {
268282
local max_age=1800 # 30 minutes in seconds
269283

270284
local droplets_json
271-
droplets_json=$(curl -sf \
272-
-H "Authorization: Bearer ${DO_API_TOKEN}" \
285+
droplets_json=$(_do_curl_auth -sf \
273286
-H "Content-Type: application/json" \
274287
"${_DO_API}/droplets?per_page=200" 2>/dev/null || true)
275288

@@ -318,9 +331,8 @@ _digitalocean_cleanup_stale() {
318331
age_str=$(format_duration "${age}")
319332
log_step "Destroying stale droplet ${droplet_name} (age: ${age_str})"
320333

321-
curl -sf -o /dev/null \
334+
_do_curl_auth -sf -o /dev/null \
322335
-X DELETE \
323-
-H "Authorization: Bearer ${DO_API_TOKEN}" \
324336
-H "Content-Type: application/json" \
325337
"${_DO_API}/droplets/${droplet_id}" 2>/dev/null || log_warn "Failed to destroy ${droplet_name}"
326338

sh/e2e/lib/clouds/hetzner.sh

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,24 @@ set -eo pipefail
77
# ---------------------------------------------------------------------------
88
_HETZNER_API="https://api.hetzner.cloud/v1"
99

10+
# ---------------------------------------------------------------------------
11+
# _hetzner_curl_auth [curl-args...]
12+
#
13+
# Wrapper around curl that passes the HCLOUD_TOKEN via a temp config file
14+
# instead of a command-line -H flag. This keeps the token out of `ps` output.
15+
# All arguments are forwarded to curl.
16+
# ---------------------------------------------------------------------------
17+
_hetzner_curl_auth() {
18+
local _cfg
19+
_cfg=$(mktemp)
20+
chmod 600 "${_cfg}"
21+
printf 'header = "Authorization: Bearer %s"\n' "${HCLOUD_TOKEN}" > "${_cfg}"
22+
curl -K "${_cfg}" "$@"
23+
local _rc=$?
24+
rm -f "${_cfg}"
25+
return "${_rc}"
26+
}
27+
1028
# ---------------------------------------------------------------------------
1129
# _hetzner_validate_env
1230
#
@@ -19,8 +37,7 @@ _hetzner_validate_env() {
1937
return 1
2038
fi
2139

22-
if ! curl -sf \
23-
-H "Authorization: Bearer ${HCLOUD_TOKEN}" \
40+
if ! _hetzner_curl_auth -sf \
2441
"${_HETZNER_API}/servers?per_page=1" >/dev/null 2>&1; then
2542
log_err "Hetzner API credentials are invalid"
2643
return 1
@@ -59,8 +76,7 @@ _hetzner_provision_verify() {
5976
encoded_app=$(jq -rn --arg v "${app}" '$v|@uri')
6077

6178
local response
62-
response=$(curl -sf \
63-
-H "Authorization: Bearer ${HCLOUD_TOKEN}" \
79+
response=$(_hetzner_curl_auth -sf \
6480
"${_HETZNER_API}/servers?name=${encoded_app}" 2>/dev/null || true)
6581

6682
if [ -z "${response}" ]; then
@@ -181,9 +197,8 @@ _hetzner_teardown() {
181197
log_step "Deleting Hetzner server ${app} (id=${server_id})"
182198

183199
local http_code
184-
http_code=$(curl -s -o /dev/null -w '%{http_code}' \
200+
http_code=$(_hetzner_curl_auth -s -o /dev/null -w '%{http_code}' \
185201
-X DELETE \
186-
-H "Authorization: Bearer ${HCLOUD_TOKEN}" \
187202
"${_HETZNER_API}/servers/${server_id}" 2>/dev/null || printf '000')
188203

189204
if [ "${http_code}" = "200" ] || [ "${http_code}" = "204" ]; then
@@ -209,8 +224,7 @@ _hetzner_cleanup_stale() {
209224
local max_age=1800 # 30 minutes
210225

211226
local response
212-
response=$(curl -sf \
213-
-H "Authorization: Bearer ${HCLOUD_TOKEN}" \
227+
response=$(_hetzner_curl_auth -sf \
214228
"${_HETZNER_API}/servers?per_page=50" 2>/dev/null || true)
215229

216230
if [ -z "${response}" ]; then
@@ -266,9 +280,8 @@ _hetzner_cleanup_stale() {
266280
log_step "Destroying stale Hetzner server ${server_name} (id=${server_id}, age: ${age_str})"
267281

268282
local http_code
269-
http_code=$(curl -s -o /dev/null -w '%{http_code}' \
283+
http_code=$(_hetzner_curl_auth -s -o /dev/null -w '%{http_code}' \
270284
-X DELETE \
271-
-H "Authorization: Bearer ${HCLOUD_TOKEN}" \
272285
"${_HETZNER_API}/servers/${server_id}" 2>/dev/null || printf '000')
273286

274287
if [ "${http_code}" = "200" ] || [ "${http_code}" = "204" ]; then

sh/e2e/lib/provision.sh

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,10 @@ provision_agent() {
6363
export OPENROUTER_API_KEY="${OPENROUTER_API_KEY}"
6464

6565
# Apply cloud-specific env vars (safe: only processes export VAR="VALUE" lines)
66-
# Uses sed instead of BASH_REMATCH for macOS bash 3.2 compatibility
66+
# Uses sed instead of BASH_REMATCH for macOS bash 3.2 compatibility.
67+
# Positive whitelist: only variables actually emitted by cloud_headless_env
68+
# functions are allowed. This prevents injection of arbitrary env vars.
69+
_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 "
6770
while IFS= read -r _env_line; do
6871
# Skip lines that don't look like export VAR="VALUE"
6972
case "${_env_line}" in
@@ -76,18 +79,14 @@ provision_agent() {
7679
if [ -z "${_env_name}" ]; then
7780
continue
7881
fi
79-
# Block dangerous system env vars that could enable privilege escalation
80-
case "${_env_name}" in
81-
PATH|LD_PRELOAD|LD_LIBRARY_PATH|HOME|SHELL|USER|IFS|ENV|BASH_ENV|CDPATH)
82-
log_err "Blocked dangerous env var: ${_env_name}"
82+
# Only allow whitelisted variable names (positive match)
83+
case "${_ALLOWED_HEADLESS_VARS}" in
84+
*" ${_env_name} "*) ;;
85+
*)
86+
log_err "Rejected unexpected env var from cloud_headless_env: ${_env_name}"
8387
continue
8488
;;
8589
esac
86-
# Validate env var name matches strict alphanumeric pattern
87-
if ! printf '%s' "${_env_name}" | grep -qE '^[A-Za-z_][A-Za-z0-9_]*$'; then
88-
log_err "Invalid env var name: ${_env_name}"
89-
continue
90-
fi
9190
# Validate value against a safe character whitelist BEFORE export
9291
if printf '%s' "${_env_val}" | grep -qE '[^A-Za-z0-9@%+=:,./_-]'; then
9392
log_err "Invalid characters in env value for ${_env_name}"

0 commit comments

Comments
 (0)