fix: Prevent shell/Python injection in Codespaces, Render, and FluidStack (#252)

GitHub Codespaces scripts embedded API keys directly into heredocs sent
over SSH, allowing single-quote breakout for command injection. Fixed by
adding upload_file/run_server/inject_env_vars helpers to Codespaces lib
and using safe temp-file-upload pattern (matching Railway/Render).

Render claude.sh and openclaw.sh built JSON config via unescaped heredocs.
Fixed by using shared setup_claude_code_config/setup_openclaw_config
helpers which properly json_escape values.

FluidStack had triple-quote injection in SSH key registration (pub_key
embedded in Python triple-quotes) and missing single-quote validation in
create_server env var checks. Fixed by reading values via stdin/argv
instead of string interpolation, and added single-quote to validation.

Agent: security-auditor

Co-authored-by: A <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
A 2026-02-10 14:14:41 -08:00 committed by GitHub
parent 281ea2a74f
commit f39ffd6e24
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 108 additions and 159 deletions

View file

@ -84,11 +84,12 @@ fluidstack_check_ssh_key() {
local existing_keys
existing_keys=$(fluidstack_api GET "/ssh_keys")
# FluidStack returns SSH key fingerprints in MD5 format in "public_key_fingerprint" field
echo "${existing_keys}" | python3 -c "
import json, sys
echo "${existing_keys}" | _SPAWN_FINGERPRINT="${fingerprint}" python3 -c "
import json, sys, os
fingerprint = os.environ.get('_SPAWN_FINGERPRINT', '')
data = json.loads(sys.stdin.read())
for key in data.get('ssh_keys', []):
if '${fingerprint}' in key.get('public_key_fingerprint', '') or '${fingerprint}' in key.get('name', ''):
if fingerprint in key.get('public_key_fingerprint', '') or fingerprint in key.get('name', ''):
sys.exit(0)
sys.exit(1)
"
@ -98,17 +99,16 @@ sys.exit(1)
fluidstack_register_ssh_key() {
local key_name="${1}"
local pub_path="${2}"
local pub_key
pub_key=$(cat "${pub_path}")
local register_body
register_body=$(python3 -c "
import json
import json, sys
pub_key = sys.stdin.read().strip()
print(json.dumps({
'name': '${key_name}',
'public_key': '''${pub_key}'''
'name': sys.argv[1],
'public_key': pub_key
}))
")
" "${key_name}" < "${pub_path}")
local register_response
register_response=$(fluidstack_api POST "/ssh_keys" "${register_body}")
@ -186,20 +186,22 @@ create_server() {
local ssh_key_name="${FLUIDSTACK_SSH_KEY_NAME:-spawn-${USER}}"
# Block injection chars in string values (quotes, backslashes)
if [[ "${gpu_type}" =~ [\"\`\$\\] ]]; then log_error "Invalid FLUIDSTACK_GPU_TYPE: contains unsafe characters"; return 1; fi
if [[ "${ssh_key_name}" =~ [\"\`\$\\] ]]; then log_error "Invalid FLUIDSTACK_SSH_KEY_NAME: contains unsafe characters"; return 1; fi
if [[ "${gpu_type}" =~ [\"\'\`\$\\] ]]; then log_error "Invalid FLUIDSTACK_GPU_TYPE: contains unsafe characters"; return 1; fi
if [[ "${ssh_key_name}" =~ [\"\'\`\$\\] ]]; then log_error "Invalid FLUIDSTACK_SSH_KEY_NAME: contains unsafe characters"; return 1; fi
log_warn "Creating FluidStack instance '${name}' (GPU: ${gpu_type})..."
# Build instance creation request
# Build instance creation request safely via stdin
local create_body
create_body=$(python3 -c "
import json
import json, sys
parts = sys.stdin.read().strip().split('\n')
print(json.dumps({
'gpu_type': '${gpu_type}',
'ssh_key': '${ssh_key_name}'
'gpu_type': parts[0],
'ssh_key': parts[1]
}))
")
" <<< "${gpu_type}
${ssh_key_name}")
local response
response=$(fluidstack_api POST "/instances" "${create_body}")

View file

@ -31,12 +31,15 @@ fi
log_info "Codespace created: $CODESPACE"
# Set CODESPACE_NAME for upload_file/run_server/inject_env_vars helpers
CODESPACE_NAME="$CODESPACE"
# 3. Wait for codespace to be ready
wait_for_codespace "$CODESPACE"
# 4. Install Aider
log_warn "Installing Aider..."
run_in_codespace "$CODESPACE" "pip install aider-chat 2>/dev/null || pip3 install aider-chat"
run_server "pip install aider-chat 2>/dev/null || pip3 install aider-chat"
log_info "Aider installed"
# 5. Get OpenRouter API key
@ -50,16 +53,9 @@ fi
# 6. Get model preference
MODEL_ID=$(get_model_id_interactive "openrouter/auto" "Aider") || exit 1
# 7. Inject environment variables into ~/.bashrc
log_warn "Setting up environment variables..."
ENV_VARS="
export OPENROUTER_API_KEY='${OPENROUTER_API_KEY}'
"
run_in_codespace "$CODESPACE" "cat >> ~/.bashrc << 'ENVEOF'
$ENV_VARS
ENVEOF"
# 7. Inject environment variables via safe temp file upload
inject_env_vars \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}"
echo ""
log_info "GitHub Codespace setup completed successfully!"
@ -73,4 +69,4 @@ echo ""
sleep 1
# Launch Aider with model
run_in_codespace "$CODESPACE" "source ~/.bashrc && aider --model openrouter/${MODEL_ID}"
run_server "source ~/.bashrc && aider --model openrouter/${MODEL_ID}"

View file

@ -31,15 +31,18 @@ fi
log_info "Codespace created: $CODESPACE"
# Set CODESPACE_NAME for upload_file/run_server/inject_env_vars helpers
CODESPACE_NAME="$CODESPACE"
# 3. Wait for codespace to be ready
wait_for_codespace "$CODESPACE"
# 4. Install Claude Code
log_warn "Installing Claude Code..."
run_in_codespace "$CODESPACE" "curl -fsSL https://claude.ai/install.sh | bash"
run_server "curl -fsSL https://claude.ai/install.sh | bash"
# Verify installation
if ! run_in_codespace "$CODESPACE" "command -v claude" &>/dev/null; then
if ! run_server "command -v claude" &>/dev/null; then
log_error "Claude Code installation failed"
delete_codespace "$CODESPACE"
exit 1
@ -54,59 +57,18 @@ else
OPENROUTER_API_KEY=$(get_openrouter_api_key_oauth 5180)
fi
# 6. Inject environment variables into ~/.bashrc
log_warn "Setting up environment variables..."
# 6. Inject environment variables via safe temp file upload
inject_env_vars \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}" \
"ANTHROPIC_BASE_URL=https://openrouter.ai/api" \
"ANTHROPIC_AUTH_TOKEN=${OPENROUTER_API_KEY}" \
"ANTHROPIC_API_KEY=" \
"CLAUDE_CODE_SKIP_ONBOARDING=1" \
"CLAUDE_CODE_ENABLE_TELEMETRY=0" \
"PATH=\$HOME/.claude/local/bin:\$HOME/.bun/bin:\$PATH"
ENV_VARS="
export OPENROUTER_API_KEY='${OPENROUTER_API_KEY}'
export ANTHROPIC_BASE_URL='https://openrouter.ai/api'
export ANTHROPIC_AUTH_TOKEN='${OPENROUTER_API_KEY}'
export ANTHROPIC_API_KEY=''
export CLAUDE_CODE_SKIP_ONBOARDING='1'
export CLAUDE_CODE_ENABLE_TELEMETRY='0'
export PATH=\"\$HOME/.claude/local/bin:\$HOME/.bun/bin:\$PATH\"
"
run_in_codespace "$CODESPACE" "cat >> ~/.bashrc << 'ENVEOF'
$ENV_VARS
ENVEOF"
# 7. Configure Claude Code settings
log_warn "Configuring Claude Code..."
run_in_codespace "$CODESPACE" "mkdir -p ~/.claude"
# Create settings.json
SETTINGS_JSON="{
\"theme\": \"dark\",
\"editor\": \"vim\",
\"env\": {
\"CLAUDE_CODE_ENABLE_TELEMETRY\": \"0\",
\"ANTHROPIC_BASE_URL\": \"https://openrouter.ai/api\",
\"ANTHROPIC_AUTH_TOKEN\": \"${OPENROUTER_API_KEY}\"
},
\"permissions\": {
\"defaultMode\": \"bypassPermissions\",
\"dangerouslySkipPermissions\": true
}
}"
run_in_codespace "$CODESPACE" "cat > ~/.claude/settings.json << 'SETTINGSEOF'
$SETTINGS_JSON
SETTINGSEOF"
# Create global state file
GLOBAL_STATE="{
\"hasCompletedOnboarding\": true,
\"bypassPermissionsModeAccepted\": true
}"
run_in_codespace "$CODESPACE" "cat > ~/.claude.json << 'STATEEOF'
$GLOBAL_STATE
STATEEOF"
# Create empty CLAUDE.md
run_in_codespace "$CODESPACE" "touch ~/.claude/CLAUDE.md"
# 7. Configure Claude Code settings via shared helper
setup_claude_code_config "$OPENROUTER_API_KEY" "upload_file" "run_server"
echo ""
log_info "Setup complete. Opening interactive session..."

View file

@ -31,12 +31,15 @@ fi
log_info "Codespace created: $CODESPACE"
# Set CODESPACE_NAME for upload_file/run_server/inject_env_vars helpers
CODESPACE_NAME="$CODESPACE"
# 3. Wait for codespace to be ready
wait_for_codespace "$CODESPACE"
# 4. Install gptme
log_warn "Installing gptme..."
run_in_codespace "$CODESPACE" "pip install gptme 2>/dev/null || pip3 install gptme"
run_server "pip install gptme 2>/dev/null || pip3 install gptme"
log_info "gptme installed"
# 5. Get OpenRouter API key
@ -50,16 +53,9 @@ fi
# 6. Get model preference
MODEL_ID=$(get_model_id_interactive "openrouter/auto" "gptme") || exit 1
# 7. Inject environment variables into ~/.bashrc
log_warn "Setting up environment variables..."
ENV_VARS="
export OPENROUTER_API_KEY='${OPENROUTER_API_KEY}'
"
run_in_codespace "$CODESPACE" "cat >> ~/.bashrc << 'ENVEOF'
$ENV_VARS
ENVEOF"
# 7. Inject environment variables via safe temp file upload
inject_env_vars \
"OPENROUTER_API_KEY=${OPENROUTER_API_KEY}"
echo ""
log_info "GitHub Codespace setup completed successfully!"
@ -73,4 +69,4 @@ echo ""
sleep 1
# Launch gptme with model
run_in_codespace "$CODESPACE" "source ~/.bashrc && gptme -m openrouter/${MODEL_ID}"
run_server "source ~/.bashrc && gptme -m openrouter/${MODEL_ID}"

View file

@ -174,6 +174,57 @@ delete_codespace() {
}
}
# Upload a file to codespace via gh codespace cp
# Args: $1 = local path
# $2 = remote path
upload_file() {
local local_path="$1"
local remote_path="$2"
if [[ ! -f "$local_path" ]]; then
log_error "Local file not found: $local_path"
return 1
fi
if [[ -z "${CODESPACE_NAME:-}" ]]; then
log_error "CODESPACE_NAME not set. Call create_codespace first."
return 1
fi
gh codespace cp "$local_path" "${CODESPACE_NAME}:${remote_path}"
}
# Run a command on the codespace (wrapper matching other providers' interface)
run_server() {
local cmd="$1"
if [[ -z "${CODESPACE_NAME:-}" ]]; then
log_error "CODESPACE_NAME not set. Call create_codespace first."
return 1
fi
gh codespace ssh --codespace "$CODESPACE_NAME" -- bash -c "$cmd"
}
# Inject environment variables into shell config
# Writes to a temp file and uploads to avoid shell interpolation of values
inject_env_vars() {
log_warn "Injecting environment variables..."
local env_temp
env_temp=$(mktemp)
chmod 600 "${env_temp}"
track_temp_file "${env_temp}"
generate_env_config "$@" > "${env_temp}"
# Upload and append to .bashrc
upload_file "${env_temp}" "/tmp/env_config"
run_server "cat /tmp/env_config >> ~/.bashrc && rm /tmp/env_config"
log_info "Environment variables configured"
}
# Get codespace info
# Args: $1 = codespace name
get_codespace_info() {

View file

@ -54,48 +54,8 @@ inject_env_vars \
"CLAUDE_CODE_ENABLE_TELEMETRY=0" \
"PATH=\$HOME/.claude/local/bin:\$HOME/.bun/bin:\$PATH"
# 7. Configure Claude Code settings
log_warn "Configuring Claude Code..."
run_server "mkdir -p /root/.claude"
# Upload settings.json
SETTINGS_TEMP=$(mktemp)
chmod 600 "$SETTINGS_TEMP"
cat > "$SETTINGS_TEMP" << EOF
{
"theme": "dark",
"editor": "vim",
"env": {
"CLAUDE_CODE_ENABLE_TELEMETRY": "0",
"ANTHROPIC_BASE_URL": "https://openrouter.ai/api",
"ANTHROPIC_AUTH_TOKEN": "${OPENROUTER_API_KEY}"
},
"permissions": {
"defaultMode": "bypassPermissions",
"dangerouslySkipPermissions": true
}
}
EOF
upload_file "$SETTINGS_TEMP" "/root/.claude/settings.json"
rm "$SETTINGS_TEMP"
# Upload ~/.claude.json global state
GLOBAL_STATE_TEMP=$(mktemp)
chmod 600 "$GLOBAL_STATE_TEMP"
cat > "$GLOBAL_STATE_TEMP" << EOF
{
"hasCompletedOnboarding": true,
"bypassPermissionsModeAccepted": true
}
EOF
upload_file "$GLOBAL_STATE_TEMP" "/root/.claude.json"
rm "$GLOBAL_STATE_TEMP"
# Create empty CLAUDE.md
run_server "touch /root/.claude/CLAUDE.md"
# 7. Configure Claude Code settings via shared helper (uses json_escape for safe key handling)
setup_claude_code_config "$OPENROUTER_API_KEY" "upload_file" "run_server"
echo ""
log_info "Render service setup completed successfully!"

View file

@ -58,17 +58,15 @@ inject_env_vars \
"ANTHROPIC_API_KEY=${OPENROUTER_API_KEY}" \
"ANTHROPIC_BASE_URL=https://openrouter.ai/api"
# 9. Create nanoclaw .env file
# 9. Create nanoclaw .env file safely via temp file
log_warn "Configuring nanoclaw..."
DOTENV_TEMP=$(mktemp)
chmod 600 "$DOTENV_TEMP"
cat > "$DOTENV_TEMP" << EOF
ANTHROPIC_API_KEY=${OPENROUTER_API_KEY}
EOF
track_temp_file "$DOTENV_TEMP"
printf 'ANTHROPIC_API_KEY=%s\n' "${OPENROUTER_API_KEY}" > "$DOTENV_TEMP"
upload_file "$DOTENV_TEMP" "/root/nanoclaw/.env"
rm "$DOTENV_TEMP"
echo ""
log_info "Render service setup completed successfully!"

View file

@ -58,24 +58,8 @@ inject_env_vars \
"ANTHROPIC_BASE_URL=https://openrouter.ai/api" \
"PATH=\$HOME/.bun/bin:\$PATH"
# 9. Configure openclaw settings
log_warn "Configuring openclaw..."
run_server "mkdir -p /root/.openclaw"
# Create openclaw config with API key and model
OPENCLAW_CONFIG_TEMP=$(mktemp)
chmod 600 "$OPENCLAW_CONFIG_TEMP"
cat > "$OPENCLAW_CONFIG_TEMP" << EOF
{
"model": "${MODEL_ID}",
"apiKey": "${OPENROUTER_API_KEY}",
"baseUrl": "https://openrouter.ai/api"
}
EOF
upload_file "$OPENCLAW_CONFIG_TEMP" "/root/.openclaw/config.json"
rm "$OPENCLAW_CONFIG_TEMP"
# 9. Configure openclaw settings via shared helper (uses json_escape for safe key handling)
setup_openclaw_config "$OPENROUTER_API_KEY" "$MODEL_ID" "upload_file" "run_server"
echo ""
log_info "Render service setup completed successfully!"