fix(security): harden remote command construction in provision.sh (#2886)

* fix(security): harden remote command construction in provision.sh

Split the .spawnrc upload fallback into two separate cloud_exec calls
to separate data from commands. Step 1 writes the validated base64
payload to a remote temp file. Step 2 decodes from that file and
sets up shell rc sourcing using a static command string with no
interpolated variables.

This eliminates command injection risk in the control-flow portion
of the remote command (for loop, grep, etc.) even if the base64
validation were ever bypassed, since user-controlled data never
appears in the same command string as shell control flow.

Fixes #2882

Agent: complexity-hunter
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: correct error handling + use mktemp for temp file

- Return 1 (not 0) when step 1 fails to avoid masking provisioning failures
- Use mktemp -t spawnrc.b64 to avoid race conditions on concurrent provisions

Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: propagate step 2 failure in provision.sh (return 1)

The else branch for step 2 (decode + shell rc setup) logged an error
but the function still returned 0, masking the failure. Now returns 1
so provisioning failures are correctly propagated.

Agent: pr-maintainer
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

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-22 20:44:33 -07:00 committed by GitHub
parent b0593952df
commit 4d08dbe2a7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -300,18 +300,38 @@ CLOUD_ENV
return 1
fi
# SECURITY: env_b64 is embedded directly in the command string. This is safe
# because env_b64 is validated above to contain only [A-Za-z0-9+/=] — the
# standard base64 alphabet — which cannot break out of single quotes or
# cause shell injection. Piping via stdin is NOT used because Sprite's exec
# driver replaces stdin with the command pipe, causing piped data to be lost.
# The \$ escapes below are for remote shell variables, not local ones.
if cloud_exec "${app_name}" "printf '%s' '${env_b64}' | base64 -d > ~/.spawnrc && chmod 600 ~/.spawnrc && \
# SECURITY: Split into two cloud_exec calls to separate data from commands.
# Step 1 writes the validated base64 payload to a remote temp file.
# Step 2 decodes from that file and sets up .spawnrc + shell rc sourcing.
# This avoids embedding variable data in a shell command string that contains
# control flow (for loops, conditionals), eliminating command injection risk
# even if the base64 validation were ever bypassed.
# Piping via stdin is NOT used because Sprite's exec driver replaces stdin
# with the command pipe, causing piped data to be lost.
# Step 1: Create a temp file and write base64 data to it on the remote host.
# env_b64 is validated above to contain only [A-Za-z0-9+/=] (base64 alphabet),
# which cannot break out of single quotes or cause shell injection.
local b64_tmp
b64_tmp=$(cloud_exec "${app_name}" "mktemp -t spawnrc.b64.XXXXXX" 2>/dev/null | tr -d '[:space:]')
if [ -z "${b64_tmp}" ]; then
log_err "Failed to create remote temp file for .spawnrc payload"
return 1
fi
if ! cloud_exec "${app_name}" "printf '%s' '${env_b64}' > '${b64_tmp}'" >/dev/null 2>&1; then
log_err "Failed to write .spawnrc payload to remote temp file"
return 1
fi
# Step 2: Decode from the temp file and set up shell rc sourcing.
# The only interpolated variable is b64_tmp (a mktemp path, safe characters only).
if cloud_exec "${app_name}" "base64 -d < '${b64_tmp}' > ~/.spawnrc && chmod 600 ~/.spawnrc && rm -f '${b64_tmp}' && \
for _rc in ~/.bashrc ~/.profile ~/.bash_profile; do \
grep -q 'source ~/.spawnrc' \"\$_rc\" 2>/dev/null || printf '%s\n' '[ -f ~/.spawnrc ] && source ~/.spawnrc' >> \"\$_rc\"; done" >/dev/null 2>&1; then
log_ok "Manual .spawnrc created successfully"
else
log_err "Failed to create manual .spawnrc"
return 1
fi
return 0
}