Przeglądaj źródła

CI: fix branch-name script injection, pin Railway CLI, explicit Vercel team

- github.head_ref reached the preview script by interpolation; a branch
  name is attacker-controlled. Pass it through env (Sonar S7630).
- Install the Railway CLI from the pinned npm package (@railway/cli
  5.35.0) in all jobs instead of piping install.sh into sh (S8482, S6506).
- The Vercel token is team-scoped and can't list teams (403), so the
  team (yasko) and client project (neo) are set in the workflow;
  domains.sh still discovers the team when VERCEL_TEAM is unset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Andrey Yasko 2 tygodni temu
rodzic
commit
15394bb006
2 zmienionych plików z 27 dodań i 14 usunięć
  1. 15 10
      .github/workflows/ci.yml
  2. 12 4
      scripts/railway/domains.sh

+ 15 - 10
.github/workflows/ci.yml

@@ -102,6 +102,8 @@ jobs:
       RAILWAY_PR_ENVIRONMENT: pr-${{ github.event.pull_request.number }}
       # unacms.app hosts: the backend on Railway, the NEO client (neo-ci on Vercel) in front of it
       VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
+      VERCEL_TEAM: yasko
+      CLIENT_PROJECT: neo
       PREVIEW_API_HOST: api-pr-${{ github.event.pull_request.number }}.unacms.app
       PREVIEW_CLIENT_HOST: pr-${{ github.event.pull_request.number }}.unacms.app
       UNA_PREVIEW_API_KEY: ${{ secrets.UNA_PREVIEW_API_KEY }}
@@ -124,9 +126,8 @@ jobs:
         chmod +x deploy/entrypoint.sh
 
     - name: Install Railway CLI
-      run: |
-        curl -fsSL https://railway.com/install.sh | sh
-        echo "$HOME/.railway/bin" >> "$GITHUB_PATH"
+      # Pinned npm package instead of piping an install script into sh.
+      run: npm install --global @railway/cli@5.35.0
 
     - name: Provision PR environment
       run: |
@@ -174,11 +175,14 @@ jobs:
     - name: Preview domains on unacms.app
       id: domains
       if: env.VERCEL_TOKEN != ''
+      env:
+        # Branch names are attacker-controlled: pass as data, never interpolate into the script.
+        HEAD_REF: ${{ github.head_ref }}
       run: |
         scripts/railway/domains.sh api-domain "$PREVIEW_API_HOST" "$RAILWAY_PR_ENVIRONMENT"
         # The client build of the same-named neo-ci branch when there is one, else production.
         # Either way NEO's proxy maps pr-<n>.unacms.app to api-pr-<n>.unacms.app.
-        scripts/railway/domains.sh client-alias "$PREVIEW_CLIENT_HOST" "${{ github.head_ref }}"
+        scripts/railway/domains.sh client-alias "$PREVIEW_CLIENT_HOST" "$HEAD_REF"
         echo "ok=1" >> "$GITHUB_OUTPUT"
 
     - name: Comment preview URL on PR
@@ -238,6 +242,7 @@ jobs:
       RAILWAY_PROJECT_ID: ${{ secrets.RAILWAY_PROJECT_ID }}
       RAILWAY_ENVIRONMENT: production
       VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
+      VERCEL_TEAM: yasko
       UNA_API_KEY: ${{ secrets.UNA_API_KEY }}
     steps:
     - uses: actions/checkout@v4
@@ -258,9 +263,8 @@ jobs:
         chmod +x deploy/entrypoint.sh
 
     - name: Install Railway CLI
-      run: |
-        curl -fsSL https://railway.com/install.sh | sh
-        echo "$HOME/.railway/bin" >> "$GITHUB_PATH"
+      # Pinned npm package instead of piping an install script into sh.
+      run: npm install --global @railway/cli@5.35.0
 
     - name: Configure production
       run: |
