From e3efa896db93001223dede04d3b744ae527c4c3f Mon Sep 17 00:00:00 2001 From: nullPointerEnjoyer Date: Sat, 19 Sep 2026 18:08:39 +0400 Subject: [PATCH] Harden release workflows against tag-derived script injection The tag-derived version was interpolated via ${{ }} expressions directly into run blocks in release_macos.yml, release_docker.yml and the create-release job of release.yml. A refname-legal hostile tag such as 1.2.3$(cmd) matches the '**[0-9]+.[0-9]+.[0-9]+*' push-tags filter and reaches these steps unvalidated, so bash executes the command substitution during expansion - before sign_and_notarize.sh runs and with the Apple signing credentials in the step environment. The Linux and Windows workflows already validate the version and pass it through environment variables for exactly this reason; the macOS job (which runs in parallel and has no needs gate), the docker job and the release body step were missed. Apply the same two controls everywhere: - Validate the extracted version against the shared X.Y.Z[-suffix] grammar up front and fail the job with a clear message on non-semver tags (docker also accepts the leading 'v' it passes to build.py). - Pass the version (and the Docker Hub username / hashes) through environment variables and quote them, instead of splicing ${{ }} expressions into run scripts, so a crafted tag/ref/input is treated as inert data and cannot inject shell into the steps. --- .github/workflows/release.yml | 16 ++++++++++++++-- .github/workflows/release_docker.yml | 20 +++++++++++++++++--- .github/workflows/release_macos.yml | 23 ++++++++++++++++++----- 3 files changed, 49 insertions(+), 10 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b0e830eaa1..2bea439c42 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -44,6 +44,12 @@ jobs: run: | VERSION=${GITHUB_REF#refs/tags/} VERSION=${VERSION#v} + # Validate the grammar up front, so a non-semver tag fails here with a clear + # message (the same grammar the Linux, Windows and macOS builders enforce). + if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then + echo "derived version '$VERSION' is not X.Y.Z[-suffix]; tag a semver release" >&2 + exit 1 + fi echo "VERSION=$VERSION" >> $GITHUB_OUTPUT - name: Download Artifacts @@ -60,11 +66,17 @@ jobs: done echo "EOF" >> $GITHUB_OUTPUT + # Note: the version and the hashes are passed through environment variables + # instead of direct ${{ }} interpolation into the run block, so that a crafted + # tag/ref/input cannot inject shell into the step. - name: Generate Release Body id: generate_body + env: + VERSION: ${{ steps.get_version.outputs.VERSION }} + HASHES: ${{ steps.generate_hashes.outputs.HASHES }} run: | echo "BODY<> $GITHUB_OUTPUT - echo "Release version ${{ steps.get_version.outputs.VERSION }}" >> $GITHUB_OUTPUT + echo "Release version $VERSION" >> $GITHUB_OUTPUT echo "" >> $GITHUB_OUTPUT echo "Please download the appropriate package for your system." >> $GITHUB_OUTPUT echo "" >> $GITHUB_OUTPUT @@ -74,7 +86,7 @@ jobs: echo "" >> $GITHUB_OUTPUT echo "File Hashes (SHA256):" >> $GITHUB_OUTPUT echo "\`\`\`" >> $GITHUB_OUTPUT - echo "${{ steps.generate_hashes.outputs.HASHES }}" >> $GITHUB_OUTPUT + echo "$HASHES" >> $GITHUB_OUTPUT echo "\`\`\`" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT diff --git a/.github/workflows/release_docker.yml b/.github/workflows/release_docker.yml index c52fdd4fa6..f7406653ab 100644 --- a/.github/workflows/release_docker.yml +++ b/.github/workflows/release_docker.yml @@ -35,15 +35,29 @@ jobs: id: version run: | if [[ $GITHUB_REF == refs/tags/* ]]; then - echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + VERSION=${GITHUB_REF#refs/tags/} + # Validate the grammar up front (the version flows into docker build/push + # command lines inside build.py), so a non-semver tag fails here with a + # clear message. The leading 'v' is allowed here; build.py strips it. + if ! echo "$VERSION" | grep -Eq '^v?[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then + echo "derived version '$VERSION' is not [v]X.Y.Z[-suffix]; tag a semver release" >&2 + exit 1 + fi + echo "VERSION=$VERSION" >> $GITHUB_OUTPUT else echo "VERSION=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT fi + # Note: the version (and the Docker Hub username) are passed through environment + # variables instead of direct ${{ }} interpolation into the run block, so that a + # crafted tag/ref/input cannot inject shell into the step. - name: Build and Push Docker images + env: + VERSION: ${{ steps.version.outputs.VERSION }} + DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }} run: | - python build-tools/docker/build.py --push --docker-hub-user ${{ secrets.DOCKERHUB_USERNAME }} \ - --version ${{ steps.version.outputs.VERSION }} --latest + python build-tools/docker/build.py --push --docker-hub-user "$DOCKERHUB_USERNAME" \ + --version "$VERSION" --latest env: DOCKER_BUILDKIT: 1 diff --git a/.github/workflows/release_macos.yml b/.github/workflows/release_macos.yml index e4adb1aa45..177bfe63a1 100644 --- a/.github/workflows/release_macos.yml +++ b/.github/workflows/release_macos.yml @@ -23,6 +23,13 @@ jobs: run: | VERSION=${GITHUB_REF#refs/tags/} VERSION=${VERSION#v} + # Validate the grammar up front, so a non-semver tag fails here with a clear + # message instead of reaching the signing step (the same grammar the Linux and + # Windows builders enforce). + if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then + echo "derived version '$VERSION' is not X.Y.Z[-suffix]; tag a semver release" >&2 + exit 1 + fi echo "VERSION=$VERSION" >> $GITHUB_OUTPUT echo "Version extracted: $VERSION" @@ -36,6 +43,9 @@ jobs: run: | cargo build --release --locked --target ${{ matrix.arch }}-apple-darwin --features trezor,ledger + # Note: the version (and the workflow input) are passed through environment + # variables instead of direct ${{ }} interpolation into the run blocks, so + # that a crafted tag/ref/input cannot inject shell into the steps. - name: Sign and Notarize GUI env: MACOS_CERTIFICATE_BASE64: ${{ secrets.MACOS_CERTIFICATE }} @@ -46,16 +56,19 @@ jobs: APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }} VERSION: ${{ steps.get_version.outputs.VERSION }} run: | - ./build-tools/osx/sign_and_notarize.sh ${{ matrix.arch }} ${{ steps.get_version.outputs.VERSION }} + ./build-tools/osx/sign_and_notarize.sh ${{ matrix.arch }} "$VERSION" - name: Package Mintlayer Node (without GUI) + env: + VERSION: ${{ steps.get_version.outputs.VERSION }} + BINARY_LIST: ${{ inputs.binary_list }} run: | - mkdir -p Mintlayer_Node_macos_${{ steps.get_version.outputs.VERSION }}_${{ matrix.arch }} - IFS=',' read -ra BINARIES <<< "${{ inputs.binary_list }}" + mkdir -p "Mintlayer_Node_macos_${VERSION}_${{ matrix.arch }}" + IFS=',' read -ra BINARIES <<< "$BINARY_LIST" for binary in "${BINARIES[@]}"; do - cp target/${{ matrix.arch }}-apple-darwin/release/$binary Mintlayer_Node_macos_${{ steps.get_version.outputs.VERSION }}_${{ matrix.arch }}/ + cp "target/${{ matrix.arch }}-apple-darwin/release/$binary" "Mintlayer_Node_macos_${VERSION}_${{ matrix.arch }}/" done - zip -r Mintlayer_Node_macos_${{ steps.get_version.outputs.VERSION }}_${{ matrix.arch }}.zip Mintlayer_Node_macos_${{ steps.get_version.outputs.VERSION }}_${{ matrix.arch }} + zip -r "Mintlayer_Node_macos_${VERSION}_${{ matrix.arch }}.zip" "Mintlayer_Node_macos_${VERSION}_${{ matrix.arch }}" - name: Upload DMG Artifact (GUI) uses: actions/upload-artifact@v4