From 6e28852aaea40ddd35f1162170c4c163cae264c7 Mon Sep 17 00:00:00 2001 From: chaodu-agent Date: Mon, 13 Jul 2026 19:42:15 -0400 Subject: [PATCH] fix(ci): harden release workflows against expression injection + SHA-pin actions Follow-up to a workflow-security audit across oablab/chi, oablab/ecsctl, and openabdev/ghpool (companion of openabdev/openab#1393/#1395/#1396, which fixed the same pattern in openab's release-pr.yml). release-pr.yml and release.yml expanded ${{ inputs.version }}, ${{ inputs.tag }}, and ${{ steps.*.outputs.* }} directly inside run: shell blocks across five jobs (resolve-version, create-release, build, docker, docker-manifest). Actions expands ${{ }} before bash ever sees the script, so a crafted version/tag string can break out of the shell string context. Threat model: release-pr.yml is workflow_dispatch-only (requires repo write access + carries an App token with contents:write + pull-requests:write via steps.app-token.outputs.token). release.yml triggers on 'push: tags' too, which is also gated on write access to push a tag. Same insider/leaked-credential threat model as openab's release-pr.yml. Fix: every inputs.*/steps.*.outputs.* reference in a run: block now goes through env: and is referenced as "$VAR" in shell, matching the canonical pattern already applied across openab's workflows. No behavioral change. Also SHA-pins dtolnay/rust-toolchain@stable and Swatinem/rust-cache@v2 across ci.yml, e2e.yml, and release.yml (same rationale as openabdev/openab#1396: a mutable tag/branch ref can be moved by anyone with push access to the upstream action repo; a commit SHA cannot). Version kept as a trailing comment for readability. --- .github/workflows/ci.yml | 4 +-- .github/workflows/e2e.yml | 4 +-- .github/workflows/release-pr.yml | 11 +++++--- .github/workflows/release.yml | 43 ++++++++++++++++++++------------ 4 files changed, 38 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef1d3ed..ad27252 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,10 +15,10 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable (2026-07-13) with: components: clippy - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 (2026-03-12) - name: cargo check run: cargo check - name: cargo clippy diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index f16ed9a..fab7fd7 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -44,8 +44,8 @@ jobs: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} owner: openabdev - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable (2026-07-13) + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 (2026-03-12) - name: cargo build run: cargo build - name: e2e — MCP reverse proxy against hosted endpoint diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml index a2727b7..b69263d 100644 --- a/.github/workflows/release-pr.yml +++ b/.github/workflows/release-pr.yml @@ -29,9 +29,11 @@ jobs: - name: Resolve version id: version + env: + INPUT_VERSION: ${{ inputs.version }} run: | - if [ -n "${{ inputs.version }}" ]; then - VERSION="${{ inputs.version }}" + if [ -n "$INPUT_VERSION" ]; then + VERSION="$INPUT_VERSION" else CURRENT=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/') IFS='.' read -r major minor patch <<< "$CURRENT" @@ -43,15 +45,16 @@ jobs: echo "::notice::Release version: ${VERSION}" - name: Update Cargo.toml version + env: + VERSION: ${{ steps.version.outputs.version }} run: | - VERSION="${{ steps.version.outputs.version }}" sed -i "s/^version = .*/version = \"${VERSION}\"/" Cargo.toml - name: Create release PR env: GH_TOKEN: ${{ steps.app-token.outputs.token }} + VERSION: ${{ steps.version.outputs.version }} run: | - VERSION="${{ steps.version.outputs.version }}" BRANCH="release/v${VERSION}" git config user.name "openab-app[bot]" git config user.email "274185012+openab-app[bot]@users.noreply.github.com" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b236020..e094f98 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,16 +20,19 @@ jobs: - uses: actions/checkout@v6 - name: Resolve tag id: tag + env: + INPUT_TAG: ${{ inputs.tag }} run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - echo "tag=${{ inputs.tag }}" >> "$GITHUB_OUTPUT" + if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then + echo "tag=${INPUT_TAG}" >> "$GITHUB_OUTPUT" else echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT" fi - name: Create release env: GH_TOKEN: ${{ github.token }} - run: gh release create "${{ steps.tag.outputs.tag }}" --generate-notes + TAG: ${{ steps.tag.outputs.tag }} + run: gh release create "$TAG" --generate-notes build: needs: create-release @@ -49,16 +52,18 @@ jobs: - name: Resolve tag id: tag shell: bash + env: + INPUT_TAG: ${{ inputs.tag }} run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - TAG="${{ inputs.tag }}" + if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then + TAG="$INPUT_TAG" else TAG="${GITHUB_REF_NAME}" fi echo "tag=${TAG}" >> "$GITHUB_OUTPUT" echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" - - uses: dtolnay/rust-toolchain@stable + - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable (2026-07-13) with: targets: ${{ matrix.target }} @@ -66,7 +71,7 @@ jobs: if: contains(matrix.target, 'musl') run: sudo apt-get update && sudo apt-get install -y musl-tools - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 (2026-03-12) with: key: ${{ matrix.target }} @@ -77,8 +82,9 @@ jobs: - name: Package shell: bash + env: + VERSION: ${{ steps.tag.outputs.version }} run: | - VERSION="${{ steps.tag.outputs.version }}" NAME="ghpool-${VERSION}-${{ matrix.os }}-${{ matrix.arch }}" mkdir -p dist cp target/${{ matrix.target }}/release/ghpool${{ matrix.ext }} dist/ @@ -90,10 +96,10 @@ jobs: - name: Upload to release env: GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.tag.outputs.tag }} + VERSION: ${{ steps.tag.outputs.version }} shell: bash run: | - TAG="${{ steps.tag.outputs.tag }}" - VERSION="${{ steps.tag.outputs.version }}" NAME="ghpool-${VERSION}-${{ matrix.os }}-${{ matrix.arch }}.tar.gz" gh release upload "$TAG" "$NAME" --clobber @@ -119,15 +125,17 @@ jobs: - name: Resolve tag id: tag + env: + INPUT_TAG: ${{ inputs.tag }} run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - TAG="${{ inputs.tag }}" + if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then + TAG="$INPUT_TAG" else TAG="${GITHUB_REF_NAME}" fi echo "tag=${TAG}" >> "$GITHUB_OUTPUT" echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" - echo "arch=$(echo ${{ matrix.platform }} | tr '/' '-')" >> "$GITHUB_OUTPUT" + echo "arch=$(echo "${{ matrix.platform }}" | tr '/' '-')" >> "$GITHUB_OUTPUT" - uses: docker/setup-buildx-action@v3 @@ -152,9 +160,11 @@ jobs: steps: - name: Resolve tag id: tag + env: + INPUT_TAG: ${{ inputs.tag }} run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - TAG="${{ inputs.tag }}" + if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then + TAG="$INPUT_TAG" else TAG="${GITHUB_REF_NAME}" fi @@ -167,8 +177,9 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Create and push manifest + env: + VERSION: ${{ steps.tag.outputs.version }} run: | - VERSION="${{ steps.tag.outputs.version }}" IMAGE="ghcr.io/${{ github.repository }}" docker buildx imagetools create -t ${IMAGE}:${VERSION} \ ${IMAGE}:${VERSION}-linux-amd64 \