diff --git a/.claude/skills/setup-trigger-service/refactor.sh b/.claude/skills/setup-trigger-service/refactor.sh index 2e0573f6..7c000bf3 100755 --- a/.claude/skills/setup-trigger-service/refactor.sh +++ b/.claude/skills/setup-trigger-service/refactor.sh @@ -15,6 +15,12 @@ cd "${REPO_ROOT}" SPAWN_ISSUE="${SPAWN_ISSUE:-}" SPAWN_REASON="${SPAWN_REASON:-manual}" +# Validate SPAWN_ISSUE is a positive integer to prevent command injection +if [[ -n "${SPAWN_ISSUE}" ]] && [[ ! "${SPAWN_ISSUE}" =~ ^[0-9]+$ ]]; then + echo "ERROR: SPAWN_ISSUE must be a positive integer, got: '${SPAWN_ISSUE}'" >&2 + exit 1 +fi + if [[ -n "${SPAWN_ISSUE}" ]]; then RUN_MODE="issue" WORKTREE_BASE="/tmp/spawn-worktrees/issue-${SPAWN_ISSUE}" diff --git a/.claude/skills/setup-trigger-service/trigger-server.ts b/.claude/skills/setup-trigger-service/trigger-server.ts index c11182f3..d18dc211 100644 --- a/.claude/skills/setup-trigger-service/trigger-server.ts +++ b/.claude/skills/setup-trigger-service/trigger-server.ts @@ -330,6 +330,14 @@ const server = Bun.serve({ const reason = url.searchParams.get("reason") ?? "manual"; const issue = url.searchParams.get("issue") ?? ""; + // Validate issue is a positive integer (prevents injection into shell commands) + if (issue && !/^\d+$/.test(issue)) { + return Response.json( + { error: "issue must be a positive integer" }, + { status: 400 } + ); + } + // Dedup: reject if a run for the same issue is already in progress if (issue) { for (const [, run] of runs) { diff --git a/hyperstack/lib/common.sh b/hyperstack/lib/common.sh index 344f4b5a..ca1f4d54 100644 --- a/hyperstack/lib/common.sh +++ b/hyperstack/lib/common.sh @@ -29,7 +29,7 @@ hyperstack_api() { local endpoint="$2" local body="${3:-}" # shellcheck disable=SC2154 - generic_cloud_api "$HYPERSTACK_API_BASE" "$HYPERSTACK_API_KEY" "$method" "$endpoint" "$body" "api_key" + generic_cloud_api "$HYPERSTACK_API_BASE" "$HYPERSTACK_API_KEY" "$method" "$endpoint" "$body" } test_hyperstack_api_key() { diff --git a/render/lib/common.sh b/render/lib/common.sh index a632e11f..2d90eeaf 100644 --- a/render/lib/common.sh +++ b/render/lib/common.sh @@ -133,26 +133,35 @@ _render_create_service() { local service_name="$1" log_warn "Creating Render web service: $service_name" + # Build JSON body safely via Python to prevent injection + local body + body=$(printf '%s' "$service_name" | python3 -c " +import json, sys +name = sys.stdin.read() +body = { + 'type': 'web_service', + 'name': name, + 'runtime': 'docker', + 'dockerfilePath': './Dockerfile', + 'repo': 'https://github.com/render-examples/docker-hello-world', + 'autoDeploy': 'yes', + 'serviceDetails': { + 'plan': 'starter', + 'region': 'oregon', + 'healthCheckPath': '/', + 'env': 'docker', + 'disk': None + } +} +print(json.dumps(body)) +") + # Create service via API local create_response create_response=$(curl -s -X POST "https://api.render.com/v1/services" \ -H "Authorization: Bearer ${RENDER_API_KEY}" \ -H "Content-Type: application/json" \ - -d "{ - \"type\": \"web_service\", - \"name\": \"${service_name}\", - \"runtime\": \"docker\", - \"dockerfilePath\": \"./Dockerfile\", - \"repo\": \"https://github.com/render-examples/docker-hello-world\", - \"autoDeploy\": \"yes\", - \"serviceDetails\": { - \"plan\": \"starter\", - \"region\": \"oregon\", - \"healthCheckPath\": \"/\", - \"env\": \"docker\", - \"disk\": null - } - }" 2>&1) + -d "$body" 2>&1) if echo "$create_response" | grep -q "error"; then log_error "Failed to create Render service"