From 2446e3bd4f08cb303a6b590be5ff5a0def2bf534 Mon Sep 17 00:00:00 2001 From: "Dylan M. Taylor" Date: Thu, 22 Oct 2026 11:40:53 -0400 Subject: [PATCH] security(actions): stop evaluating tool-version inputs as shell/pwsh source (#45) * security(actions): stop evaluating tool-version inputs as shell/pwsh source The install-wix, install-nfpm, install-trunk, and packaging-lint composite actions interpolated their *-version (and deb-arch) inputs directly into Bash/PowerShell source when constructing download URLs. A command substitution or subexpression in those values executed on the runner before any checksum verification (CWE-78). install-trunk additionally placed the version into a URL path component, so path-traversal segments could point both the archive and its fallback checksum at an attacker-controlled repository (CWE-23). Fixes: - Pass every version/arch input through the step `env:` and read it with quoted variable expansions instead of GitHub `${{ }}` interpolation in script bodies. - Validate a strict version grammar (and an allow-list for deb-arch) before the value is used, which also excludes slashes and other URL path-traversal characters so the download publisher stays fixed. No change to default versions, pinned checksums, or install behavior. * security(actions): anchor version validation against the whole value Address review feedback: - The bash version checks used `printf %s | grep -Eq '^...$'`, which matches line-by-line. A value like `2.49.0\ninvalid` passed and then flowed into the download URL. Switch to a bash `[[ =~ ^...$ ]]` test, which anchors the entire string and rejects any value containing a newline. Applied to install-nfpm, install-trunk, and packaging-lint. - The install-wix PowerShell check used `$`, which in .NET also matches before a trailing newline. Use `\z` (absolute end of input) so a trailing-newline value cannot bypass validation. --- actions/install-nfpm/action.yml | 22 ++++++++++++++++++---- actions/install-trunk/action.yml | 16 ++++++++++++++-- actions/install-wix/action.yml | 11 +++++++++-- actions/packaging-lint/action.yml | 15 +++++++++++---- 4 files changed, 52 insertions(+), 12 deletions(-) diff --git a/actions/install-nfpm/action.yml b/actions/install-nfpm/action.yml index 6f992ef..83b069c 100644 --- a/actions/install-nfpm/action.yml +++ b/actions/install-nfpm/action.yml @@ -16,11 +16,25 @@ runs: steps: - name: Download and verify nfpm shell: bash + env: + NFPM_VERSION: ${{ inputs.nfpm-version }} + DEB_ARCH: ${{ inputs.deb-arch }} run: | - deb_url="https://github.com/goreleaser/nfpm/releases/download/v${{ inputs.nfpm-version }}/nfpm_${{ inputs.nfpm-version }}_${{ inputs.deb-arch }}.deb" + version="$NFPM_VERSION" + arch="$DEB_ARCH" + if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then + echo "::error::invalid nfpm-version '$version'" + exit 1 + fi + case "$arch" in + amd64 | arm64) ;; + *) echo "::error::invalid deb-arch '$arch'"; exit 1 ;; + esac + + deb_url="https://github.com/goreleaser/nfpm/releases/download/v${version}/nfpm_${version}_${arch}.deb" curl --proto '=https' --proto-redir =https -fsSL -o "$RUNNER_TEMP/nfpm.deb" "$deb_url" - case "${{ inputs.nfpm-version }}:${{ inputs.deb-arch }}" in + case "${version}:${arch}" in "2.48.0:amd64") expected="787af76e172dd5fc7cedf4d7c5a090a2f62be9001557a4db1103e270f90ddefd" ;; @@ -34,8 +48,8 @@ runs: expected="27419eb382695a7942be8ad52259f3ec1854fad001b3ae4baed34ce39a223b97" ;; *) - curl --proto '=https' --proto-redir =https -fsSL -o "$RUNNER_TEMP/checksums.txt" "https://github.com/goreleaser/nfpm/releases/download/v${{ inputs.nfpm-version }}/checksums.txt" - expected=$(grep -E "nfpm_${{ inputs.nfpm-version }}_${{ inputs.deb-arch }}\.deb" "$RUNNER_TEMP/checksums.txt" | awk '{print $1}') + curl --proto '=https' --proto-redir =https -fsSL -o "$RUNNER_TEMP/checksums.txt" "https://github.com/goreleaser/nfpm/releases/download/v${version}/checksums.txt" + expected=$(grep -E "nfpm_${version}_${arch}\.deb" "$RUNNER_TEMP/checksums.txt" | awk '{print $1}') ;; esac diff --git a/actions/install-trunk/action.yml b/actions/install-trunk/action.yml index 33b9419..b1e8edc 100644 --- a/actions/install-trunk/action.yml +++ b/actions/install-trunk/action.yml @@ -12,10 +12,22 @@ runs: steps: - name: Install trunk shell: bash + env: + TRUNK_VERSION: ${{ inputs.trunk-version }} run: | - tar_url="https://github.com/trunk-rs/trunk/releases/download/v${{ inputs.trunk-version }}/trunk-x86_64-unknown-linux-gnu.tar.gz" + version="$TRUNK_VERSION" + # Restrict the version to a single safe release-tag component. This keeps + # the value from carrying slashes or other URL path-traversal segments + # that could redirect downloads to an attacker-controlled repository. The + # bash regex anchors the whole string, so values containing a newline are + # rejected (unlike a line-oriented grep check). + if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then + echo "::error::invalid trunk-version '$version'" + exit 1 + fi + tar_url="https://github.com/trunk-rs/trunk/releases/download/v${version}/trunk-x86_64-unknown-linux-gnu.tar.gz" curl --proto '=https' --proto-redir =https -fsSL -o "$RUNNER_TEMP/trunk.tar.gz" "$tar_url" - case "${{ inputs.trunk-version }}" in + case "$version" in "0.21.14") expected="f2b4680cd239693a646a2795e4633c625328d7b2a044fbe749fa3a2fe9e7036b" ;; diff --git a/actions/install-wix/action.yml b/actions/install-wix/action.yml index 288e4ea..f344826 100644 --- a/actions/install-wix/action.yml +++ b/actions/install-wix/action.yml @@ -16,9 +16,16 @@ runs: steps: - name: Install WiX shell: pwsh + env: + WIX_VERSION: ${{ inputs.wix-version }} + INSTALL_EXTENSIONS: ${{ inputs.install-extensions }} run: | - $version = "${{ inputs.wix-version }}" + $version = $env:WIX_VERSION + if ($version -notmatch '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?\z') { + Write-Output "::error::invalid wix-version '$version'" + exit 1 + } dotnet tool install --global wix --version $version - if ("${{ inputs.install-extensions }}" -eq "true") { + if ($env:INSTALL_EXTENSIONS -eq "true") { wix extension add -g "WixToolset.UI.wixext/$version" "WixToolset.Util.wixext/$version" } diff --git a/actions/packaging-lint/action.yml b/actions/packaging-lint/action.yml index 0ac9bdc..a40cd3d 100644 --- a/actions/packaging-lint/action.yml +++ b/actions/packaging-lint/action.yml @@ -12,18 +12,25 @@ runs: steps: - name: Install tools shell: bash + env: + ACTIONLINT_VERSION: ${{ inputs.actionlint-version }} run: | + version="$ACTIONLINT_VERSION" + if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]]; then + echo "::error::invalid actionlint-version '$version'" + exit 1 + fi sudo apt-get update sudo apt-get install -y --no-install-recommends shellcheck desktop-file-utils appstream libxml2-utils python3-yaml - tar_url="https://github.com/rhysd/actionlint/releases/download/v${{ inputs.actionlint-version }}/actionlint_${{ inputs.actionlint-version }}_linux_amd64.tar.gz" + tar_url="https://github.com/rhysd/actionlint/releases/download/v${version}/actionlint_${version}_linux_amd64.tar.gz" curl --proto '=https' --proto-redir =https -fsSL -o "$RUNNER_TEMP/actionlint.tar.gz" "$tar_url" - case "${{ inputs.actionlint-version }}" in + case "$version" in "1.7.12") expected="8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8" ;; *) - curl --proto '=https' --proto-redir =https -fsSL -o "$RUNNER_TEMP/checksums.txt" "https://github.com/rhysd/actionlint/releases/download/v${{ inputs.actionlint-version }}/actionlint_${{ inputs.actionlint-version }}_checksums.txt" - expected=$(grep -E "actionlint_${{ inputs.actionlint-version }}_linux_amd64\.tar\.gz" "$RUNNER_TEMP/checksums.txt" | awk '{print $1}') + curl --proto '=https' --proto-redir =https -fsSL -o "$RUNNER_TEMP/checksums.txt" "https://github.com/rhysd/actionlint/releases/download/v${version}/actionlint_${version}_checksums.txt" + expected=$(grep -E "actionlint_${version}_linux_amd64\.tar\.gz" "$RUNNER_TEMP/checksums.txt" | awk '{print $1}') ;; esac echo "$expected $RUNNER_TEMP/actionlint.tar.gz" | sha256sum -c -