mirror of
https://github.com/OpenRouterTeam/spawn.git
synced 2026-05-19 08:01:17 +00:00
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:
parent
281ea2a74f
commit
f39ffd6e24
8 changed files with 108 additions and 159 deletions
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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..."
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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!"
|
||||
|
|
|
|||
|
|
@ -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!"
|
||||
|
|
|
|||
|
|
@ -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!"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue