Skip to content

Commit 763ce57

Browse files
authored
fix(ci): harden against template injection and credential exposure (#59)
## Summary This patch series addresses GitHub Actions security findings from the PSEC-923 sweep of `chainguard-dev/setup-chainctl`. Four commits are included. Two findings (`dangerous-triggers`, `github-env`) require human judgment and are documented in `manual-review.md` only. ## Patches ### 0001 — fix(ci): resolve template injection findings in action.yaml Fixes all 36 `template-injection` findings, concentrated in the composite `action.yaml` at the root of this repository. The action has three `run:` blocks; all were using inline `${{ inputs.* }}`, `${{ runner.* }}`, and `${{ env.* }}` expressions directly in shell code. **Step "Install chainctl"** (lines 94–132): Added `ENVIRONMENT: ${{ inputs.environment }}` to the existing `env:` block to cover the `inputs.environment` inline expansion. Replaced `${{ runner.os }}` and `${{ runner.arch }}` with the built-in `$RUNNER_OS` and `$RUNNER_ARCH` env vars that the GitHub Actions runner provides automatically — no extra `env:` entries needed for those. Also replaced `${{ env.CURL_RETRY_ALL_ERRORS }}` with `$CURL_RETRY_ALL_ERRORS`. **Step "Authenticate with Chainguard (assumed identity)"** (lines 134–178): Added two env vars to the existing `env:` block: - `LIBRARIES_HOST: ${{ inputs.libraries-host }}` - `APK_HOST: ${{ inputs.apk-host }}` Replaced all remaining `${{ env.IDENTITY }}`, `${{ env.VERBOSITY }}`, `${{ env.EXPORT_AUTH }}`, `${{ inputs.libraries-host }}`, and `${{ inputs.apk-host }}` inline expansions with `$VAR` shell references. Also quoted `-v="$VERBOSITY"` arguments that were previously unquoted (`-v=${{ env.VERBOSITY }}`). **Step "Authenticate with Chainguard (DEPRECATED invite-code)"** (lines 180–208): Added `APK_HOST: ${{ inputs.apk-host }}` to the existing `env:` block. Replaced all remaining `${{ env.* }}` and `${{ inputs.apk-host }}` inline expansions. Renamed the shell-local `AUDIENCE` variable to `AUDIENCE_VAL` to avoid clobbering the env var `AUDIENCE` that is set from `inputs.audience` in the same step. All `env:` blocks were already present before the fix; no placement changes were needed. All shell references use double-quoted `"$VAR"` form. ### 0002 — fix(ci): add persist-credentials: false to checkout steps Fixes 2 `artipacked` findings: - `.github/workflows/auth.yaml` — checkout followed only by `./` (the composite action under test) and `chainctl auth status`. No downstream git operations use the credential store. `persist-credentials: false` is correct. - `.github/workflows/test.yaml` — checkout followed only by `./` and `chainctl version`. No downstream git operations. `persist-credentials: false` is correct. ### 0003 — fix(ci): add pedantic persona, zizmor.yml suppressions, and dependabot cooldown Three related configuration changes: - **`.github/workflows/zizmor.yaml`**: Adds `persona: pedantic` to the `zizmor-action` step so that CI catches all template expansion findings, not only those from demonstrably attacker-controlled sources. - **`.github/zizmor.yml`** (new file): Suppresses four pedantic-only rules that have no exploitable security impact: - `concurrency-limits` — 4 findings; low security value - `anonymous-definition` — cosmetic/style only - `undocumented-permissions` — documentation style only - Also sets `dependabot-cooldown: days: 3` to match the cooldown added to `dependabot.yaml` - **`.github/dependabot.yaml`**: Adds `cooldown: default-days: 3` to prevent dependency churn from same-day upstream releases that may not yet have full test coverage. ### 0004 — fix(ci): resolve shellcheck findings in action.yaml Fixes three shellcheck findings in the composite `action.yaml` that were not covered by the template-injection patch: - **SC2046 + SC2164** (`cd $(mktemp -d)`, line 99): Quoted the subshell and added `|| exit` so a failed `cd` does not silently continue execution in the wrong directory. - **SC2086** (`>> $GITHUB_PATH`, line 129): Quoted the environment file path in the append redirection. - **SC2086** (`>> $GITHUB_ENV`, lines 181 and 213): Quoted the environment file path in the two `HTTP_AUTH` append redirections (one in the assumed-identity step, one in the DEPRECATED invite-code step). ## Findings Not Patched ### `dangerous-triggers` — `auth.yaml` uses `pull_request_target` with `id-token: write` Documented in `manual-review.md`. This requires human judgment about the intended authentication testing workflow. The risk is real: any fork PR triggers an OIDC token request for the hardcoded Chainguard identity. Options include switching to `pull_request` + GitHub environment protection, adding same-repo guards, or splitting into push-only auth testing. ### `github-env` — Three findings in `action.yaml` Documented in `manual-review.md`: 1. `action.yaml:129` — `echo "$(pwd)" >> $GITHUB_PATH`: The path is a mktemp directory, not attacker-controlled. **Low risk; no fix needed** (the unquoted `$GITHUB_PATH` was addressed by SC2086 fix in 0004, but the underlying `github-env` finding remains in scope for zizmor). 2. `action.yaml:181` — Writing `HTTP_AUTH` with APK token to `$GITHUB_ENV` in the assumed-identity step. The `APK_HOST` value is caller-supplied. Recommend validating `APK_HOST` and masking the token. **Medium risk.** 3. `action.yaml:213` — Same pattern in the DEPRECATED invite-code step. **Medium risk.** Best resolved by completing migration of callers away from invite codes. ## Statistics | Finding | Count | Action | |---------|-------|--------| | `template-injection` | 36 | Fixed in 0001 | | `artipacked` | 2 | Fixed in 0002 | | `concurrency-limits` | 4 | Suppressed in 0003 (no security impact) | | `dependabot-cooldown` | 1 | Addressed in 0003 | | shellcheck (SC2046/SC2164/SC2086) | 4 | Fixed in 0004 | | `github-env` | 3 | Manual review only | | `dangerous-triggers` | 1 | Manual review only | ## References - Campaign: PSEC-923 - zizmor version: 1.24.1 - Sweep date: 2026-05-02
1 parent 94783c5 commit 763ce57

6 files changed

Lines changed: 51 additions & 28 deletions

File tree

.github/dependabot.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ updates:
55
directory: "/"
66
schedule:
77
interval: weekly
8+
cooldown:
9+
default-days: 3
810
open-pull-requests-limit: 10
911
groups:
1012
actions:

.github/workflows/auth.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ jobs:
3333
issuer.enforce.dev:443
3434
3535
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
36+
with:
37+
persist-credentials: false
3638
- uses: ./
3739
with:
3840
identity: ce2d1984a010471142503340d670612d63ffb9f6/d05d31ba65ec54d1

.github/workflows/test.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,5 +45,7 @@ jobs:
4545
updates.cdn-apple.com:443
4646
4747
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
48+
with:
49+
persist-credentials: false
4850
- uses: ./
4951
- run: chainctl version

.github/workflows/zizmor.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,5 @@ jobs:
4444

4545
- name: Run zizmor
4646
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
47+
with:
48+
persona: pedantic

.github/zizmor.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
rules:
2+
dependabot-cooldown:
3+
config:
4+
days: 3
5+
anonymous-definition:
6+
disable: true
7+
undocumented-permissions:
8+
disable: true
9+
concurrency-limits:
10+
disable: true

action.yaml

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -95,38 +95,40 @@ runs:
9595
shell: bash
9696
env:
9797
CURL_RETRY_ALL_ERRORS: ${{ inputs.retry-all-errors }}
98+
ENVIRONMENT: ${{ inputs.environment }}
9899
run: |
99-
cd $(mktemp -d)
100+
cd "$(mktemp -d)" || exit
100101
101102
# Massage GitHub's values to the ones we expect.
102103
# https://docs.github.com/en/actions/learn-github-actions/contexts#runner-context
103-
os=$(echo "${{ runner.os }}" | tr '[:upper:]' '[:lower:]')
104+
# RUNNER_OS and RUNNER_ARCH are set automatically by the GitHub Actions runner.
105+
os=$(echo "$RUNNER_OS" | tr '[:upper:]' '[:lower:]')
104106
if [[ "${os}" == "macos" ]]; then
105107
os="darwin"
106108
fi
107109
108-
arch="${{ runner.arch }}"
110+
arch="$RUNNER_ARCH"
109111
if [[ "${arch}" == "X64" ]]; then
110112
arch="x86_64"
111113
elif [[ "${arch}" == "ARM64" ]]; then
112114
arch="arm64"
113115
fi
114116
115-
url="https://dl.${{ inputs.environment }}/chainctl/latest/chainctl_${os}_${arch}"
117+
url="https://dl.${ENVIRONMENT}/chainctl/latest/chainctl_${os}_${arch}"
116118
out="chainctl"
117119
if [[ "${os}" == "windows" ]]; then
118120
url="${url}.exe"
119121
out="${out}.exe"
120122
fi
121123
echo "Downloading chainctl from ${url}"
122124
123-
if [[ "${{ env.CURL_RETRY_ALL_ERRORS }}" != "true" ]]; then
125+
if [[ "$CURL_RETRY_ALL_ERRORS" != "true" ]]; then
124126
curl -o ./${out} -fsL --retry 5 --retry-delay 1 "${url}"
125127
else
126128
curl -o ./${out} -fsL --retry 5 --retry-delay 1 --retry-all-errors "${url}"
127129
fi
128130
chmod +x ./${out}
129-
echo "$(pwd)" >> $GITHUB_PATH
131+
echo "$(pwd)" >> "$GITHUB_PATH"
130132
131133
# Print the version of chainctl installed
132134
./${out} version
@@ -140,40 +142,42 @@ runs:
140142
IDENTITY: ${{ inputs.identity }}
141143
CHAINCTL_CONFIG: ${{ inputs.config-path }}
142144
EXPORT_AUTH: ${{ inputs.env-auth }}
145+
LIBRARIES_HOST: ${{ inputs.libraries-host }}
146+
APK_HOST: ${{ inputs.apk-host }}
143147
run: |
144-
echo "::group::Authenticating with Chainguard as ${{ env.IDENTITY }}"
145-
if chainctl auth login --identity "${{ env.IDENTITY }}" -v=${{ env.VERBOSITY }}; then
146-
echo Logged in as ${{ env.IDENTITY }}!
148+
echo "::group::Authenticating with Chainguard as $IDENTITY"
149+
if chainctl auth login --identity "$IDENTITY" -v="$VERBOSITY"; then
150+
echo Logged in as "$IDENTITY"!
147151
else
148-
echo "::error Unable to assume the identity ${{ env.IDENTITY }}."
152+
echo "::error Unable to assume the identity $IDENTITY."
149153
exit 1
150154
fi
151155
echo "::endgroup::"
152156
153-
echo "::group::Authenticating with ${{ inputs.libraries-host }} as ${{ env.IDENTITY }}"
154-
if ! chainctl auth login --identity "${{ env.IDENTITY }}" --audience ${{ inputs.libraries-host }} -v=${{ env.VERBOSITY }}; then
155-
echo "::error Unable to assume the identity ${{ env.IDENTITY }} for ${{ inputs.libraries-host }}."
157+
echo "::group::Authenticating with $LIBRARIES_HOST as $IDENTITY"
158+
if ! chainctl auth login --identity "$IDENTITY" --audience "$LIBRARIES_HOST" -v="$VERBOSITY"; then
159+
echo "::error Unable to assume the identity $IDENTITY for $LIBRARIES_HOST."
156160
exit 1
157161
fi
158162
echo "::endgroup::"
159163
160-
echo "::group::Authenticating with ${{ inputs.apk-host }} as ${{ env.IDENTITY }}"
161-
if ! chainctl auth login --identity "${{ env.IDENTITY }}" --audience ${{ inputs.apk-host }} -v=${{ env.VERBOSITY }}; then
162-
echo "::error Unable to assume the identity ${{ env.IDENTITY }} for ${{ inputs.apk-host }}."
164+
echo "::group::Authenticating with $APK_HOST as $IDENTITY"
165+
if ! chainctl auth login --identity "$IDENTITY" --audience "$APK_HOST" -v="$VERBOSITY"; then
166+
echo "::error Unable to assume the identity $IDENTITY for $APK_HOST."
163167
exit 1
164168
fi
165169
echo "::endgroup::"
166170
167171
echo "::group::Registering docker credential helper"
168-
if ! chainctl auth configure-docker --identity "${{ env.IDENTITY }}" -v=${{ env.VERBOSITY }}; then
169-
echo "::error Unable to register credential helper as ${{ env.IDENTITY }}."
172+
if ! chainctl auth configure-docker --identity "$IDENTITY" -v="$VERBOSITY"; then
173+
echo "::error Unable to register credential helper as $IDENTITY."
170174
exit 1
171175
fi
172176
echo "::endgroup::"
173177
174-
if [ "${{ env.EXPORT_AUTH }}" == "true" ]; then
175-
echo "::group::Exporting HTTP_AUTH environment variable for ${{ inputs.apk-host }}"
176-
echo HTTP_AUTH="basic:${{ inputs.apk-host }}:user:$(chainctl auth token --audience ${{ inputs.apk-host }})" >> $GITHUB_ENV
178+
if [ "$EXPORT_AUTH" == "true" ]; then
179+
echo "::group::Exporting HTTP_AUTH environment variable for $APK_HOST"
180+
echo HTTP_AUTH="basic:${APK_HOST}:user:$(chainctl auth token --audience "$APK_HOST")" >> "$GITHUB_ENV"
177181
echo "::endgroup::"
178182
fi
179183
@@ -187,24 +191,25 @@ runs:
187191
ENVIRONMENT: ${{ inputs.environment }}
188192
AUDIENCE: ${{ inputs.audience }}
189193
EXPORT_AUTH: ${{ inputs.env-auth }}
194+
APK_HOST: ${{ inputs.apk-host }}
190195
run: |
191196
echo "::warning::The use of invite codes with Github actions is deprecated, use assumed identities instead."
192197
193-
AUDIENCE="${{ env.AUDIENCE }}"
194-
if [[ -z "${AUDIENCE}" ]]; then
195-
AUDIENCE=issuer.${{ env.ENVIRONMENT }}
198+
AUDIENCE_VAL="$AUDIENCE"
199+
if [[ -z "${AUDIENCE_VAL}" ]]; then
200+
AUDIENCE_VAL="issuer.${ENVIRONMENT}"
196201
fi
197-
IDTOKEN=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=${AUDIENCE}" | jq -r '.value')
202+
IDTOKEN=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=${AUDIENCE_VAL}" | jq -r '.value')
198203
199204
# This will start failing once the invite code expires, which is why we have the login guard.
200-
if chainctl auth login --create-group=false --identity-token "${IDTOKEN}" --invite-code="${CHAINGUARD_INVITE_CODE}" -v=${{ env.VERBOSITY }}; then
205+
if chainctl auth login --create-group=false --identity-token "${IDTOKEN}" --invite-code="${CHAINGUARD_INVITE_CODE}" -v="$VERBOSITY"; then
201206
echo Logged in!
202207
else
203208
echo Failed to log in with invite code
204209
exit 1
205210
fi
206-
if [ "${{ env.EXPORT_AUTH }}" == "true" ]; then
207-
echo HTTP_AUTH="basic:${{ inputs.apk-host }}:user:$(chainctl auth token --audience ${{ inputs.apk-host }})" >> $GITHUB_ENV
211+
if [ "$EXPORT_AUTH" == "true" ]; then
212+
echo HTTP_AUTH="basic:${APK_HOST}:user:$(chainctl auth token --audience "$APK_HOST")" >> "$GITHUB_ENV"
208213
fi
209214
210215
- if: ${{ inputs.setup-python-keyring == 'true' }}

0 commit comments

Comments
 (0)