@@ -307,6 +311,8 @@ jobs:
       RAILWAY_BASE_ENVIRONMENT: ${{ secrets.RAILWAY_ENVIRONMENT }}
       RAILWAY_PR_ENVIRONMENT: pr-${{ github.event.pull_request.number }}
       VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
+      VERCEL_TEAM: yasko
+      CLIENT_PROJECT: neo
     steps:
     - uses: actions/checkout@v4
       with:
@@ -320,9 +326,8 @@ jobs:
         scripts/railway/domains.sh dns-remove "api-pr-${{ github.event.pull_request.number }}.unacms.app"
 
     - name: Install Railway CLI
-      run: |
-        curl -fsSL https://railway.com/install.sh | sh
-        echo "$HOME/.railway/bin" >> "$GITHUB_PATH"
+      # Pinned npm package instead of piping an install script into sh.
+      run: npm install --global @railway/cli@5.35.0
 
     - name: Delete PR environment
       run: |

+ 12 - 4
scripts/railway/domains.sh

@@ -9,11 +9,12 @@
 #   domains.sh client-unalias <host>
 #
 # Environment: VERCEL_TOKEN, RAILWAY_PROJECT_ID (api-domain), RAILWAY_API_TOKEN (railway CLI),
-#   DNS_ZONE (default unacms.app), CLIENT_PROJECT (default neo-ci).
+#   VERCEL_TEAM (team slug or id; found from DNS_ZONE ownership when unset),
+#   DNS_ZONE (default unacms.app), CLIENT_PROJECT (Vercel project of the NEO client, default neo).
 set -euo pipefail
 
 DNS_ZONE="${DNS_ZONE:-unacms.app}"
-CLIENT_PROJECT="${CLIENT_PROJECT:-neo-ci}"
+CLIENT_PROJECT="${CLIENT_PROJECT:-neo}"
 RAILWAY_SERVICE="${RAILWAY_SERVICE:-una}"
 VERCEL_API=https://api.vercel.com
 
@@ -23,7 +24,7 @@ die() { echo "domains.sh: $*" >&2; exit 1; }
 vercel() { # vercel <METHOD> <path> [json-body]
     local method=$1 path=$2 body=${3:-}
     local sep='?'; case "$path" in *\?*) sep='&' ;; esac
-    local url="$VERCEL_API$path${TEAM_ID:+${sep}teamId=$TEAM_ID}"
+    local url="$VERCEL_API$path${TEAM_ID:+${sep}teamId=$TEAM_ID}${TEAM_SLUG:+${sep}slug=$TEAM_SLUG}"
     if [ -n "$body" ]; then
         curl -fsS -X "$method" -H "Authorization: Bearer $VERCEL_TOKEN" -H 'Content-Type: application/json' -d "$body" "$url"
     else
@@ -32,10 +33,17 @@ vercel() { # vercel <METHOD> <path> [json-body]
 }
 
 # The Vercel team (or personal account) that owns DNS_ZONE; it also owns the client project.
+# A team-scoped token can't list teams (403), so VERCEL_TEAM is the normal path.
 TEAM_ID=""
+TEAM_SLUG=""
 resolve_team() {
     local team
-    for team in "" $(TEAM_ID="" vercel GET /v2/teams | jq -r '.teams[].id'); do
+    if [ -n "${VERCEL_TEAM:-}" ]; then
+        case "$VERCEL_TEAM" in team_*) TEAM_ID=$VERCEL_TEAM ;; *) TEAM_SLUG=$VERCEL_TEAM ;; esac
+        vercel GET "/v5/domains/$DNS_ZONE" >/dev/null || die "team $VERCEL_TEAM has no access to $DNS_ZONE"
+        return
+    fi
+    for team in "" $(TEAM_ID="" vercel GET /v2/teams 2>/dev/null | jq -r '.teams[].id'); do
         if TEAM_ID="$team" vercel GET "/v5/domains/$DNS_ZONE" >/dev/null 2>&1; then
             TEAM_ID="$team"
             return